diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 029661e..2a76f68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,25 +53,41 @@ jobs: MACOSX_DEPLOYMENT_TARGET: "10.15" CIBW_BUILD: "${{ matrix.os_dist.dist }}" CIBW_ARCHS_MACOS: "${{ matrix.os_dist.macosarch }}" - CIBW_TEST_REQUIRES: pytest stim sinter pygltflib + CIBW_TEST_REQUIRES: pytest stim~=1.14 sinter pygltflib CIBW_TEST_COMMAND: pytest {project}/src steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - run: python tools/overwrite_dev_versions_with_date.py - - run: mkdir -p output/stim - - run: mkdir -p output/stimcirq - - run: mkdir -p output/sinter - run: python -m pip install pybind11~=2.11.1 cibuildwheel~=2.16.2 setuptools - run: python -m cibuildwheel --print-build-identifiers - run: python -m cibuildwheel --output-dir output/chromobius + - uses: actions/upload-artifact@v4.4.0 + with: + name: "dist-chromobius-${{ matrix.os_dist.os }}-${{ matrix.os_dist.dist }}-${{ matrix.os_dist.macosarch }}" + path: dist/* + build_sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - run: python -m pip install setuptools pybind11~=2.11.1 + - run: python tools/overwrite_dev_versions_with_date.py + - run: mkdir output - run: python setup.py sdist - - run: mv dist/* output/chromobius - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4.4.0 with: - name: dist - path: | - ./output/chromobius/* + name: "dist-chromobius-sdist" + path: dist/*.tar.gz + merge_upload_artifacts: + needs: ["build_dist", "build_sdist"] + runs-on: ubuntu-latest + steps: + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: dist-chromobius + pattern: dist-chromobius-* check_sdist_installs: runs-on: ubuntu-latest steps: @@ -90,14 +106,19 @@ jobs: build_bazel: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: bazelbuild/setup-bazelisk@v1 + - uses: actions/checkout@v3 + - 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 :chromobius_test build_clang: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 @@ -115,14 +136,14 @@ jobs: perf: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: cmake . - run: make chromobius_perf -j 2 - run: out/chromobius_perf test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 @@ -136,7 +157,7 @@ jobs: test_o3: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: | cd .. git clone https://github.com/google/googletest.git -b release-1.12.1 @@ -152,7 +173,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 :chromobius_dev_wheel - run: pip install bazel-bin/chromobius-0.0.dev0-py3-none-any.whl - run: diff <(python tools/gen_chromobius_api_reference.py -dev) doc/chromobius_api_reference.md @@ -172,10 +198,15 @@ 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 :chromobius_dev_wheel - run: pip install bazel-bin/chromobius-0.0.dev0-py3-none-any.whl - - run: pip install pytest stim sinter pygltflib + - run: pip install pytest stim~=1.14 sinter pygltflib - run: pytest src - run: tools/doctest_proper.py --module chromobius upload_dev_release_to_pypi: diff --git a/CMakeLists.txt b/CMakeLists.txt index 99ab4d0..c667ff4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,7 @@ set(SIMD_WIDTH 128) include(FetchContent) FetchContent_Declare(stim GIT_REPOSITORY https://github.com/quantumlib/stim.git - GIT_TAG 3e38d12d0a0fb3022646b694137b733a4700d300) + GIT_TAG da4594c5ede00a063ec2b84bd830f846b5d097dd) FetchContent_GetProperties(stim) if(NOT stim_POPULATED) FetchContent_Populate(stim) diff --git a/WORKSPACE b/WORKSPACE index 29e3ebb..87d646a 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -3,7 +3,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "stim", - commit = "3e38d12d0a0fb3022646b694137b733a4700d300", + commit = "da4594c5ede00a063ec2b84bd830f846b5d097dd", remote = "https://github.com/quantumlib/stim.git", ) diff --git a/doc/chromobius.pyi b/doc/chromobius.pyi index f18e411..10f5ab6 100644 --- a/doc/chromobius.pyi +++ b/doc/chromobius.pyi @@ -48,6 +48,7 @@ class CompiledDecoder: 3 = Red Z 4 = Green Z 5 = Blue Z + -1 = Ignore this Detector 2. Rainbow triplets. Bulk errors with three symptoms in one basis should have one symptom of each color. Errors with three symptoms that repeat a color will cause an exception unless they can be decomposed @@ -161,6 +162,87 @@ class CompiledDecoder: >>> mistakes = np.count_nonzero(differences) >>> assert mistakes < shots / 5 """ + @staticmethod + def predict_weighted_obs_flips_from_dets_bit_packed( + dets: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + """Predicts observable flips and weights from detection events. + + The returned weight comes directly from the underlying call to pymatching, not + accounting for the lifting process. + + Args: + dets: A bit packed numpy array of detection event data. The array can either + be 1-dimensional (a single shot to decode) or 2-dimensional (multiple + shots to decode, with the first axis being the shot axis and the second + axis being the detection event byte axis). + + The array's dtype must be np.uint8. If you have an array of dtype + np.bool_, you have data that's not bit packed. You can pack it by + using `np.packbits(array, bitorder='little')`. But ideally you + should attempt to never have unpacked data in the first place, + since it's 8x larger which can be a large performance loss. For + example, stim's sampler methods all have a `bit_packed=True` argument + that cause them to return bit packed data. + + Returns: + A tuple (obs, weights). + Obs is a bit packed numpy array of observable flip data. + Weights is a numpy array (or scalar) of floats. + + If dets is a 1D array, then the result has: + obs.shape = (math.ceil(num_obs / 8),) + obs.dtype = np.uint8 + weights.shape = () + weights.dtype = np.float32 + If dets is a 2D array, then the result has: + shape = (dets.shape[0], math.ceil(num_obs / 8),) + dtype = np.uint8 + weights.shape = (dets.shape[0],) + weights.dtype = np.float32 + + To determine if the observable with index k was flipped in shot s, compute: + `bool((obs[s, k // 8] >> (k % 8)) & 1)` + + Example: + >>> import stim + >>> import chromobius + >>> import numpy as np + + >>> repetition_color_code = stim.Circuit(''' + ... # Apply noise. + ... X_ERROR(0.1) 0 1 2 3 4 5 6 7 + ... # Measure three-body stabilizers to catch errors. + ... MPP Z0*Z1*Z2 Z1*Z2*Z3 Z2*Z3*Z4 Z3*Z4*Z5 Z4*Z5*Z6 Z5*Z6*Z7 + ... + ... # Annotate detectors, with a coloring in the 4th coordinate. + ... DETECTOR(0, 0, 0, 2) rec[-6] + ... DETECTOR(1, 0, 0, 0) rec[-5] + ... DETECTOR(2, 0, 0, 1) rec[-4] + ... DETECTOR(3, 0, 0, 2) rec[-3] + ... DETECTOR(4, 0, 0, 0) rec[-2] + ... DETECTOR(5, 0, 0, 1) rec[-1] + ... + ... # Check on the message. + ... M 0 + ... OBSERVABLE_INCLUDE(0) rec[-1] + ... ''') + + >>> # Sample the circuit. + >>> shots = 4096 + >>> sampler = repetition_color_code.compile_detector_sampler() + >>> dets, actual_obs_flips = sampler.sample( + ... shots=shots, + ... separate_observables=True, + ... bit_packed=True, + ... ) + + >>> # Decode with Chromobius. + >>> dem = repetition_color_code.detector_error_model() + >>> decoder = chromobius.compile_decoder_for_dem(dem) + >>> result = decoder.predict_weighted_obs_flips_from_dets_bit_packed(dets) + >>> pred, weights = result + """ def compile_decoder_for_dem( dem: stim.DetectorErrorModel, ) -> chromobius.CompiledDecoder: @@ -178,6 +260,7 @@ def compile_decoder_for_dem( 3 = Red Z 4 = Green Z 5 = Blue Z + -1 = Ignore this Detector 2. Rainbow triplets. Bulk errors with three symptoms in one basis should have one symptom of each color. Errors with three symptoms that repeat a color will cause an exception unless they can be decomposed diff --git a/doc/chromobius_api_reference.md b/doc/chromobius_api_reference.md index 968137e..5a20c89 100644 --- a/doc/chromobius_api_reference.md +++ b/doc/chromobius_api_reference.md @@ -7,6 +7,7 @@ - [`chromobius.CompiledDecoder`](#chromobius.CompiledDecoder) - [`chromobius.CompiledDecoder.from_dem`](#chromobius.CompiledDecoder.from_dem) - [`chromobius.CompiledDecoder.predict_obs_flips_from_dets_bit_packed`](#chromobius.CompiledDecoder.predict_obs_flips_from_dets_bit_packed) + - [`chromobius.CompiledDecoder.predict_weighted_obs_flips_from_dets_bit_packed`](#chromobius.CompiledDecoder.predict_weighted_obs_flips_from_dets_bit_packed) ```python # Types used by the method definitions. from typing import overload, TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union @@ -37,6 +38,7 @@ def compile_decoder_for_dem( 3 = Red Z 4 = Green Z 5 = Blue Z + -1 = Ignore this Detector 2. Rainbow triplets. Bulk errors with three symptoms in one basis should have one symptom of each color. Errors with three symptoms that repeat a color will cause an exception unless they can be decomposed @@ -143,6 +145,7 @@ def from_dem( 3 = Red Z 4 = Green Z 5 = Blue Z + -1 = Ignore this Detector 2. Rainbow triplets. Bulk errors with three symptoms in one basis should have one symptom of each color. Errors with three symptoms that repeat a color will cause an exception unless they can be decomposed @@ -264,3 +267,91 @@ def predict_obs_flips_from_dets_bit_packed( >>> assert mistakes < shots / 5 """ ``` + + +```python +# chromobius.CompiledDecoder.predict_weighted_obs_flips_from_dets_bit_packed + +# (in class chromobius.CompiledDecoder) +@staticmethod +def predict_weighted_obs_flips_from_dets_bit_packed( + dets: np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + """Predicts observable flips and weights from detection events. + + The returned weight comes directly from the underlying call to pymatching, not + accounting for the lifting process. + + Args: + dets: A bit packed numpy array of detection event data. The array can either + be 1-dimensional (a single shot to decode) or 2-dimensional (multiple + shots to decode, with the first axis being the shot axis and the second + axis being the detection event byte axis). + + The array's dtype must be np.uint8. If you have an array of dtype + np.bool_, you have data that's not bit packed. You can pack it by + using `np.packbits(array, bitorder='little')`. But ideally you + should attempt to never have unpacked data in the first place, + since it's 8x larger which can be a large performance loss. For + example, stim's sampler methods all have a `bit_packed=True` argument + that cause them to return bit packed data. + + Returns: + A tuple (obs, weights). + Obs is a bit packed numpy array of observable flip data. + Weights is a numpy array (or scalar) of floats. + + If dets is a 1D array, then the result has: + obs.shape = (math.ceil(num_obs / 8),) + obs.dtype = np.uint8 + weights.shape = () + weights.dtype = np.float32 + If dets is a 2D array, then the result has: + shape = (dets.shape[0], math.ceil(num_obs / 8),) + dtype = np.uint8 + weights.shape = (dets.shape[0],) + weights.dtype = np.float32 + + To determine if the observable with index k was flipped in shot s, compute: + `bool((obs[s, k // 8] >> (k % 8)) & 1)` + + Example: + >>> import stim + >>> import chromobius + >>> import numpy as np + + >>> repetition_color_code = stim.Circuit(''' + ... # Apply noise. + ... X_ERROR(0.1) 0 1 2 3 4 5 6 7 + ... # Measure three-body stabilizers to catch errors. + ... MPP Z0*Z1*Z2 Z1*Z2*Z3 Z2*Z3*Z4 Z3*Z4*Z5 Z4*Z5*Z6 Z5*Z6*Z7 + ... + ... # Annotate detectors, with a coloring in the 4th coordinate. + ... DETECTOR(0, 0, 0, 2) rec[-6] + ... DETECTOR(1, 0, 0, 0) rec[-5] + ... DETECTOR(2, 0, 0, 1) rec[-4] + ... DETECTOR(3, 0, 0, 2) rec[-3] + ... DETECTOR(4, 0, 0, 0) rec[-2] + ... DETECTOR(5, 0, 0, 1) rec[-1] + ... + ... # Check on the message. + ... M 0 + ... OBSERVABLE_INCLUDE(0) rec[-1] + ... ''') + + >>> # Sample the circuit. + >>> shots = 4096 + >>> sampler = repetition_color_code.compile_detector_sampler() + >>> dets, actual_obs_flips = sampler.sample( + ... shots=shots, + ... separate_observables=True, + ... bit_packed=True, + ... ) + + >>> # Decode with Chromobius. + >>> dem = repetition_color_code.detector_error_model() + >>> decoder = chromobius.compile_decoder_for_dem(dem) + >>> result = decoder.predict_weighted_obs_flips_from_dets_bit_packed(dets) + >>> pred, weights = result + """ +``` diff --git a/doc/getting_started.ipynb b/doc/getting_started.ipynb index 0e24c83..66154a4 100644 --- a/doc/getting_started.ipynb +++ b/doc/getting_started.ipynb @@ -346,6 +346,7 @@ "- 3: red Z\n", "- 4: green Z\n", "- 5: blue Z.\n", + "- (-1): ignore this detector (e.g. it's from something beyond the color code).\n", "\n", "As you can maybe tell, making a circuit can be a very involved process!\n", "This notebook isn't really about making circuits, and you probably don't want to get too bogged down in this detail, so let's not get stuck on this.\n", diff --git a/file_lists/source_files_no_main b/file_lists/source_files_no_main index 9ecf062..366befd 100644 --- a/file_lists/source_files_no_main +++ b/file_lists/source_files_no_main @@ -14,6 +14,9 @@ src/chromobius/datatypes/color_basis.h src/chromobius/datatypes/conf.h src/chromobius/datatypes/rgb_edge.cc src/chromobius/datatypes/rgb_edge.h +src/chromobius/datatypes/stim_integration.cc +src/chromobius/datatypes/stim_integration.h +src/chromobius/datatypes/xor_vec.h src/chromobius/decode/decoder.cc src/chromobius/decode/decoder.h src/chromobius/decode/matcher_interface.h diff --git a/file_lists/test_files b/file_lists/test_files index ee61c0f..9e5b2f8 100644 --- a/file_lists/test_files +++ b/file_lists/test_files @@ -6,6 +6,8 @@ src/chromobius/commands/main_predict.test.cc src/chromobius/datatypes/atomic_error.test.cc src/chromobius/datatypes/color_basis.test.cc src/chromobius/datatypes/rgb_edge.test.cc +src/chromobius/datatypes/stim_integration.test.cc +src/chromobius/datatypes/xor_vec.test.cc src/chromobius/decode/decoder.test.cc src/chromobius/decode/decoder_integration.test.cc src/chromobius/graph/charge_graph.test.cc diff --git a/requirements.txt b/requirements.txt index 4442a6a..af00399 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ numpy pymatching ~= 2.0 pytest scipy -sinter ~= 1.12.0 -stim ~= 1.12.0 +sinter ~= 1.14 +stim ~= 1.14 pygltflib diff --git a/src/chromobius.h b/src/chromobius.h index 4441d1e..d9ad43e 100644 --- a/src/chromobius.h +++ b/src/chromobius.h @@ -11,6 +11,8 @@ #include "chromobius/datatypes/color_basis.h" #include "chromobius/datatypes/conf.h" #include "chromobius/datatypes/rgb_edge.h" +#include "chromobius/datatypes/stim_integration.h" +#include "chromobius/datatypes/xor_vec.h" #include "chromobius/decode/decoder.h" #include "chromobius/decode/matcher_interface.h" #include "chromobius/decode/pymatcher.h" diff --git a/src/chromobius/commands/main_all.cc b/src/chromobius/commands/main_all.cc index eb19d0d..a4261ca 100644 --- a/src/chromobius/commands/main_all.cc +++ b/src/chromobius/commands/main_all.cc @@ -14,10 +14,13 @@ #include "chromobius/commands/main_all.h" +#include +#include +#include + #include "chromobius/commands/main_benchmark.h" #include "chromobius/commands/main_describe_decoder.h" #include "chromobius/commands/main_predict.h" -#include "stim.h" using namespace chromobius; diff --git a/src/chromobius/commands/main_benchmark.cc b/src/chromobius/commands/main_benchmark.cc index b91f508..1e76f85 100644 --- a/src/chromobius/commands/main_benchmark.cc +++ b/src/chromobius/commands/main_benchmark.cc @@ -17,7 +17,6 @@ #include #include "chromobius/decode/decoder.h" -#include "stim.h" using namespace chromobius; diff --git a/src/chromobius/datatypes/atomic_error.cc b/src/chromobius/datatypes/atomic_error.cc index 7b14565..93bd2d0 100644 --- a/src/chromobius/datatypes/atomic_error.cc +++ b/src/chromobius/datatypes/atomic_error.cc @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + #include "chromobius/datatypes/atomic_error.h" using namespace chromobius; diff --git a/src/chromobius/datatypes/atomic_error.h b/src/chromobius/datatypes/atomic_error.h index bef0e51..a58d26a 100644 --- a/src/chromobius/datatypes/atomic_error.h +++ b/src/chromobius/datatypes/atomic_error.h @@ -17,10 +17,11 @@ #ifndef _CHROMOBIUS_ATOMIC_ERROR_H #define _CHROMOBIUS_ATOMIC_ERROR_H +#include +#include #include #include "chromobius/datatypes/color_basis.h" -#include "stim.h" namespace chromobius { @@ -59,6 +60,13 @@ struct AtomicErrorKey { inline AtomicErrorKey(node_offset_int det1, node_offset_int det2, node_offset_int det3) : dets(sort3(det1, det2, det3)) { } + inline AtomicErrorKey(std::span dets) + : dets(sort3( + dets.size() > 0 ? dets[0] : BOUNDARY_NODE, + dets.size() > 1 ? dets[1] : BOUNDARY_NODE, + dets.size() > 2 ? dets[2] : BOUNDARY_NODE)) { + assert(dets.size() <= 3); + } inline bool operator<(const AtomicErrorKey &other) const { for (size_t k = 0; k < 3; k++) { if (dets[k] != other.dets[k]) { diff --git a/src/chromobius/datatypes/atomic_error.test.cc b/src/chromobius/datatypes/atomic_error.test.cc index 579c6a4..391ed3b 100644 --- a/src/chromobius/datatypes/atomic_error.test.cc +++ b/src/chromobius/datatypes/atomic_error.test.cc @@ -14,6 +14,8 @@ #include "chromobius/datatypes/atomic_error.h" +#include + #include "gtest/gtest.h" using namespace chromobius; @@ -31,6 +33,28 @@ TEST(atomic_error, sort3) { } } +TEST(atomic_error, atomic_error_key_construct) { + AtomicErrorKey n{std::vector{}}; + ASSERT_EQ(n.dets[0], BOUNDARY_NODE); + ASSERT_EQ(n.dets[1], BOUNDARY_NODE); + ASSERT_EQ(n.dets[2], BOUNDARY_NODE); + + n = AtomicErrorKey{std::vector{1}}; + ASSERT_EQ(n.dets[0], 1); + ASSERT_EQ(n.dets[1], BOUNDARY_NODE); + ASSERT_EQ(n.dets[2], BOUNDARY_NODE); + + n = AtomicErrorKey{std::vector{1, 3}}; + ASSERT_EQ(n.dets[0], 1); + ASSERT_EQ(n.dets[1], 3); + ASSERT_EQ(n.dets[2], BOUNDARY_NODE); + + n = AtomicErrorKey{std::vector{4, 1, 3}}; + ASSERT_EQ(n.dets[0], 1); + ASSERT_EQ(n.dets[1], 3); + ASSERT_EQ(n.dets[2], 4); +} + TEST(atomic_error, atomic_error_key_basic) { AtomicErrorKey n{2, 3, 5}; ASSERT_EQ(n.dets[0], 2); diff --git a/src/chromobius/datatypes/color_basis.cc b/src/chromobius/datatypes/color_basis.cc index fc18664..aabb94b 100644 --- a/src/chromobius/datatypes/color_basis.cc +++ b/src/chromobius/datatypes/color_basis.cc @@ -19,7 +19,7 @@ using namespace chromobius; bool ColorBasis::operator==(const ColorBasis &other) const { - return color == other.color && basis == other.basis; + return color == other.color && basis == other.basis && ignored == other.ignored; } bool ColorBasis::operator!=(const ColorBasis &other) const { @@ -31,7 +31,11 @@ std::string ColorBasis::str() const { return ss.str(); } std::ostream &chromobius::operator<<(std::ostream &out, const ColorBasis &val) { - out << "ColorBasis{.color=" << val.color << ", .basis=" << val.basis << "}"; + out << "ColorBasis{.color=" << val.color << ", .basis=" << val.basis; + if (val.ignored) { + out << ", .ignored=true"; + } + out << "}"; return out; } @@ -91,35 +95,6 @@ std::ostream &chromobius::operator<<(std::ostream &out, const SubGraphCoord &val return out; } -ColorBasis chromobius::detector_instruction_to_color_basis( - const stim::DemInstruction &instruction, std::span coord_offsets) { - assert(instruction.type == stim::DemInstructionType::DEM_ERROR); - double c = -1; - if (instruction.arg_data.size() > 3) { - c = instruction.arg_data[3]; - if (coord_offsets.size() > 3) { - c += coord_offsets[3]; - } - } - int r = (int)c; - if (r < 0 || r >= 6 || r != c) { - throw std::invalid_argument( - "Expected all detectors to have at least 4 coordinates, with the 4th " - "identifying the basis and color " - "(RedX=0, GreenX=1, BlueX=2, RedZ=3, GreenZ=4, BlueZ=5), but got " + - instruction.str()); - } - constexpr std::array mapping{ - ColorBasis{Charge::R, Basis::X}, - ColorBasis{Charge::G, Basis::X}, - ColorBasis{Charge::B, Basis::X}, - ColorBasis{Charge::R, Basis::Z}, - ColorBasis{Charge::G, Basis::Z}, - ColorBasis{Charge::B, Basis::Z}, - }; - return mapping[r]; -} - std::tuple chromobius::mobius_node_to_detector( uint64_t mobius_node, std::span colors) { auto n = mobius_node >> 1; diff --git a/src/chromobius/datatypes/color_basis.h b/src/chromobius/datatypes/color_basis.h index fae48fc..1e076d9 100644 --- a/src/chromobius/datatypes/color_basis.h +++ b/src/chromobius/datatypes/color_basis.h @@ -19,9 +19,9 @@ #include #include +#include #include "chromobius/datatypes/conf.h" -#include "stim.h" namespace chromobius { @@ -65,6 +65,7 @@ enum Basis : uint8_t { struct ColorBasis { Charge color; Basis basis; + bool ignored = false; bool operator==(const ColorBasis &other) const; bool operator!=(const ColorBasis &other) const; std::string str() const; @@ -74,8 +75,6 @@ std::ostream &operator<<(std::ostream &out, const Charge &val); std::ostream &operator<<(std::ostream &out, const SubGraphCoord &val); std::ostream &operator<<(std::ostream &out, const Basis &val); -ColorBasis detector_instruction_to_color_basis( - const stim::DemInstruction &instruction, std::span coord_offsets); std::tuple mobius_node_to_detector( uint64_t mobius_node, std::span colors); uint64_t detector_to_mobius_node(node_offset_int node, SubGraphCoord subgraph, std::span colors); diff --git a/src/chromobius/datatypes/color_basis.test.cc b/src/chromobius/datatypes/color_basis.test.cc index 186acc7..c568383 100644 --- a/src/chromobius/datatypes/color_basis.test.cc +++ b/src/chromobius/datatypes/color_basis.test.cc @@ -30,23 +30,6 @@ TEST(types, color_basis_basic) { ASSERT_EQ(e.str(), "ColorBasis{.color=R, .basis=X}"); } -TEST(atomic_error, detector_instruction_to_color_basis) { - std::vector args{-1, -1, -1, 2}; - std::vector offsets{-3, -3, -3, 3, -2}; - stim::DemInstruction instruction{ - .arg_data = args, - .target_data = {}, - .type = stim::DemInstructionType::DEM_ERROR, - }; - ASSERT_EQ(detector_instruction_to_color_basis(instruction, offsets), (ColorBasis{Charge::B, Basis::Z})); - offsets[3] = 100; - ASSERT_THROW({ detector_instruction_to_color_basis(instruction, offsets); }, std::invalid_argument); - offsets[3] = 0.5; - ASSERT_THROW({ detector_instruction_to_color_basis(instruction, offsets); }, std::invalid_argument); - args[3] = 0.5; - ASSERT_EQ(detector_instruction_to_color_basis(instruction, offsets), (ColorBasis{Charge::G, Basis::X})); -} - TEST(atomic_error, mobius_node_to_detector_vs_detector_to_mobius_node) { std::vector colors; colors.resize(50); diff --git a/src/chromobius/datatypes/rgb_edge.h b/src/chromobius/datatypes/rgb_edge.h index 4e9f935..bf85c2f 100644 --- a/src/chromobius/datatypes/rgb_edge.h +++ b/src/chromobius/datatypes/rgb_edge.h @@ -17,12 +17,12 @@ #ifndef _CHROMOBIUS_DEM_GRAPH_H #define _CHROMOBIUS_DEM_GRAPH_H +#include #include #include #include "chromobius/datatypes/atomic_error.h" #include "chromobius/datatypes/color_basis.h" -#include "stim.h" namespace chromobius { @@ -43,6 +43,7 @@ struct RgbEdge { return (&red_node)[c - 1]; } inline node_offset_int &color_node(Charge c) { + assert(c != NEUTRAL); return (&red_node)[c - 1]; } diff --git a/src/chromobius/datatypes/stim_integration.cc b/src/chromobius/datatypes/stim_integration.cc new file mode 100644 index 0000000..b32a778 --- /dev/null +++ b/src/chromobius/datatypes/stim_integration.cc @@ -0,0 +1,67 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "chromobius/datatypes/stim_integration.h" + +#include + +using namespace chromobius; + +ColorBasis chromobius::detector_instruction_to_color_basis( + const stim::DemInstruction &instruction, std::span coord_offsets) { + assert(instruction.type == stim::DemInstructionType::DEM_DETECTOR); + bool failed = false; + double c = -2; + if (instruction.arg_data.size() > 3) { + c = instruction.arg_data[3]; + if (coord_offsets.size() > 3) { + c += coord_offsets[3]; + } + } else { + failed = true; + } + + int r = 0; + if (c < -1 || c > 5) { + failed = true; + } else { + r = (int)c; + if (r != c) { + failed = true; + } + } + if (failed) { + throw std::invalid_argument( + "Expected all detectors to have at least 4 coordinates, with the 4th " + "identifying the basis and color " + "(RedX=0, GreenX=1, BlueX=2, RedZ=3, GreenZ=4, BlueZ=5), but got " + + instruction.str()); + } + if (r == -1) { + return ColorBasis{ + .color=Charge::NEUTRAL, + .basis=Basis::UNKNOWN_BASIS, + .ignored=true, + }; + } + constexpr std::array mapping{ + ColorBasis{Charge::R, Basis::X}, + ColorBasis{Charge::G, Basis::X}, + ColorBasis{Charge::B, Basis::X}, + ColorBasis{Charge::R, Basis::Z}, + ColorBasis{Charge::G, Basis::Z}, + ColorBasis{Charge::B, Basis::Z}, + }; + return mapping[r]; +} diff --git a/src/chromobius/datatypes/stim_integration.h b/src/chromobius/datatypes/stim_integration.h new file mode 100644 index 0000000..0d7a50b --- /dev/null +++ b/src/chromobius/datatypes/stim_integration.h @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _CHROMOBIUS_STIM_INTEGRATION_H +#define _CHROMOBIUS_STIM_INTEGRATION_H + +#include +#include + +#include "chromobius/datatypes/conf.h" +#include "chromobius/datatypes/color_basis.h" +#include "stim.h" + +namespace chromobius { + +ColorBasis detector_instruction_to_color_basis( + const stim::DemInstruction &instruction, std::span coord_offsets); + +} // namespace chromobius + +#endif diff --git a/src/chromobius/datatypes/stim_integration.test.cc b/src/chromobius/datatypes/stim_integration.test.cc new file mode 100644 index 0000000..c2bf13a --- /dev/null +++ b/src/chromobius/datatypes/stim_integration.test.cc @@ -0,0 +1,38 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "chromobius/datatypes/stim_integration.h" + +#include "gtest/gtest.h" + +using namespace chromobius; + +TEST(atomic_error, detector_instruction_to_color_basis) { + std::vector args{-1, -1, -1, 2}; + std::vector offsets{-3, -3, -3, 3, -2}; + stim::DemInstruction instruction{ + .arg_data = args, + .target_data = {}, + .type = stim::DemInstructionType::DEM_DETECTOR, + }; + ASSERT_EQ(detector_instruction_to_color_basis(instruction, offsets), (ColorBasis{Charge::B, Basis::Z})); + offsets[3] = 100; + ASSERT_THROW({ detector_instruction_to_color_basis(instruction, offsets); }, std::invalid_argument); + offsets[3] = 0.5; + ASSERT_THROW({ detector_instruction_to_color_basis(instruction, offsets); }, std::invalid_argument); + args[3] = 0.5; + ASSERT_EQ(detector_instruction_to_color_basis(instruction, offsets), (ColorBasis{Charge::G, Basis::X})); + args[3] = -1.5; + ASSERT_EQ(detector_instruction_to_color_basis(instruction, offsets), (ColorBasis{Charge::NEUTRAL, Basis::UNKNOWN_BASIS, true})); +} diff --git a/src/chromobius/datatypes/xor_vec.h b/src/chromobius/datatypes/xor_vec.h new file mode 100644 index 0000000..c20b320 --- /dev/null +++ b/src/chromobius/datatypes/xor_vec.h @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _CHROMOBIUS_XOR_VEC_H +#define _CHROMOBIUS_XOR_VEC_H + +#include +#include + +namespace chromobius { + +template +inline std::span inplace_xor_sort(std::span items) { + std::sort(items.begin(), items.end()); + size_t new_size = 0; + for (size_t k = 0; k < items.size(); k++) { + if (new_size > 0 && items[k] == items[new_size - 1]) { + new_size--; + } else { + if (k != new_size) { + std::swap(items[new_size], items[k]); + } + new_size++; + } + } + return items.subspan(0, new_size); +} + +} // namespace chromobius + +#endif diff --git a/src/chromobius/datatypes/xor_vec.test.cc b/src/chromobius/datatypes/xor_vec.test.cc new file mode 100644 index 0000000..3e44707 --- /dev/null +++ b/src/chromobius/datatypes/xor_vec.test.cc @@ -0,0 +1,24 @@ +#include "chromobius/datatypes/xor_vec.h" + +#include "gtest/gtest.h" + +using namespace chromobius; + +TEST(xor_vec, inplace_xor_sort) { + auto f = [](std::vector v) -> std::vector { + std::span s = v; + auto r = inplace_xor_sort(s); + v.resize(r.size()); + return v; + }; + ASSERT_EQ(f({}), (std::vector({}))); + ASSERT_EQ(f({5}), (std::vector({5}))); + ASSERT_EQ(f({5, 5}), (std::vector({}))); + ASSERT_EQ(f({5, 5, 5}), (std::vector({5}))); + ASSERT_EQ(f({5, 5, 5, 5}), (std::vector({}))); + ASSERT_EQ(f({5, 4, 5, 5}), (std::vector({4, 5}))); + ASSERT_EQ(f({4, 5, 5, 5}), (std::vector({4, 5}))); + ASSERT_EQ(f({5, 5, 5, 4}), (std::vector({4, 5}))); + ASSERT_EQ(f({4, 5, 5, 4}), (std::vector({}))); + ASSERT_EQ(f({3, 5, 5, 4}), (std::vector({3, 4}))); +} diff --git a/src/chromobius/decode/decoder.cc b/src/chromobius/decode/decoder.cc index 938701d..cb9dc79 100644 --- a/src/chromobius/decode/decoder.cc +++ b/src/chromobius/decode/decoder.cc @@ -230,25 +230,28 @@ void Decoder::check_invariants() const { static void detection_events_to_mobius_detection_events( std::span bit_packed_detection_events, - std::vector *out_mobius_detection_events) { + std::vector *out_mobius_detection_events, + std::span node_colors) { // Derive the mobius matching problem. for (size_t k = 0; k < bit_packed_detection_events.size(); k++) { for (uint8_t b = bit_packed_detection_events[k], k2 = 0; b; b >>= 1, k2++) { if (b & 1) { auto d = k * 8 + k2; - out_mobius_detection_events->push_back(d * 2 + 0); - out_mobius_detection_events->push_back(d * 2 + 1); + if (!node_colors[d].ignored) { + out_mobius_detection_events->push_back(d * 2 + 0); + out_mobius_detection_events->push_back(d * 2 + 1); + } } } } } -obsmask_int Decoder::decode_detection_events(std::span bit_packed_detection_events) { +obsmask_int Decoder::decode_detection_events(std::span bit_packed_detection_events, float *weight_out) { // Derive and decode the mobius matching problem. sparse_det_buffer.clear(); matcher_edge_buf.clear(); - detection_events_to_mobius_detection_events(bit_packed_detection_events, &sparse_det_buffer); - matcher->match_edges(sparse_det_buffer, &matcher_edge_buf); + detection_events_to_mobius_detection_events(bit_packed_detection_events, &sparse_det_buffer, node_colors); + matcher->match_edges(sparse_det_buffer, &matcher_edge_buf, weight_out); // Write solution to stderr if requested. if (write_mobius_match_to_std_err) { diff --git a/src/chromobius/decode/decoder.h b/src/chromobius/decode/decoder.h index 6c4820a..3808222 100644 --- a/src/chromobius/decode/decoder.h +++ b/src/chromobius/decode/decoder.h @@ -116,7 +116,7 @@ struct Decoder { /// events. /// /// As part of running, this method clears the detection event data back to 0. - obsmask_int decode_detection_events(std::span bit_packed_detection_events); + obsmask_int decode_detection_events(std::span bit_packed_detection_events, float *weight_out = nullptr); private: /// Handles getting rid of excitation events within a cycle found by the diff --git a/src/chromobius/decode/decoder.perf.cc b/src/chromobius/decode/decoder.perf.cc index d97a975..dd61a4f 100644 --- a/src/chromobius/decode/decoder.perf.cc +++ b/src/chromobius/decode/decoder.perf.cc @@ -17,7 +17,6 @@ #include #include "chromobius/util.perf.h" -#include "stim.h" using namespace chromobius; diff --git a/src/chromobius/decode/decoder.test.cc b/src/chromobius/decode/decoder.test.cc index e728fd2..3437b4a 100644 --- a/src/chromobius/decode/decoder.test.cc +++ b/src/chromobius/decode/decoder.test.cc @@ -138,3 +138,47 @@ TEST(decoder, mobius_dem) { )DEM"); ASSERT_TRUE(decoder.mobius_dem.approx_equals(expected, 1e-5)); } + +TEST(decoder, ignores_detectors_annotated_with_minus_1) { + stim::DetectorErrorModel dem(R"DEM( + error(0.125) D0 D1 D2 + error(0.0625) D3 D4 D5 + error(0.0625) D0 D1 D2 D3 D4 D5 + error(0.25) D0 L1 + detector(0, 0, 0, 0) D0 + detector(0, 0, 0, 1) D1 + detector(0, 0, 0, 2) D2 + detector(0, 0, 0, 3) D3 + error(0.125) D20 D21 D22 D23 + detector(0, 0, 0, -1) D20 + detector(0, 0, 0, -1) D21 + detector(0, 0, 0, -1) D22 + detector(0, 0, 0, -1) D23 + repeat 2 { + detector(0, 0, 0, 4) D4 + shift_detectors(0, 0, 0, 1) 1 + } + )DEM"); + + Decoder decoder = Decoder::from_dem(dem, DecoderConfigOptions{.include_coords_in_mobius_dem=true}); + stim::DetectorErrorModel expected(R"DEM( + detector(0, 0, 0, 0, 2) D0 + detector(0, 0, 0, 0, 3) D1 + detector(0, 0, 0, 1, 1) D2 + detector(0, 0, 0, 1, 3) D3 + detector(0, 0, 0, 2, 1) D4 + detector(0, 0, 0, 2, 2) D5 + detector(0, 0, 0, 3, 2) D6 + detector(0, 0, 0, 3, 3) D7 + detector(0, 0, 0, 4, 1) D8 + detector(0, 0, 0, 4, 3) D9 + detector(0, 0, 0, 5, 1) D10 + detector(0, 0, 0, 5, 2) D11 + error(0.125) D1 D3 ^ D2 D4 ^ D0 D5 + error(0.0625) D7 D9 ^ D8 D10 ^ D6 D11 + error(0.0625) D1 D3 ^ D2 D4 ^ D0 D5 ^ D7 D9 ^ D8 D10 ^ D6 D11 + error(0.0625) D0 D1 + detector D47 + )DEM"); + ASSERT_TRUE(decoder.mobius_dem.approx_equals(expected, 1e-5)); +} diff --git a/src/chromobius/decode/decoder_integration.test.cc b/src/chromobius/decode/decoder_integration.test.cc index af21b22..0b53403 100644 --- a/src/chromobius/decode/decoder_integration.test.cc +++ b/src/chromobius/decode/decoder_integration.test.cc @@ -147,6 +147,7 @@ INSTANTIATE_TEST_SUITE_P( std::tuple{"midout_color_code_d9_r36_p1000.stim", 45}, std::tuple{"superdense_color_code_d5_r20_p1000.stim", 329}, // Including remnant errors reduces to 273? std::tuple{"phenom_color_code_d5_r5_p1000.stim", 0}, + std::tuple{"phenom_color_code_d5_r5_p1000_with_ignored.stim", 0}, std::tuple{"surface_code_d5_r5_p1000.stim", 3}, std::tuple{"rep_code_d9_transit_p10.stim", 1}, std::tuple{"rep_code_rg_d9_transit_p10.stim", 1}, @@ -173,3 +174,46 @@ TEST(FixCheck, euler_tour_ordering) { TEST(FixCheck, phenom_rgb_reps_for_last_layer) { decode_single("fix_check_1.stim", {8915, 8914, 8890}); } + +TEST(ConfigureDecoder, unreordered_detectors) { + FILE *f = open_test_data_file("old_fail_to_configure_cases/detector_unreorder_superdense_round.stim"); + stim::Circuit src_circuit = stim::Circuit::from_file(f); + fclose(f); + + auto src_dem = + stim::ErrorAnalyzer::circuit_to_detector_error_model(src_circuit, false, true, false, 0, false, false); + Decoder decoder = Decoder::from_dem(src_dem, DecoderConfigOptions{}); + decoder.check_invariants(); +} + +TEST(ConfigureDecoder, reordered_detectors) { + FILE *f = open_test_data_file("old_fail_to_configure_cases/detector_reorder_superdense_round.stim"); + stim::Circuit src_circuit = stim::Circuit::from_file(f); + fclose(f); + + auto src_dem = + stim::ErrorAnalyzer::circuit_to_detector_error_model(src_circuit, false, true, false, 0, false, false); + Decoder decoder = Decoder::from_dem(src_dem, DecoderConfigOptions{}); + decoder.check_invariants(); +} + +TEST(ConfigureDecoder, hard_config) { + FILE *f = open_test_data_file("old_fail_to_configure_cases/hard_config.stim"); + stim::Circuit src_circuit = stim::Circuit::from_file(f); + fclose(f); + + auto src_dem = + stim::ErrorAnalyzer::circuit_to_detector_error_model(src_circuit, false, true, false, 0, false, false); + Decoder decoder = Decoder::from_dem(src_dem, DecoderConfigOptions{}); + decoder.check_invariants(); +} + +TEST(ConfigureDecoder, fail_config) { + auto src_dem = stim::DetectorErrorModel(R"DEM( + error(0.1) D0 + detector D0 + )DEM"); + EXPECT_THROW({ + Decoder::from_dem(src_dem, DecoderConfigOptions{}); + }, std::invalid_argument); +} diff --git a/src/chromobius/decode/matcher_interface.h b/src/chromobius/decode/matcher_interface.h index ca2bd1e..fd217b4 100644 --- a/src/chromobius/decode/matcher_interface.h +++ b/src/chromobius/decode/matcher_interface.h @@ -41,7 +41,7 @@ struct MatcherInterface { /// is an edge. There should be no boundary edges in the result, since the mobius /// dem is guaranteed to not contain any boundary edges. virtual void match_edges( - const std::vector &mobius_detection_event_indices, std::vector *out_edge_buffer) = 0; + const std::vector &mobius_detection_event_indices, std::vector *out_edge_buffer, float *out_weight = nullptr) = 0; }; } // namespace chromobius diff --git a/src/chromobius/decode/pymatcher.cc b/src/chromobius/decode/pymatcher.cc index 8fe9e02..36ccdea 100644 --- a/src/chromobius/decode/pymatcher.cc +++ b/src/chromobius/decode/pymatcher.cc @@ -16,16 +16,29 @@ using namespace chromobius; -PymatchingMatcher::PymatchingMatcher() : pymatching_matcher() { +PymatchingMatcher::PymatchingMatcher() : pymatching_matcher(), weight_scaling_constant(1) { } PymatchingMatcher::PymatchingMatcher(const stim::DetectorErrorModel &dem) - : pymatching_matcher(pm::detector_error_model_to_mwpm(dem, 1 << 24, true)) { + : pymatching_matcher(pm::detector_error_model_to_mwpm(dem, 1 << 24, true)), weight_scaling_constant(pymatching_matcher.flooder.graph.normalising_constant) { + } void PymatchingMatcher::match_edges( - const std::vector &mobius_detection_event_indices, std::vector *out_edge_buffer) { + const std::vector &mobius_detection_event_indices, + std::vector *out_edge_buffer, + float *out_weight) { pm::decode_detection_events_to_edges(pymatching_matcher, mobius_detection_event_indices, *out_edge_buffer); + if (out_weight != nullptr) { + pm::total_weight_int w = 0; + auto &e = *out_edge_buffer; + for (size_t k = 0; k < e.size(); k += 2) { + auto &d1 = pymatching_matcher.search_flooder.graph.nodes[e[k]]; + auto &d2 = pymatching_matcher.search_flooder.graph.nodes[e[k + 1]]; + w += d2.neighbor_weights[d2.index_of_neighbor(&d1)]; + } + *out_weight = (float)(w / weight_scaling_constant); + } } std::unique_ptr PymatchingMatcher::configured_for_mobius_dem(const stim::DetectorErrorModel &dem) { diff --git a/src/chromobius/decode/pymatcher.h b/src/chromobius/decode/pymatcher.h index 765507e..23f9753 100644 --- a/src/chromobius/decode/pymatcher.h +++ b/src/chromobius/decode/pymatcher.h @@ -19,11 +19,13 @@ #include "chromobius/decode/matcher_interface.h" #include "pymatching/sparse_blossom/driver/mwpm_decoding.h" +#include "stim.h" namespace chromobius { struct PymatchingMatcher : MatcherInterface { pm::Mwpm pymatching_matcher; + double weight_scaling_constant; PymatchingMatcher(); PymatchingMatcher(const stim::DetectorErrorModel &dem); @@ -32,7 +34,7 @@ struct PymatchingMatcher : MatcherInterface { virtual std::unique_ptr configured_for_mobius_dem(const stim::DetectorErrorModel &dem) override; virtual void match_edges( - const std::vector &mobius_detection_event_indices, std::vector *out_edge_buffer) override; + const std::vector &mobius_detection_event_indices, std::vector *out_edge_buffer, float *out_weight = nullptr) override; }; } // namespace chromobius diff --git a/src/chromobius/graph/charge_graph.cc b/src/chromobius/graph/charge_graph.cc index eef80ef..e5e2bf8 100644 --- a/src/chromobius/graph/charge_graph.cc +++ b/src/chromobius/graph/charge_graph.cc @@ -18,6 +18,8 @@ #include #include +#include "chromobius/datatypes/xor_vec.h" + using namespace chromobius; bool ChargeGraphNode::operator==(const ChargeGraphNode &other) const { @@ -112,7 +114,7 @@ ChargeGraph ChargeGraph::from_atomic_errors( } // Form more graphlike edges by pairing overlapping errors. - stim::SparseXorVec xor_buf; + std::vector xor_buf; for (const auto &[_, neighbors] : node2neighbors) { for (size_t k1 = 0; k1 < neighbors.size(); k1++) { for (size_t k2 = k1 + 1; k2 < neighbors.size(); k2++) { @@ -125,24 +127,25 @@ ChargeGraph ChargeGraph::from_atomic_errors( // Merge the errors. xor_buf.clear(); - xor_buf.xor_item(e1.dets[0]); - xor_buf.xor_item(e1.dets[1]); - xor_buf.xor_item(e1.dets[2]); - xor_buf.xor_item(e2.dets[0]); - xor_buf.xor_item(e2.dets[1]); - xor_buf.xor_item(e2.dets[2]); + xor_buf.push_back(e1.dets[0]); + xor_buf.push_back(e1.dets[1]); + xor_buf.push_back(e1.dets[2]); + xor_buf.push_back(e2.dets[0]); + xor_buf.push_back(e2.dets[1]); + xor_buf.push_back(e2.dets[2]); + auto xor_items = inplace_xor_sort(std::span(xor_buf)); // Check if the resulting error is graphlike, pulling out its symptoms. node_offset_int a; node_offset_int b; - if (xor_buf.sorted_items.size() == 1) { - a = xor_buf.sorted_items[0]; + if (xor_items.size() == 1) { + a = xor_items[0]; b = BOUNDARY_NODE; } else if ( - xor_buf.sorted_items.size() == 2 || - (xor_buf.sorted_items.size() == 3 && xor_buf.sorted_items.back() == BOUNDARY_NODE)) { - a = xor_buf.sorted_items[0]; - b = xor_buf.sorted_items[1]; + xor_items.size() == 2 || + (xor_items.size() == 3 && xor_items.back() == BOUNDARY_NODE)) { + a = xor_items[0]; + b = xor_items[1]; } else { continue; } diff --git a/src/chromobius/graph/charge_graph.h b/src/chromobius/graph/charge_graph.h index 29be611..c2ab5f7 100644 --- a/src/chromobius/graph/charge_graph.h +++ b/src/chromobius/graph/charge_graph.h @@ -17,12 +17,12 @@ #ifndef _CHROMOBIUS_CHARGE_GRAPH_H #define _CHROMOBIUS_CHARGE_GRAPH_H +#include #include #include #include #include "chromobius/datatypes/rgb_edge.h" -#include "stim.h" namespace chromobius { diff --git a/src/chromobius/graph/choose_rgb_reps.cc b/src/chromobius/graph/choose_rgb_reps.cc index b11a7ca..9d1c385 100644 --- a/src/chromobius/graph/choose_rgb_reps.cc +++ b/src/chromobius/graph/choose_rgb_reps.cc @@ -35,7 +35,9 @@ std::vector chromobius::choose_rgb_reps_from_atomic_errors( size_t weight = 0; for (auto n : err.dets) { if (n != BOUNDARY_NODE) { - Charge c = node_colors[n].color; + ColorBasis cb = node_colors[n]; + assert(!cb.ignored); + Charge c = cb.color; rep.color_node(c) = n; rep.charge_flip ^= c; weight += 1; diff --git a/src/chromobius/graph/choose_rgb_reps.h b/src/chromobius/graph/choose_rgb_reps.h index d7e3fe7..87986cf 100644 --- a/src/chromobius/graph/choose_rgb_reps.h +++ b/src/chromobius/graph/choose_rgb_reps.h @@ -17,6 +17,8 @@ #ifndef _CHROMOBIUS_CHOOSE_RGB_REPS_H #define _CHROMOBIUS_CHOOSE_RGB_REPS_H +#include + #include "chromobius/datatypes/atomic_error.h" #include "chromobius/datatypes/rgb_edge.h" diff --git a/src/chromobius/graph/collect_atomic_errors.cc b/src/chromobius/graph/collect_atomic_errors.cc index fc917b7..8fa830b 100644 --- a/src/chromobius/graph/collect_atomic_errors.cc +++ b/src/chromobius/graph/collect_atomic_errors.cc @@ -16,41 +16,49 @@ using namespace chromobius; -static void extract_atomic_errors_from_dem_error_instruction_dets( +AtomicErrorKey chromobius::extract_atomic_errors_from_dem_error_instruction_dets( std::span dets, obsmask_int obs_flip, std::span node_colors, std::map *out_atomic_errors) { switch (dets.size()) { case 1: { - (*out_atomic_errors)[AtomicErrorKey{dets[0], BOUNDARY_NODE, BOUNDARY_NODE}] = obs_flip; - return; + AtomicErrorKey key{dets[0], BOUNDARY_NODE, BOUNDARY_NODE}; + (*out_atomic_errors)[key] = obs_flip; + return key; } case 2: { + AtomicErrorKey key{dets[0], dets[1], BOUNDARY_NODE}; ColorBasis c0 = node_colors[dets[0]]; ColorBasis c1 = node_colors[dets[1]]; if (c0.basis == c1.basis) { - (*out_atomic_errors)[AtomicErrorKey{dets[0], dets[1], BOUNDARY_NODE}] = obs_flip; + (*out_atomic_errors)[key] = obs_flip; + return key; } - return; + return AtomicErrorKey{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; } case 3: { + AtomicErrorKey key{dets[0], dets[1], dets[2]}; ColorBasis c0 = node_colors[dets[0]]; ColorBasis c1 = node_colors[dets[1]]; ColorBasis c2 = node_colors[dets[2]]; Charge net_charge = c0.color ^ c1.color ^ c2.color; if (net_charge == Charge::NEUTRAL && c0.basis == c1.basis && c1.basis == c2.basis) { - (*out_atomic_errors)[AtomicErrorKey{dets[0], dets[1], dets[2]}] = obs_flip; + (*out_atomic_errors)[key] = obs_flip; + return key; } - return; + return AtomicErrorKey{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; } + default: + return AtomicErrorKey{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; } } void chromobius::extract_obs_and_dets_from_error_instruction( stim::DemInstruction instruction, stim::SparseXorVec *out_xor_detectors_buffer, - obsmask_int *out_obs_flip) { + obsmask_int *out_obs_flip, + std::span node_colors) { out_xor_detectors_buffer->clear(); *out_obs_flip = 0; for (const auto &t : instruction.target_data) { @@ -65,7 +73,16 @@ void chromobius::extract_obs_and_dets_from_error_instruction( ss << std::numeric_limits::max(); throw std::invalid_argument(ss.str()); } - out_xor_detectors_buffer->xor_item((node_offset_int)u); + if (!node_colors[u].ignored) { + if (node_colors[u].color == NEUTRAL) { + throw std::invalid_argument( + "Expected all detectors to have at least 4 coordinates, with the 4th " + "identifying the basis and color " + "(RedX=0, GreenX=1, BlueX=2, RedZ=3, GreenZ=4, BlueZ=5), but got " + + instruction.str()); + } + out_xor_detectors_buffer->xor_item((node_offset_int)u); + } } else if (t.is_observable_id()) { if (t.raw_id() >= sizeof(obsmask_int) * 8) { std::stringstream ss; @@ -89,7 +106,7 @@ std::map chromobius::collect_atomic_errors( std::map result; dem.iter_flatten_error_instructions([&](stim::DemInstruction instruction) { - extract_obs_and_dets_from_error_instruction(instruction, &dets, &obs_flip); + extract_obs_and_dets_from_error_instruction(instruction, &dets, &obs_flip, node_colors); extract_atomic_errors_from_dem_error_instruction_dets(dets.sorted_items, obs_flip, node_colors, &result); }); diff --git a/src/chromobius/graph/collect_atomic_errors.h b/src/chromobius/graph/collect_atomic_errors.h index 92f8803..e9723af 100644 --- a/src/chromobius/graph/collect_atomic_errors.h +++ b/src/chromobius/graph/collect_atomic_errors.h @@ -17,6 +17,7 @@ #ifndef _CHROMOBIUS_COLLECT_ATOMIC_EDGES_H #define _CHROMOBIUS_COLLECT_ATOMIC_EDGES_H +#include #include #include "chromobius/datatypes/atomic_error.h" @@ -44,7 +45,14 @@ std::map collect_atomic_errors( void extract_obs_and_dets_from_error_instruction( stim::DemInstruction instruction, stim::SparseXorVec *out_xor_detectors_buffer, - obsmask_int *out_obs_flip); + obsmask_int *out_obs_flip, + std::span node_colors); + +AtomicErrorKey extract_atomic_errors_from_dem_error_instruction_dets( + std::span dets, + obsmask_int obs_flip, + std::span node_colors, + std::map *out_atomic_errors); } // namespace chromobius diff --git a/src/chromobius/graph/collect_composite_errors.cc b/src/chromobius/graph/collect_composite_errors.cc index ecc2bae..3007db0 100644 --- a/src/chromobius/graph/collect_composite_errors.cc +++ b/src/chromobius/graph/collect_composite_errors.cc @@ -23,7 +23,7 @@ static inline void try_grow_decomposition( AtomicErrorKey e2, std::span node_colors, const std::map &atomic_errors, - std::vector *out_atoms, + AtomicErrorKey *out_atom, int &best_score) { bool c1 = atomic_errors.contains(e1); bool c2 = atomic_errors.contains(e2); @@ -38,83 +38,32 @@ static inline void try_grow_decomposition( return; } - if (best_score > 0) { - out_atoms->pop_back(); - out_atoms->pop_back(); - } - out_atoms->push_back(e1); - out_atoms->push_back(e2); + *out_atom = c2 ? e2 : e1; best_score = score; } -static inline bool try_finish_decomposition( - int best_score, - obsmask_int obs_flip, - const std::map &atomic_errors, - std::vector *out_atoms, - std::map *out_remnants) { - assert(best_score == 0 || out_atoms->size() >= 2); - if (best_score == 1) { - AtomicErrorKey cur = (*out_atoms)[out_atoms->size() - 2]; - AtomicErrorKey rem = (*out_atoms)[out_atoms->size() - 1]; - (*out_remnants)[rem] = obs_flip ^ atomic_errors.at(cur); - } else if (best_score == 2) { - AtomicErrorKey cur = (*out_atoms)[out_atoms->size() - 1]; - AtomicErrorKey rem = (*out_atoms)[out_atoms->size() - 2]; - (*out_remnants)[rem] = obs_flip ^ atomic_errors.at(cur); - } - return best_score > 0; -} - -static bool decompose_single_basis_dets_into_atoms_helper_n2( +static AtomicErrorKey decompose_single_basis_dets_into_atoms_helper_n2( std::span dets, - obsmask_int obs_flip, - std::span node_colors, - const std::map &atomic_errors, - std::vector *out_atoms, - std::map *out_remnants) { - // Check if it's just directly included. - AtomicErrorKey e{dets[0], dets[1], BOUNDARY_NODE}; - if (atomic_errors.contains(e)) { - out_atoms->push_back(e); - return true; - } - - int best_score = 0; + const std::map &atomic_errors) { - // 1:1 decomposition. - for (size_t k1 = 0; k1 < dets.size(); k1++) { - try_grow_decomposition( - AtomicErrorKey{dets[k1], BOUNDARY_NODE, BOUNDARY_NODE}, - AtomicErrorKey{ - dets[0 + (k1 <= 0)], - BOUNDARY_NODE, - BOUNDARY_NODE, - }, - node_colors, - atomic_errors, - out_atoms, - best_score); + AtomicErrorKey a1{dets[0], BOUNDARY_NODE, BOUNDARY_NODE}; + AtomicErrorKey a2{dets[0], BOUNDARY_NODE, BOUNDARY_NODE}; + if (atomic_errors.contains(a1)) { + return a1; } - - return try_finish_decomposition(best_score, obs_flip, atomic_errors, out_atoms, out_remnants); + if (atomic_errors.contains(a2)) { + return a2; + } + return AtomicErrorKey{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; } -static bool decompose_single_basis_dets_into_atoms_helper_n3( +static AtomicErrorKey decompose_single_basis_dets_into_atoms_helper_n3( std::span dets, - obsmask_int obs_flip, std::span node_colors, - const std::map &atomic_errors, - std::vector *out_atoms, - std::map *out_remnants) { - // Check if it's just directly included. - AtomicErrorKey e{dets[0], dets[1], dets[2]}; - if (atomic_errors.contains(e)) { - out_atoms->push_back(e); - return true; - } + const std::map &atomic_errors) { int best_score = 0; + AtomicErrorKey result{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; // 1:2 decomposition. for (size_t k1 = 0; k1 < dets.size(); k1++) { @@ -127,21 +76,19 @@ static bool decompose_single_basis_dets_into_atoms_helper_n3( }, node_colors, atomic_errors, - out_atoms, + &result, best_score); } - return try_finish_decomposition(best_score, obs_flip, atomic_errors, out_atoms, out_remnants); + return result; } -static bool decompose_single_basis_dets_into_atoms_helper_n4( +static AtomicErrorKey decompose_single_basis_dets_into_atoms_helper_n4( std::span dets, - obsmask_int obs_flip, std::span node_colors, - const std::map &atomic_errors, - std::vector *out_atoms, - std::map *out_remnants) { + const std::map &atomic_errors) { int best_score = 0; + AtomicErrorKey result{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; // 2:2 decomposition. for (size_t k1 = 0; k1 < dets.size() && best_score < 2; k1++) { @@ -149,13 +96,13 @@ static bool decompose_single_basis_dets_into_atoms_helper_n4( try_grow_decomposition( AtomicErrorKey{dets[k1], dets[k2], BOUNDARY_NODE}, AtomicErrorKey{ - dets[0 + (k1 <= 0) + (k2 <= 0)], - dets[1 + (k1 <= 1) + (k2 <= 1)], + dets[0 + (k1 <= 0) + (k2 <= 1)], + dets[1 + (k1 <= 1) + (k2 <= 2)], BOUNDARY_NODE, }, node_colors, atomic_errors, - out_atoms, + &result, best_score); } } @@ -171,21 +118,20 @@ static bool decompose_single_basis_dets_into_atoms_helper_n4( }, node_colors, atomic_errors, - out_atoms, + &result, best_score); } - return try_finish_decomposition(best_score, obs_flip, atomic_errors, out_atoms, out_remnants); + return result; } -static bool decompose_single_basis_dets_into_atoms_helper_n5( +static AtomicErrorKey decompose_single_basis_dets_into_atoms_helper_n5( std::span dets, - obsmask_int obs_flip, std::span node_colors, - const std::map &atomic_errors, - std::vector *out_atoms, - std::map *out_remnants) { + const std::map &atomic_errors) { + int best_score = 0; + AtomicErrorKey result{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; // 2:3 decomposition. for (size_t k1 = 0; k1 < dets.size() && best_score < 2; k1++) { @@ -193,28 +139,27 @@ static bool decompose_single_basis_dets_into_atoms_helper_n5( try_grow_decomposition( AtomicErrorKey{dets[k1], dets[k2], BOUNDARY_NODE}, AtomicErrorKey{ - dets[0 + (k1 <= 0) + (k2 <= 0)], - dets[1 + (k1 <= 1) + (k2 <= 1)], - dets[2 + (k1 <= 2) + (k2 <= 2)], + dets[0 + (k1 <= 0) + (k2 <= 1)], + dets[1 + (k1 <= 1) + (k2 <= 2)], + dets[2 + (k1 <= 2) + (k2 <= 3)], }, node_colors, atomic_errors, - out_atoms, + &result, best_score); } } - return try_finish_decomposition(best_score, obs_flip, atomic_errors, out_atoms, out_remnants); + return result; } -static bool decompose_single_basis_dets_into_atoms_helper_n6( +static AtomicErrorKey decompose_single_basis_dets_into_atoms_helper_n6( std::span dets, - obsmask_int obs_flip, std::span node_colors, - const std::map &atomic_errors, - std::vector *out_atoms, - std::map *out_remnants) { + const std::map &atomic_errors) { + int best_score = 0; + AtomicErrorKey result{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; // 3:3 decomposition. for (size_t k1 = 0; k1 < dets.size() && best_score < 2; k1++) { @@ -223,51 +168,51 @@ static bool decompose_single_basis_dets_into_atoms_helper_n6( try_grow_decomposition( AtomicErrorKey{dets[k1], dets[k2], dets[k3]}, AtomicErrorKey{ - dets[0 + (k1 <= 0) + (k2 <= 0) + (k3 <= 0)], - dets[1 + (k1 <= 1) + (k2 <= 1) + (k3 <= 1)], - dets[2 + (k1 <= 2) + (k2 <= 2) + (k3 <= 2)], + dets[0 + (k1 <= 0) + (k2 <= 1) + (k3 <= 2)], + dets[1 + (k1 <= 1) + (k2 <= 2) + (k3 <= 3)], + dets[2 + (k1 <= 2) + (k2 <= 3) + (k3 <= 4)], }, node_colors, atomic_errors, - out_atoms, + &result, best_score); } } } - return try_finish_decomposition(best_score, obs_flip, atomic_errors, out_atoms, out_remnants); + return result; } -static bool decompose_single_basis_dets_into_atoms( +static AtomicErrorKey decompose_single_basis_dets_into_atoms( std::span dets, - obsmask_int obs_flip, std::span node_colors, - const std::map &atomic_errors, - std::vector *out_atoms, - std::map *out_remnants) { + const std::map &atomic_errors) { + if (dets.size() <= 3) { + AtomicErrorKey solo{dets}; + if (atomic_errors.contains(solo)) { + return solo; + } + } + switch (dets.size()) { - case 0: - return true; - case 1: - out_atoms->push_back(AtomicErrorKey{dets[0], BOUNDARY_NODE, BOUNDARY_NODE}); - return atomic_errors.contains(out_atoms->back()); case 2: return decompose_single_basis_dets_into_atoms_helper_n2( - dets, obs_flip, node_colors, atomic_errors, out_atoms, out_remnants); + dets, atomic_errors); case 3: return decompose_single_basis_dets_into_atoms_helper_n3( - dets, obs_flip, node_colors, atomic_errors, out_atoms, out_remnants); + dets, node_colors, atomic_errors); case 4: return decompose_single_basis_dets_into_atoms_helper_n4( - dets, obs_flip, node_colors, atomic_errors, out_atoms, out_remnants); + dets, node_colors, atomic_errors); case 5: return decompose_single_basis_dets_into_atoms_helper_n5( - dets, obs_flip, node_colors, atomic_errors, out_atoms, out_remnants); + dets, node_colors, atomic_errors); case 6: return decompose_single_basis_dets_into_atoms_helper_n6( - dets, obs_flip, node_colors, atomic_errors, out_atoms, out_remnants); + dets, node_colors, atomic_errors); default: - return false; + // Failed to decompose. + return AtomicErrorKey{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; } } @@ -288,18 +233,8 @@ static void decompose_dets_into_atoms( buf_z_detectors->clear(); for (const auto &t : dets) { auto cb = node_colors[t]; - int c = (int)cb.color - 1; - int b = (int)cb.basis - 1; - if (c < 0 || c >= 3 || b < 0 || b >= 2) { - std::stringstream ss; - ss << "Detector D" << t << " originating from instruction (after shifting) '" - << instruction_for_error_message << "'"; - ss << " is missing coordinate data indicating its color and basis.\n"; - ss << "Every detector used in an error must have a 4th coordinate in " - "[0,6) where RedX=0, GreenX=1, BlueX=2, RedZ=3, GreenZ=4, BlueZ=5."; - throw std::invalid_argument(ss.str()); - } - if (b == 0) { + assert(!cb.ignored); + if (cb.basis == Basis::X) { buf_x_detectors->push_back(t); } else { buf_z_detectors->push_back(t); @@ -308,19 +243,69 @@ static void decompose_dets_into_atoms( // Split into atomic errors. out_atoms->clear(); - bool x_worked = decompose_single_basis_dets_into_atoms( - *buf_x_detectors, obs_flip, node_colors, atomic_errors, out_atoms, out_remnants); - bool z_worked = decompose_single_basis_dets_into_atoms( - *buf_z_detectors, obs_flip, node_colors, atomic_errors, out_atoms, out_remnants); - if (!(x_worked && z_worked) && !ignore_decomposition_failures) { + for (size_t rep = 0; rep < 3; rep++) { + for (auto *basis_dets : std::array *, 2>{buf_x_detectors, buf_z_detectors}) { + AtomicErrorKey removed{BOUNDARY_NODE, BOUNDARY_NODE, BOUNDARY_NODE}; + if (rep == 2) { + removed = extract_atomic_errors_from_dem_error_instruction_dets( + *basis_dets, + obs_flip, + node_colors, + out_remnants); + } else { + removed = decompose_single_basis_dets_into_atoms(*basis_dets, node_colors, atomic_errors); + } + + auto w = removed.weight(); + if (w) { + for (size_t k = 0; k < w; k++) { + // Remove matching item. + for (size_t i = 0; i < basis_dets->size(); i++) { + if ((*basis_dets)[i] == removed.dets[k]) { + (*basis_dets)[i] = basis_dets->back(); + basis_dets->pop_back(); + break; + } + } + } + if (atomic_errors.contains(removed)) { + obs_flip ^= atomic_errors.at(removed); + } else { + obs_flip ^= out_remnants->at(removed); + } + out_atoms->push_back(removed); + } + } + } + if ((!buf_x_detectors->empty() || !buf_z_detectors->empty()) && !ignore_decomposition_failures) { std::stringstream ss; ss << "Failed to decompose a complex error instruction into basic errors.\n"; ss << " The instruction (after shifting): " + instruction_for_error_message.str() << "\n"; - if (!x_worked) { - ss << " The undecomposed X detectors: [" << stim::comma_sep(*buf_x_detectors) << "]\n"; + ss << " The undecomposed X detectors: [" << stim::comma_sep(*buf_x_detectors) << "]\n"; + ss << " The undecomposed Z detectors: [" << stim::comma_sep(*buf_z_detectors) << "]\n"; + for (auto e : *out_atoms) { + ss << " Decomposed piece:"; + for (auto d : e.dets) { + if (d != BOUNDARY_NODE) { + ss << " D" << d; + } + } + obsmask_int l = atomic_errors.contains(e) ? atomic_errors.at(e) : out_remnants->at(e); + for (size_t k = 0; k < sizeof(obsmask_int) * 8; k++) { + if ((l >> k) & 1) { + ss << " L" << k; + } + } + ss << "\n"; } - if (!z_worked) { - ss << " The undecomposed Z detectors: [" << stim::comma_sep(*buf_z_detectors) << "]\n"; + if (obs_flip) { + ss << " The undecomposed observable mask:"; + for (size_t k = 0; k < sizeof(obsmask_int) * 8; k++) { + if ((obs_flip >> k) & 1) { + ss << " L" << k; + } + } + ss << "\n"; } ss << " Detector data:\n"; std::set ds; @@ -366,7 +351,7 @@ void chromobius::collect_composite_errors_and_remnants_into_mobius_dem( dem.iter_flatten_error_instructions([&](stim::DemInstruction instruction) { obsmask_int obs_flip; - extract_obs_and_dets_from_error_instruction(instruction, &dets, &obs_flip); + extract_obs_and_dets_from_error_instruction(instruction, &dets, &obs_flip, node_colors); decompose_dets_into_atoms( dets.sorted_items, diff --git a/src/chromobius/graph/collect_composite_errors.h b/src/chromobius/graph/collect_composite_errors.h index 1800dbf..c375613 100644 --- a/src/chromobius/graph/collect_composite_errors.h +++ b/src/chromobius/graph/collect_composite_errors.h @@ -22,7 +22,6 @@ #include "chromobius/datatypes/atomic_error.h" #include "chromobius/datatypes/color_basis.h" #include "chromobius/graph/collect_atomic_errors.h" -#include "stim.h" namespace chromobius { diff --git a/src/chromobius/graph/collect_nodes.cc b/src/chromobius/graph/collect_nodes.cc index 4c9ee57..34942eb 100644 --- a/src/chromobius/graph/collect_nodes.cc +++ b/src/chromobius/graph/collect_nodes.cc @@ -14,6 +14,8 @@ #include "chromobius/graph/collect_nodes.h" +#include "chromobius/datatypes/stim_integration.h" + using namespace chromobius; static void collect_nodes_from_dem_helper_process_detector_instruction( @@ -23,36 +25,13 @@ static void collect_nodes_from_dem_helper_process_detector_instruction( std::vector *coord_buffer, std::span out_node_color, stim::DetectorErrorModel *out_mobius_dem) { - double c = -1; - if (instruction.arg_data.size() > 3) { - c = instruction.arg_data[3]; - if (coord_offsets.size() > 3) { - c += coord_offsets[3]; - } - } - int r = (int)c; - if (r < 0 || r >= 6 || r != c) { - throw std::invalid_argument( - "Expected all detectors to have at least 4 coordinates, with the 4th " - "identifying the basis and color " - "(RedX=0, GreenX=1, BlueX=2, RedZ=3, GreenZ=4, BlueZ=5), but got " + - instruction.str()); - } - constexpr std::array mapping{ - ColorBasis{Charge::R, Basis::X}, - ColorBasis{Charge::G, Basis::X}, - ColorBasis{Charge::B, Basis::X}, - ColorBasis{Charge::R, Basis::Z}, - ColorBasis{Charge::G, Basis::Z}, - ColorBasis{Charge::B, Basis::Z}, - }; - ColorBasis cb = mapping[r]; + ColorBasis cb = detector_instruction_to_color_basis(instruction, coord_offsets); for (const auto &t : instruction.target_data) { auto n = t.raw_id() + det_offset; out_node_color[n] = cb; - if (out_mobius_dem != nullptr) { + if (out_mobius_dem != nullptr && !cb.ignored) { SubGraphCoord g0; SubGraphCoord g1; switch (cb.color) { diff --git a/src/chromobius/graph/drag_graph.cc b/src/chromobius/graph/drag_graph.cc index 77dd61b..4179b55 100644 --- a/src/chromobius/graph/drag_graph.cc +++ b/src/chromobius/graph/drag_graph.cc @@ -15,6 +15,8 @@ #include "chromobius/graph/drag_graph.h" #include +#include +#include using namespace chromobius; diff --git a/src/chromobius/graph/euler_tours.cc b/src/chromobius/graph/euler_tours.cc index b52a5c4..24abc1d 100644 --- a/src/chromobius/graph/euler_tours.cc +++ b/src/chromobius/graph/euler_tours.cc @@ -98,7 +98,14 @@ bool EulerTourGraph::rotate_cycle_to_end_with_unfinished_node() { std::ostream &chromobius::operator<<(std::ostream &out, const EulerTourGraph &val) { out << "EulerTourGraph{\n"; - out << " .cycle_buf={" << stim::comma_sep(val.cycle_buf) << "}\n"; + out << " .cycle_buf={"; + for (size_t k = 0; k < val.cycle_buf.size(); k++) { + if (k > 0) { + out << ", "; + } + out << val.cycle_buf[k]; + } + out << "}\n"; out << " .nodes.size()=" << val.nodes.size() << "\n"; for (size_t k = 0; k < val.nodes.size(); k++) { if (!val.nodes[k].neighbors.empty()) { diff --git a/src/chromobius/graph/euler_tours.h b/src/chromobius/graph/euler_tours.h index cf6eae7..1aaf3af 100644 --- a/src/chromobius/graph/euler_tours.h +++ b/src/chromobius/graph/euler_tours.h @@ -21,7 +21,6 @@ #include "chromobius/datatypes/atomic_error.h" #include "chromobius/datatypes/color_basis.h" -#include "stim.h" namespace chromobius { diff --git a/src/chromobius/graph/euler_tours.perf.cc b/src/chromobius/graph/euler_tours.perf.cc index a80311b..f9bd48e 100644 --- a/src/chromobius/graph/euler_tours.perf.cc +++ b/src/chromobius/graph/euler_tours.perf.cc @@ -14,8 +14,10 @@ #include "chromobius/graph/euler_tours.h" +#include +#include + #include "chromobius/util.perf.h" -#include "stim.h" using namespace chromobius; diff --git a/src/chromobius/pybind/chromobius.pybind.cc b/src/chromobius/pybind/chromobius.pybind.cc index 4de7c3e..90b3c0f 100644 --- a/src/chromobius/pybind/chromobius.pybind.cc +++ b/src/chromobius/pybind/chromobius.pybind.cc @@ -20,8 +20,6 @@ #include #include -#include "stim.h" - #define str_literal(s) #s #define xstr_literal(s) str_literal(s) @@ -30,7 +28,6 @@ struct CompiledDecoder { uint64_t num_detectors; uint64_t num_detector_bytes; uint64_t num_observable_bytes; - std::vector result_buffer; static CompiledDecoder from_dem(const pybind11::object &dem) { auto type_name = pybind11::str(dem.get_type()); @@ -46,11 +43,10 @@ struct CompiledDecoder { .num_detectors = num_dets, .num_detector_bytes = (num_dets + 7) / 8, .num_observable_bytes = (converted_dem.count_observables() + 7) / 8, - .result_buffer = {}, }; } - pybind11::object predict_obs_flips_from_dets_bit_packed(const pybind11::object &dets_obj) { + pybind11::object predict_obs_flips_from_dets_bit_packed(const pybind11::object &dets_obj, bool include_weight) { if (!pybind11::isinstance>(dets_obj)) { throw std::invalid_argument("Expected bit packed detection event data, but dets.dtype wasn't np.uint8."); } @@ -59,10 +55,17 @@ struct CompiledDecoder { size_t num_shots; size_t shot_stride; size_t det_shape; + auto numpy = pybind11::module::import("numpy"); + pybind11::array_t result_buf; + pybind11::array_t weight_buf; if (dets.ndim() == 2) { num_shots = dets.shape(0); shot_stride = dets.strides(0); det_shape = dets.shape(1); + result_buf = numpy.attr("empty")(pybind11::make_tuple(num_shots, num_observable_bytes), numpy.attr("uint8")); + if (include_weight) { + weight_buf = numpy.attr("empty")(pybind11::make_tuple(num_shots), numpy.attr("float32")); + } if (dets.strides(1) != 1) { std::stringstream ss; ss << "Bit packed shot data must be contiguous in memory, but dets.stride[1] wasn't equal to 1.\n"; @@ -73,6 +76,10 @@ struct CompiledDecoder { num_shots = 1; shot_stride = 0; det_shape = dets.shape(0); + result_buf = numpy.attr("empty")(pybind11::make_tuple(num_observable_bytes), numpy.attr("uint8")); + if (include_weight) { + weight_buf = numpy.attr("empty")(pybind11::make_tuple(), numpy.attr("float32")); + } } else { throw std::invalid_argument("dets.shape not in [1, 2]"); } @@ -85,38 +92,27 @@ struct CompiledDecoder { throw std::invalid_argument(ss.str()); } - result_buffer.clear(); + uint8_t *result_ptr = result_buf.mutable_data(); + float *weight_ptr = nullptr; + if (include_weight) { + weight_ptr = weight_buf.mutable_data(); + } for (size_t shot = 0; shot < num_shots; shot++) { const uint8_t *data = dets.data() + shot_stride * shot; chromobius::obsmask_int prediction = - decoder.decode_detection_events({data, data + num_detector_bytes}); - result_buffer.push_back(prediction); - } - - // Write predictions into output numpy array. - uint8_t *buffer = new uint8_t[num_observable_bytes * num_shots]; - size_t offset = 0; - for (chromobius::obsmask_int obs : result_buffer) { + decoder.decode_detection_events({data, data + num_detector_bytes}, weight_ptr); for (size_t k = 0; k < num_observable_bytes; k++) { - buffer[offset++] = obs & 255; - obs >>= 8; + *result_ptr++ = (prediction >> (8*k)) & 0xFF; + } + if (weight_ptr != nullptr) { + weight_ptr++; } } - pybind11::capsule free_when_done(buffer, [](void *f) { - delete[] reinterpret_cast(f); - }); - if (dets.ndim() == 2) { - return pybind11::array_t( - {(pybind11::ssize_t)num_shots, (pybind11::ssize_t)num_observable_bytes}, - {(pybind11::ssize_t)num_observable_bytes, (pybind11::ssize_t)1}, - buffer, - free_when_done); + + if (include_weight) { + return pybind11::make_tuple(result_buf, weight_buf); } else { - return pybind11::array_t( - {(pybind11::ssize_t)num_observable_bytes}, - {(pybind11::ssize_t)1}, - buffer, - free_when_done); + return result_buf; } } }; @@ -156,7 +152,9 @@ PYBIND11_MODULE(chromobius, m) { compiled_decoder.def( "predict_obs_flips_from_dets_bit_packed", - &CompiledDecoder::predict_obs_flips_from_dets_bit_packed, + [](CompiledDecoder &self, const pybind11::object &dets_obj) -> pybind11::object{ + return self.predict_obs_flips_from_dets_bit_packed(dets_obj, false); + }, pybind11::arg("dets"), stim::clean_doc_string(R"DOC( @signature def predict_obs_flips_from_dets_bit_packed(dets: np.ndarray) -> np.ndarray: @@ -235,6 +233,93 @@ PYBIND11_MODULE(chromobius, m) { )DOC") .data()); + compiled_decoder.def( + "predict_weighted_obs_flips_from_dets_bit_packed", + [](CompiledDecoder &self, const pybind11::object &dets_obj) -> pybind11::object{ + return self.predict_obs_flips_from_dets_bit_packed(dets_obj, true); + }, + pybind11::arg("dets"), + stim::clean_doc_string(R"DOC( + @signature def predict_weighted_obs_flips_from_dets_bit_packed(dets: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + Predicts observable flips and weights from detection events. + + The returned weight comes directly from the underlying call to pymatching, not + accounting for the lifting process. + + Args: + dets: A bit packed numpy array of detection event data. The array can either + be 1-dimensional (a single shot to decode) or 2-dimensional (multiple + shots to decode, with the first axis being the shot axis and the second + axis being the detection event byte axis). + + The array's dtype must be np.uint8. If you have an array of dtype + np.bool_, you have data that's not bit packed. You can pack it by + using `np.packbits(array, bitorder='little')`. But ideally you + should attempt to never have unpacked data in the first place, + since it's 8x larger which can be a large performance loss. For + example, stim's sampler methods all have a `bit_packed=True` argument + that cause them to return bit packed data. + + Returns: + A tuple (obs, weights). + Obs is a bit packed numpy array of observable flip data. + Weights is a numpy array (or scalar) of floats. + + If dets is a 1D array, then the result has: + obs.shape = (math.ceil(num_obs / 8),) + obs.dtype = np.uint8 + weights.shape = () + weights.dtype = np.float32 + If dets is a 2D array, then the result has: + shape = (dets.shape[0], math.ceil(num_obs / 8),) + dtype = np.uint8 + weights.shape = (dets.shape[0],) + weights.dtype = np.float32 + + To determine if the observable with index k was flipped in shot s, compute: + `bool((obs[s, k // 8] >> (k % 8)) & 1)` + + Example: + >>> import stim + >>> import chromobius + >>> import numpy as np + + >>> repetition_color_code = stim.Circuit(''' + ... # Apply noise. + ... X_ERROR(0.1) 0 1 2 3 4 5 6 7 + ... # Measure three-body stabilizers to catch errors. + ... MPP Z0*Z1*Z2 Z1*Z2*Z3 Z2*Z3*Z4 Z3*Z4*Z5 Z4*Z5*Z6 Z5*Z6*Z7 + ... + ... # Annotate detectors, with a coloring in the 4th coordinate. + ... DETECTOR(0, 0, 0, 2) rec[-6] + ... DETECTOR(1, 0, 0, 0) rec[-5] + ... DETECTOR(2, 0, 0, 1) rec[-4] + ... DETECTOR(3, 0, 0, 2) rec[-3] + ... DETECTOR(4, 0, 0, 0) rec[-2] + ... DETECTOR(5, 0, 0, 1) rec[-1] + ... + ... # Check on the message. + ... M 0 + ... OBSERVABLE_INCLUDE(0) rec[-1] + ... ''') + + >>> # Sample the circuit. + >>> shots = 4096 + >>> sampler = repetition_color_code.compile_detector_sampler() + >>> dets, actual_obs_flips = sampler.sample( + ... shots=shots, + ... separate_observables=True, + ... bit_packed=True, + ... ) + + >>> # Decode with Chromobius. + >>> dem = repetition_color_code.detector_error_model() + >>> decoder = chromobius.compile_decoder_for_dem(dem) + >>> result = decoder.predict_weighted_obs_flips_from_dets_bit_packed(dets) + >>> pred, weights = result + )DOC") + .data()); + m.def( "compile_decoder_for_dem", &CompiledDecoder::from_dem, @@ -255,6 +340,7 @@ PYBIND11_MODULE(chromobius, m) { 3 = Red Z 4 = Green Z 5 = Blue Z + -1 = Ignore this Detector 2. Rainbow triplets. Bulk errors with three symptoms in one basis should have one symptom of each color. Errors with three symptoms that repeat a color will cause an exception unless they can be decomposed @@ -313,6 +399,7 @@ PYBIND11_MODULE(chromobius, m) { 3 = Red Z 4 = Green Z 5 = Blue Z + -1 = Ignore this Detector 2. Rainbow triplets. Bulk errors with three symptoms in one basis should have one symptom of each color. Errors with three symptoms that repeat a color will cause an exception unless they can be decomposed diff --git a/src/chromobius/pybind/chromobius_pybind_test.py b/src/chromobius/pybind/chromobius_pybind_test.py index 98a70e1..175ce53 100644 --- a/src/chromobius/pybind/chromobius_pybind_test.py +++ b/src/chromobius/pybind/chromobius_pybind_test.py @@ -27,7 +27,7 @@ def test_version(): def test_errors(): with pytest.raises(ValueError, match='must be a stim.DetectorErrorModel'): chromobius.compile_decoder_for_dem(object()) - with pytest.raises(ValueError, match='4th coordinate'): + with pytest.raises(ValueError, match='4 coordinates'): chromobius.compile_decoder_for_dem(stim.DetectorErrorModel(""" error(0.1) D0 """)) diff --git a/src/chromobius/pybind/sinter_compat.pybind.cc b/src/chromobius/pybind/sinter_compat.pybind.cc index 6d17137..fe1972e 100644 --- a/src/chromobius/pybind/sinter_compat.pybind.cc +++ b/src/chromobius/pybind/sinter_compat.pybind.cc @@ -20,8 +20,6 @@ #include #include -#include "stim.h" - struct ChromobiusSinterCompiledDecoder { chromobius::Decoder decoder; uint64_t num_detectors; @@ -31,9 +29,15 @@ struct ChromobiusSinterCompiledDecoder { pybind11::array_t decode_shots_bit_packed( const pybind11::array_t &bit_packed_detection_event_data) { - assert(bit_packed_detection_event_data.ndim() == 2); - assert(bit_packed_detection_event_data.strides(1) == 1); - assert(bit_packed_detection_event_data.shape(1) == num_detector_bytes); + if (bit_packed_detection_event_data.ndim() != 2) { + throw std::invalid_argument("bit_packed_detection_event_data.ndim() != 2"); + } + if (bit_packed_detection_event_data.strides(1) != 1) { + throw std::invalid_argument("bit_packed_detection_event_data.strides(1) != 1"); + } + if (bit_packed_detection_event_data.shape(1) != (pybind11::ssize_t)num_detector_bytes) { + throw std::invalid_argument("bit_packed_detection_event_data.shape(1) != num_detector_bytes"); + } size_t stride = bit_packed_detection_event_data.strides(0); size_t num_shots = bit_packed_detection_event_data.shape(0); diff --git a/src/clorco/color2surface_code/__init__.py b/src/clorco/color2surface_code/__init__.py index aaa2d35..e69de29 100644 --- a/src/clorco/color2surface_code/__init__.py +++ b/src/clorco/color2surface_code/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - diff --git a/src/clorco/color2surface_code/_color2surface_layouts.py b/src/clorco/color2surface_code/_color2surface_layouts.py index 2cef3db..ff3b9d2 100644 --- a/src/clorco/color2surface_code/_color2surface_layouts.py +++ b/src/clorco/color2surface_code/_color2surface_layouts.py @@ -23,10 +23,10 @@ def rgb2xyz(patch: gen.Patch, basis: str) -> gen.Patch: return gen.Patch( [ gen.Tile( - bases="XYZXYZ"[int(tile.extra_coords[0])], + bases='X' if 'color=r' in tile.flags else 'Y' if 'color=g' in tile.flags else 'Z', ordered_data_qubits=tile.ordered_data_qubits, measurement_qubit=tile.measurement_qubit, - extra_coords=tile.extra_coords, + flags=tile.flags, ) for tile in patch.tiles if tile.basis == basis @@ -64,9 +64,7 @@ def make_color2surface_layout( measurement_qubit=tile.measurement_qubit, ordered_data_qubits=tile.ordered_data_qubits, bases=tile.bases, - extra_coords=tile.extra_coords - if tile.measurement_qubit.real != 5 - else [2 + 3 * (tile.basis == "Z")], + flags=tile.flags if tile.measurement_qubit.real != 5 else {'color=b', f'basis={tile.basis}'} ) for tile in surface_patch.tiles ) @@ -94,7 +92,7 @@ def make_color2surface_layout( m + 1j + 1, ], bases="X", - extra_coords=[0], + flags={'color=r', 'basis=X'}, ) for tile in dropped_tiles ], @@ -108,7 +106,7 @@ def make_color2surface_layout( m + 3, ], bases="Z", - extra_coords=[5], + flags={'color=b', 'basis=Z'}, ) for tile in dropped_tiles ], @@ -116,7 +114,7 @@ def make_color2surface_layout( measurement_qubit=-1 - 1j, ordered_data_qubits=[0, -2j], bases="Z", - extra_coords=[5], + flags={'color=b', 'basis=Z'}, ), ] ) diff --git a/src/clorco/color2surface_code/_color2surface_layouts_test.py b/src/clorco/color2surface_code/_color2surface_layouts_test.py index 4c7130d..b5e4715 100644 --- a/src/clorco/color2surface_code/_color2surface_layouts_test.py +++ b/src/clorco/color2surface_code/_color2surface_layouts_test.py @@ -22,9 +22,9 @@ @pytest.mark.parametrize("d", [3, 5, 7, 9]) def test_make_color2surface_layout(d: int): code = make_color2surface_layout(base_data_width=d) - code.check_commutation_relationships() + code.verify() for tile in code.patch.tiles: - assert (tile.extra_coords[0] >= 3) == (tile.basis == "Z") + assert ('basis=Z' in tile.flags) == (tile.basis == "Z") err = code.make_code_capacity_circuit( noise=1e-3 ).search_for_undetectable_logical_errors( diff --git a/src/clorco/color2surface_code/_draw_mobius_graph.py b/src/clorco/color2surface_code/_draw_mobius_graph.py index 8f5f117..ab5f650 100644 --- a/src/clorco/color2surface_code/_draw_mobius_graph.py +++ b/src/clorco/color2surface_code/_draw_mobius_graph.py @@ -88,24 +88,24 @@ def draw_coord_z(c: complex) -> complex: rgb_patch_x = gen.Patch( [ gen.Tile( - bases="XYZ"[int(tile.extra_coords[0]) % 3], + bases='X' if 'color=r' in tile.flags else 'Y' if 'color=g' in tile.flags else 'Z', measurement_qubit=tile.measurement_qubit, ordered_data_qubits=tile.ordered_data_qubits, ) for tile in patch.tiles - if tile.extra_coords[0] // 3 == 0 + if 'basis=X' in tile.flags ] ) rgb_patch_z = gen.Patch( [ gen.Tile( - bases="XYZ"[int(tile.extra_coords[0]) % 3], + bases='X' if 'color=r' in tile.flags else 'Y' if 'color=g' in tile.flags else 'Z', measurement_qubit=tile.measurement_qubit, ordered_data_qubits=tile.ordered_data_qubits, ) for tile in patch.tiles - if tile.extra_coords[0] // 3 == 1 + if 'basis=X' in tile.flags ] ) @@ -124,6 +124,12 @@ def draw_coord_z(c: complex) -> complex: return "\n".join(lines) +def is_colinear(a: complex, b: complex, c: complex) -> bool: + d1 = b - a + d2 = c - a + return abs(d1.real * d2.imag - d2.real * d1.imag) < 1e-4 + + def code_capacity_mobius_graph_helper_piece( *, rgb_patch: gen.Patch, draw_coord: Callable[[complex], complex], lines: list[str] ): @@ -198,7 +204,7 @@ def code_capacity_mobius_graph_helper_piece( center2 = sum(tile2.data_set) / len(tile2.data_set) p2 = draw_coord(q) p2 = p2 + ((a - p2) + (b - p2)) * -0.4 - if gen.is_colinear(q, center1, center2): + if is_colinear(q, center1, center2): lines.append( f"""""" ) diff --git a/src/clorco/color2surface_code/_keyed_constructions.py b/src/clorco/color2surface_code/_keyed_constructions.py index b52fb84..756a91a 100644 --- a/src/clorco/color2surface_code/_keyed_constructions.py +++ b/src/clorco/color2surface_code/_keyed_constructions.py @@ -22,6 +22,25 @@ from clorco.color2surface_code._color2surface_layouts import rgb2xyz +def f2c(flow: gen.Flow) -> list[float]: + c = 0 + if 'color=r' in flow.flags: + c += 0 + elif 'color=g' in flow.flags: + c += 1 + elif 'color=b' in flow.flags: + c += 2 + else: + raise NotImplementedError(f'{flow=}') + if 'basis=X' in flow.flags: + c += 0 + elif 'basis=Z' in flow.flags: + c += 3 + else: + raise NotImplementedError(f'{flow=}') + return [c] + + def make_named_color2surface_code_constructions() -> ( dict[str, Callable[[Params], stim.Circuit]] ): @@ -38,14 +57,14 @@ def _make_simple_circuit( ) if phenom: return code.make_phenom_circuit( - noise=params.noise_model, + noise=params.noise_model.idle_depolarization, rounds=params.rounds, - debug_out_dir=params.debug_out_dir, + extra_coords_func=f2c, ) assert params.rounds == 1 return code.make_code_capacity_circuit( - noise=params.noise_model.idle_noise, - debug_out_dir=params.debug_out_dir + noise=params.noise_model.idle_depolarization, + extra_coords_func=f2c, ) constructions["transit_color2surface_code"] = lambda params: _make_simple_circuit( diff --git a/src/clorco/color_code/_color_code_layouts.py b/src/clorco/color_code/_color_code_layouts.py index 65c403a..89ae697 100644 --- a/src/clorco/color_code/_color_code_layouts.py +++ b/src/clorco/color_code/_color_code_layouts.py @@ -84,7 +84,7 @@ def make_color_code_layout( bases="XYZ"[int(q.imag % 3)], measurement_qubit=m, ordered_data_qubits=[q + d for d in order], - extra_coords=[q.imag % 3], + flags={'color=' + 'rgb'[int(q.imag % 3)]}, ) ) if x % 2 == 0: @@ -94,7 +94,7 @@ def make_color_code_layout( bases="Z", measurement_qubit=q + 1, ordered_data_qubits=[q + d for d in [1j, 0, 1, 1 + 1j]], - extra_coords=[2], + flags={'color=b'}, ) ) q = x + 1j * start_y @@ -105,7 +105,7 @@ def make_color_code_layout( bases="Y", measurement_qubit=q + 1j - 1, ordered_data_qubits=[q + d for d in [0, 1j - 1, 1j, -1]], - extra_coords=[1], + flags={'color=g'}, ) ) tiles.append( @@ -113,7 +113,7 @@ def make_color_code_layout( bases="X", measurement_qubit=q - 1, ordered_data_qubits=[q + d for d in [-1, 1j - 1]], - extra_coords=[0], + flags={'color=r'}, ) ) else: @@ -123,7 +123,7 @@ def make_color_code_layout( bases="Z", measurement_qubit=q + 1j, ordered_data_qubits=[q + d for d in [1j, 0]], - extra_coords=[2], + flags={'color=b'}, ) ) else: @@ -134,7 +134,7 @@ def make_color_code_layout( bases="X", measurement_qubit=q + 1j, ordered_data_qubits=[q + d for d in [0, 1j]], - extra_coords=[0], + flags={'color=r'}, ) ) if x % 2 == 1 or spurs == "smooth": @@ -145,7 +145,7 @@ def make_color_code_layout( bases="X", measurement_qubit=q + 2j, ordered_data_qubits=[q + d for d in [2j, 1 + 2j, 1 + 1j, 1]], - extra_coords=[0], + flags={'color=r'}, ) ) else: @@ -154,22 +154,21 @@ def make_color_code_layout( bases="Y", measurement_qubit=q + 2j, ordered_data_qubits=[q + d for d in [0, 1j, 2j, 1 + 2j]], - extra_coords=[1], + flags={'color=g'}, ) ) patch = gen.Patch(tiles).with_transformed_coords(lambda e: h * 1j + w - e + 1 + 2j) if coord_style == "hex": patch = patch.with_transformed_coords(_rect_to_hex_transform) for tile in patch.tiles: - assert "XYZ".index(tile.basis) == tile.extra_coords[0] + assert "XYZ".index(tile.basis) == ["color=r", "color=g", "color=b"].index(next(iter(tile.flags))) if not single_rgb_layer_instead_of_actual_code: patch = gen.Patch( gen.Tile( bases=basis, - measurement_qubit=tile.measurement_qubit - + (0.125 if basis == "Z" else 0), + measurement_qubit=tile.measurement_qubit + (0.125 if basis == "Z" else 0), ordered_data_qubits=tile.ordered_data_qubits, - extra_coords=[tile.extra_coords[0] + (3 if basis == "Z" else 0)], + flags={*tile.flags, f'basis={basis}'}, ) for tile in patch.tiles for basis in "XZ" @@ -209,7 +208,7 @@ def wrap(q: complex) -> complex: bases="X", measurement_qubit=wrap(q + 0), ordered_data_qubits=[wrap(q + d) for d in order], - extra_coords=[rgb], + flags={'color=' + 'rgb'[int(rgb)], 'basis=X'}, ) ) if rgb != 2 or not ablate_into_matchable_code: @@ -218,7 +217,7 @@ def wrap(q: complex) -> complex: bases="Z", measurement_qubit=wrap(q + 1j), ordered_data_qubits=[wrap(q + d) for d in order], - extra_coords=[rgb + 3], + flags={'color=' + 'rgb'[int(rgb)], 'basis=Z'}, ) ) @@ -326,7 +325,7 @@ def make_color_code_layout_488( bases="Z", measurement_qubit=q + 0.5, ordered_data_qubits=[q + d for d in [1j, 0, 1, 1 + 1j]], - extra_coords=[2], + flags={'color=b'}, ) ) for x in range(0, half, 2): @@ -337,7 +336,7 @@ def make_color_code_layout_488( bases="X", measurement_qubit=q + 2j - 0.5, ordered_data_qubits=[q + d for d in [1j, 2j, -1 + 2j, -1 + 1j]], - extra_coords=[0], + flags={'color=r'}, ) ) tiles.append( @@ -345,7 +344,7 @@ def make_color_code_layout_488( bases="Y", measurement_qubit=q + 1j - 1.5, ordered_data_qubits=[q + d for d in [-1 + 2j, -1 + 1j]], - extra_coords=[1], + flags={'color=g'}, ) ) else: @@ -355,7 +354,7 @@ def make_color_code_layout_488( bases="X", measurement_qubit=q + 2j - 0.5, ordered_data_qubits=[q + d for d in [1j, 2j]], - extra_coords=[0], + flags={'color=r'}, ) ) tiles.append( @@ -370,7 +369,7 @@ def make_color_code_layout_488( else [2j, 1j, 0, 1j, 1 + 1j, 1, 1 + 1j, 1 + 2j] ) ], - extra_coords=[1], + flags={'color=g'}, ) ) for x in range(0, half - 1, 2): @@ -381,7 +380,7 @@ def make_color_code_layout_488( bases="X", measurement_qubit=q + 0.5 + 1j, ordered_data_qubits=[q + d for d in [2j, 1j, 1 + 1j, 1 + 2j]], - extra_coords=[0], + flags={'color=r'}, ) ) tiles.append( @@ -389,7 +388,7 @@ def make_color_code_layout_488( bases="Z", measurement_qubit=q + 1.5 + 2j, ordered_data_qubits=[q + d for d in [1 + 1j, 1 + 2j]], - extra_coords=[2], + flags={'color=b'}, ) ) elif spurs != "smooth": @@ -398,7 +397,7 @@ def make_color_code_layout_488( bases="X", measurement_qubit=q + 1j + 0.5, ordered_data_qubits=[q + d for d in [1j, 2j]], - extra_coords=[0], + flags={'color=r'}, ) ) q -= 1 @@ -414,7 +413,7 @@ def make_color_code_layout_488( else [0, 1j, 2j, 1 + 2j, 1 + 1j, 1] ) ], - extra_coords=[2], + flags={'color=b'}, ) ) @@ -431,7 +430,7 @@ def make_color_code_layout_488( [0, 1j, 1 + 1j, 1] if x % 2 == 0 else [1j, 0, 1, 1 + 1j] ) ], - extra_coords=[0], + flags={'color=r'}, ) ) for offset in range(0, w, 2): @@ -445,7 +444,7 @@ def make_color_code_layout_488( q + d for d in [3j, 2j, 1j, 0, 1j, 1 + 1j, 1, 1 + 1j, 1 + 2j, 1 + 3j] ], - extra_coords=[1], + flags={'color=g'}, ) ) for offset in range(0, w, 2): @@ -472,7 +471,7 @@ def make_color_code_layout_488( 1 + 3j, ] ], - extra_coords=[2], + flags={'color=b'}, ) ) @@ -489,7 +488,7 @@ def make_color_code_layout_488( else -1 ), ordered_data_qubits=tile.ordered_data_qubits, - extra_coords=tile.extra_coords, + flags=tile.flags, ) for tile in tiles ] @@ -497,7 +496,7 @@ def make_color_code_layout_488( if coord_style == "oct": patch = patch.with_transformed_coords(_rect_to_oct_transform) for tile in patch.tiles: - assert "XYZ".index(tile.basis) == tile.extra_coords[0] + assert "XYZ".index(tile.basis) == ["color=r", "color=g", "color=b"].index(next(iter(tile.flags))) if not single_rgb_layer_instead_of_actual_code: patch = gen.Patch( gen.Tile( @@ -505,7 +504,7 @@ def make_color_code_layout_488( measurement_qubit=tile.measurement_qubit + (0.125 if basis == "Z" else 0), ordered_data_qubits=tile.ordered_data_qubits, - extra_coords=[tile.extra_coords[0] + (3 if basis == "Z" else 0)], + flags={*tile.flags, f'basis={basis}'}, ) for tile in patch.tiles for basis in "XZ" diff --git a/src/clorco/color_code/_color_code_layouts_test.py b/src/clorco/color_code/_color_code_layouts_test.py index 5fbc0cb..6a0315c 100644 --- a/src/clorco/color_code/_color_code_layouts_test.py +++ b/src/clorco/color_code/_color_code_layouts_test.py @@ -56,13 +56,13 @@ def test_make_color_code_patch_3_6(): ordered_data_qubits=((1 + 3j), (1 + 2j), (1 + 1j), 1j), measurement_qubit=(1 + 1j), bases="Y", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((2 + 1j), (2 + 2j), (1 + 2j), (1 + 1j)), measurement_qubit=(1 + 2j), bases="Z", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -75,13 +75,13 @@ def test_make_color_code_patch_3_6(): ), measurement_qubit=(1 + 4j), bases="X", - extra_coords=(0.0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((2 + 3j), (2 + 4j)), measurement_qubit=(2 + 3j), bases="Z", - extra_coords=(2,), + flags={'color=b', 'basis=Z'}, ), ] ) @@ -138,19 +138,19 @@ def test_make_color_code_patch_4_8_8(): ), measurement_qubit=1j, bases="Y", - extra_coords=[1], + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=(1j, 2j), measurement_qubit=2j, bases="X", - extra_coords=[0], + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((1 + 1j), 1, 2, (2 + 1j)), measurement_qubit=2.0, bases="Z", - extra_coords=[2], + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -167,37 +167,37 @@ def test_make_color_code_patch_4_8_8(): ), measurement_qubit=(2 + 1j), bases="Y", - extra_coords=[1], + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((1 + 1j), (1 + 2j), (2 + 2j), (2 + 1j)), measurement_qubit=(2 + 2j), bases="X", - extra_coords=[0], - ), + flags={'color=r', 'basis=X'}, + ), gen.Tile( ordered_data_qubits=((2 + 4j), (2 + 3j), (3 + 3j), (3 + 4j)), measurement_qubit=(2 + 3j), bases="X", - extra_coords=[0], + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((3 + 1j), 3, 4, (4 + 1j)), measurement_qubit=4.0, bases="Z", - extra_coords=[2], + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((3 + 1j), (3 + 2j), (4 + 2j), (4 + 1j)), measurement_qubit=(4 + 2j), bases="X", - extra_coords=[0], + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((4 + 3j), (4 + 4j)), measurement_qubit=(4 + 3j), bases="X", - extra_coords=[0], + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -210,7 +210,7 @@ def test_make_color_code_patch_4_8_8(): ), measurement_qubit=(4 + 4j), bases="Z", - extra_coords=[2], + flags={'color=b', 'basis=Z'}, ), ] ) @@ -233,7 +233,7 @@ def test_make_color_code_patch_4_8_8_stripped_measurements( single_rgb_layer_instead_of_actual_code=True, coord_style="rect", ) - code.check_commutation_relationships() + code.verify() patch = code.patch all_columns = {c.real for c in patch.data_set} @@ -263,7 +263,7 @@ def test_make_toric_color_code_layout( height=height, ablate_into_matchable_code=ablate_into_matchable_code, ) - code.check_commutation_relationships() + code.verify() circuit = code.make_code_capacity_circuit( noise=gen.NoiseRule(after={"DEPOLARIZE1": 1e-3}) ) @@ -302,7 +302,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=1j, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -315,7 +315,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=5j, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -328,7 +328,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(1 + 2j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -341,7 +341,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(1 + 3j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -354,7 +354,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(1 + 6j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -367,7 +367,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(1 + 7j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -380,7 +380,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(2 + 0j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -393,7 +393,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(2 + 4j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -406,7 +406,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(3 + 3j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -419,7 +419,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(3 + 7j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -432,7 +432,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(4 + 0j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -445,7 +445,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(4 + 1j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -458,7 +458,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(4 + 4j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -471,7 +471,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(4 + 5j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -484,7 +484,7 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(5 + 2j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -497,22 +497,22 @@ def test_make_toric_color_code_exact(): ), measurement_qubit=(5 + 6j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), ] ), observables_x=[ - gen.PauliString(qubits={2j: "X", 3j: "X", 6j: "X", 7j: "X"}), + gen.PauliString({2j: "X", 3j: "X", 6j: "X", 7j: "X"}), gen.PauliString( - qubits={(1 + 4j): "X", (2 + 3j): "X", (4 + 3j): "X", (5 + 4j): "X"} + {(1 + 4j): "X", (2 + 3j): "X", (4 + 3j): "X", (5 + 4j): "X"} ), ], observables_z=[ gen.PauliString( - qubits={2j: "Z", (1 + 1j): "Z", (3 + 1j): "Z", (4 + 2j): "Z"} + {2j: "Z", (1 + 1j): "Z", (3 + 1j): "Z", (4 + 2j): "Z"} ), gen.PauliString( - qubits={(2 + 2j): "Z", (2 + 3j): "Z", (2 + 6j): "Z", (2 + 7j): "Z"} + {(2 + 2j): "Z", (2 + 3j): "Z", (2 + 6j): "Z", (2 + 7j): "Z"} ), ], ) @@ -524,8 +524,7 @@ def test_make_toric_color_code_phenom_circuit_exact(): height=8, ablate_into_matchable_code=True, ) - assert code.make_code_capacity_circuit(noise=0.125) == stim.Circuit( - """ + assert code.make_code_capacity_circuit(noise=0.125) == stim.Circuit(""" QUBIT_COORDS(0, -1) 0 QUBIT_COORDS(0, 2) 1 QUBIT_COORDS(0, 3) 2 @@ -552,45 +551,37 @@ def test_make_toric_color_code_phenom_circuit_exact(): QUBIT_COORDS(5, 1) 23 QUBIT_COORDS(5, 4) 24 QUBIT_COORDS(5, 5) 25 - MPP X0*X1*X2*X3*X4 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP X5*X8*X11*X19*X24 - OBSERVABLE_INCLUDE(1) rec[-1] - MPP Z0*Z1*Z7*Z15*Z18 - OBSERVABLE_INCLUDE(2) rec[-1] - MPP Z5*Z10*Z11*Z12*Z13 - OBSERVABLE_INCLUDE(3) rec[-1] - MPP Z1*Z4*Z6*Z7*Z22*Z23 Z2*Z3*Z8*Z9*Z24*Z25 X1*X2*X7*X8*X10*X11 Z1*Z2*Z7*Z8*Z10*Z11 X3*X4*X6*X9*X12*X13 Z3*Z4*Z6*Z9*Z12*Z13 X6*X7*X10*X13*X14*X15 X8*X9*X11*X12*X16*X17 Z10*Z11*Z15*Z16*Z18*Z19 Z12*Z13*Z14*Z17*Z20*Z21 X14*X15*X18*X21*X22*X23 Z14*Z15*Z18*Z21*Z22*Z23 X16*X17*X19*X20*X24*X25 Z16*Z17*Z19*Z20*Z24*Z25 X1*X2*X18*X19*X23*X24 X3*X4*X20*X21*X22*X25 - TICK + MPP X0*X1*X2*X3*X4 X5*X8*X11*X19*X24 Z0*Z1*Z7*Z15*Z18 Z5*Z10*Z11*Z12*Z13 Z1*Z4*Z6*Z7*Z22*Z23 Z2*Z3*Z8*Z9*Z24*Z25 X1*X2*X7*X8*X10*X11 Z1*Z2*Z7*Z8*Z10*Z11 X3*X4*X6*X9*X12*X13 Z3*Z4*Z6*Z9*Z12*Z13 X6*X7*X10*X13*X14*X15 X8*X9*X11*X12*X16*X17 Z10*Z11*Z15*Z16*Z18*Z19 Z12*Z13*Z14*Z17*Z20*Z21 X14*X15*X18*X21*X22*X23 Z14*Z15*Z18*Z21*Z22*Z23 X16*X17*X19*X20*X24*X25 Z16*Z17*Z19*Z20*Z24*Z25 X1*X2*X18*X19*X23*X24 X3*X4*X20*X21*X22*X25 DEPOLARIZE1(0.125) 1 2 3 4 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 + OBSERVABLE_INCLUDE(0) rec[-20] + OBSERVABLE_INCLUDE(1) rec[-19] + OBSERVABLE_INCLUDE(2) rec[-18] + OBSERVABLE_INCLUDE(3) rec[-17] TICK - MPP Z1*Z4*Z6*Z7*Z22*Z23 Z2*Z3*Z8*Z9*Z24*Z25 X1*X2*X7*X8*X10*X11 Z1*Z2*Z7*Z8*Z10*Z11 X3*X4*X6*X9*X12*X13 Z3*Z4*Z6*Z9*Z12*Z13 X6*X7*X10*X13*X14*X15 X8*X9*X11*X12*X16*X17 Z10*Z11*Z15*Z16*Z18*Z19 Z12*Z13*Z14*Z17*Z20*Z21 X14*X15*X18*X21*X22*X23 Z14*Z15*Z18*Z21*Z22*Z23 X16*X17*X19*X20*X24*X25 Z16*Z17*Z19*Z20*Z24*Z25 X1*X2*X18*X19*X23*X24 X3*X4*X20*X21*X22*X25 - DETECTOR(0, 1, 0, 3) rec[-32] rec[-16] - DETECTOR(0, 5, 0, 3) rec[-31] rec[-15] - DETECTOR(1, 2, 0, 1) rec[-30] rec[-14] - DETECTOR(1, 3, 0, 4) rec[-29] rec[-13] - DETECTOR(1, 6, 0, 1) rec[-28] rec[-12] - DETECTOR(1, 7, 0, 4) rec[-27] rec[-11] - DETECTOR(2, 0, 0, 2) rec[-26] rec[-10] - DETECTOR(2, 4, 0, 2) rec[-25] rec[-9] - DETECTOR(3, 3, 0, 3) rec[-24] rec[-8] - DETECTOR(3, 7, 0, 3) rec[-23] rec[-7] - DETECTOR(4, 0, 0, 1) rec[-22] rec[-6] - DETECTOR(4, 1, 0, 4) rec[-21] rec[-5] - DETECTOR(4, 4, 0, 1) rec[-20] rec[-4] - DETECTOR(4, 5, 0, 4) rec[-19] rec[-3] - DETECTOR(5, 2, 0, 2) rec[-18] rec[-2] - DETECTOR(5, 6, 0, 2) rec[-17] rec[-1] - MPP X0*X1*X2*X3*X4 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP X5*X8*X11*X19*X24 - OBSERVABLE_INCLUDE(1) rec[-1] - MPP Z0*Z1*Z7*Z15*Z18 - OBSERVABLE_INCLUDE(2) rec[-1] - MPP Z5*Z10*Z11*Z12*Z13 - OBSERVABLE_INCLUDE(3) rec[-1] - """ - ) + MPP X0*X1*X2*X3*X4 X5*X8*X11*X19*X24 Z0*Z1*Z7*Z15*Z18 Z5*Z10*Z11*Z12*Z13 Z1*Z4*Z6*Z7*Z22*Z23 Z2*Z3*Z8*Z9*Z24*Z25 X1*X2*X7*X8*X10*X11 Z1*Z2*Z7*Z8*Z10*Z11 X3*X4*X6*X9*X12*X13 Z3*Z4*Z6*Z9*Z12*Z13 X6*X7*X10*X13*X14*X15 X8*X9*X11*X12*X16*X17 Z10*Z11*Z15*Z16*Z18*Z19 Z12*Z13*Z14*Z17*Z20*Z21 X14*X15*X18*X21*X22*X23 Z14*Z15*Z18*Z21*Z22*Z23 X16*X17*X19*X20*X24*X25 Z16*Z17*Z19*Z20*Z24*Z25 X1*X2*X18*X19*X23*X24 X3*X4*X20*X21*X22*X25 + OBSERVABLE_INCLUDE(0) rec[-20] + OBSERVABLE_INCLUDE(1) rec[-19] + OBSERVABLE_INCLUDE(2) rec[-18] + OBSERVABLE_INCLUDE(3) rec[-17] + DETECTOR(0, 1, 0) rec[-36] rec[-16] + DETECTOR(0, 5, 0) rec[-35] rec[-15] + DETECTOR(1, 2, 0) rec[-34] rec[-14] + DETECTOR(1, 3, 0) rec[-33] rec[-13] + DETECTOR(1, 6, 0) rec[-32] rec[-12] + DETECTOR(1, 7, 0) rec[-31] rec[-11] + DETECTOR(2, 0, 0) rec[-30] rec[-10] + DETECTOR(2, 4, 0) rec[-29] rec[-9] + DETECTOR(3, 3, 0) rec[-28] rec[-8] + DETECTOR(3, 7, 0) rec[-27] rec[-7] + DETECTOR(4, 0, 0) rec[-26] rec[-6] + DETECTOR(4, 1, 0) rec[-25] rec[-5] + DETECTOR(4, 4, 0) rec[-24] rec[-4] + DETECTOR(4, 5, 0) rec[-23] rec[-3] + DETECTOR(5, 2, 0) rec[-22] rec[-2] + DETECTOR(5, 6, 0) rec[-21] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + """) def test_make_toric_color_code_exact_square_coords(): @@ -613,7 +604,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=1j, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -626,7 +617,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=3j, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -639,7 +630,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(1 + 0j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -652,7 +643,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(1 + 1j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -665,7 +656,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(1 + 2j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -678,7 +669,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(1 + 3j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -691,7 +682,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(2 + 0j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -704,7 +695,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(2 + 2j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -717,7 +708,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(3 + 0j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -730,7 +721,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(3 + 2j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -743,7 +734,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(4 + 0j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -756,7 +747,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(4 + 1j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -769,7 +760,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(4 + 2j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -782,7 +773,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(4 + 3j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=( @@ -795,7 +786,7 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(5 + 1j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), gen.Tile( ordered_data_qubits=( @@ -808,22 +799,22 @@ def test_make_toric_color_code_exact_square_coords(): ), measurement_qubit=(5 + 3j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=X'}, ), ] ), observables_x=[ - gen.PauliString(qubits={0j: "X", 1j: "X", 2j: "X", 3j: "X"}), + gen.PauliString({0j: "X", 1j: "X", 2j: "X", 3j: "X"}), gen.PauliString( - qubits={(1 + 2j): "X", (2 + 2j): "X", (4 + 2j): "X", (5 + 2j): "X"} + {(1 + 2j): "X", (2 + 2j): "X", (4 + 2j): "X", (5 + 2j): "X"} ), ], observables_z=[ gen.PauliString( - qubits={1j: "Z", (1 + 1j): "Z", (3 + 1j): "Z", (4 + 1j): "Z"} + {1j: "Z", (1 + 1j): "Z", (3 + 1j): "Z", (4 + 1j): "Z"} ), gen.PauliString( - qubits={(2 + 0j): "Z", (2 + 1j): "Z", (2 + 2j): "Z", (2 + 3j): "Z"} + {(2 + 0j): "Z", (2 + 1j): "Z", (2 + 2j): "Z", (2 + 3j): "Z"} ), ], ) diff --git a/src/clorco/color_code/_keyed_constructions.py b/src/clorco/color_code/_keyed_constructions.py index 56231cc..b212bd5 100644 --- a/src/clorco/color_code/_keyed_constructions.py +++ b/src/clorco/color_code/_keyed_constructions.py @@ -53,13 +53,32 @@ def make_named_color_code_constructions() -> ( } +def f2c(flow: gen.Flow) -> list[float]: + c = 0 + if 'color=r' in flow.flags: + c += 0 + elif 'color=g' in flow.flags: + c += 1 + elif 'color=b' in flow.flags: + c += 2 + else: + raise NotImplementedError(f'{flow=}') + if 'basis=X' in flow.flags: + c += 0 + elif 'basis=Z' in flow.flags: + c += 3 + else: + raise NotImplementedError(f'{flow=}') + return [c] + + def _midout_color_code_circuit_constructions() -> ( dict[str, Callable[[Params], stim.Circuit]] ): constructions: dict[str, Callable[[Params], stim.Circuit]] = {} def _chunks_to_circuit(params: Params, chunks: list[gen.Chunk]) -> stim.Circuit: - circuit = gen.compile_chunks_into_circuit(chunks) + circuit = gen.compile_chunks_into_circuit(chunks, flow_to_extra_coords_func=f2c) if params.debug_out_dir is not None: make_layout = make_color_code_layout_488 if '488' in params.style else make_color_code_layout @@ -273,14 +292,14 @@ def _make_simple_circuit( ) -> stim.Circuit: if phenom: return code.make_phenom_circuit( - noise=params.noise_model, + noise=params.noise_model.idle_depolarization, rounds=params.rounds, - debug_out_dir=params.debug_out_dir, + extra_coords_func=f2c, ) assert params.rounds == 1 return code.make_code_capacity_circuit( - noise=params.noise_model.idle_noise, - debug_out_dir=params.debug_out_dir + noise=params.noise_model.idle_depolarization, + extra_coords_func=f2c, ) constructions["transit_color_code"] = lambda params: _make_simple_circuit( @@ -398,11 +417,11 @@ def _make_simple_circuit( ) if phenom: circuit = code.make_phenom_circuit( - noise=params.noise_strength, rounds=params.rounds + noise=params.noise_strength, rounds=params.rounds, extra_coords_func=f2c, ) else: assert params.rounds == 1 - circuit = code.make_code_capacity_circuit(noise=params.noise_strength) + circuit = code.make_code_capacity_circuit(noise=params.noise_strength, extra_coords_func=f2c) if params.debug_out_dir is not None: gen.write_file( params.debug_out_dir / "detslice.svg", circuit.diagram("detslice-svg") diff --git a/src/clorco/color_code/_midout_planar_color_code_circuits.py b/src/clorco/color_code/_midout_planar_color_code_circuits.py index 8d57f41..9770ae2 100644 --- a/src/clorco/color_code/_midout_planar_color_code_circuits.py +++ b/src/clorco/color_code/_midout_planar_color_code_circuits.py @@ -81,68 +81,66 @@ def _color_code_round_chunk( # Fold color code state towards next measurement. if not first_round: - builder.gate2("CX", rail_b) + builder.append("CX", rail_b) builder.tick() - builder.gate2("CX", rail_a) + builder.append("CX", rail_a) builder.tick() - builder.gate2("CX", rung_m) + builder.append("CX", rung_m) builder.tick() # Measure stabilizers. - builder.measure(mx, basis="X", save_layer="solo") - builder.measure(mz, basis="Z", save_layer="solo") + builder.append('MX', mx) + builder.append('MZ', mz) builder.shift_coords(dt=1) builder.tick() # Physically do a demolition measurement, but use feedback to present it as non-demolition to the control system. - builder.gate("RX", mx) + builder.append("RX", mx) if first_round: - builder.gate(f"R{basis}", patch.data_set - mx - mz) - builder.gate("R", mz) + builder.append(f"R{basis}", patch.data_set - mx - mz) + builder.append("R", mz) if not first_round: for ms, mb in ((mx, "Z"), (mz, "X")): for m in gen.sorted_complex(ms): builder.classical_paulis( - control_keys=[gen.AtLayer(m, "solo")], targets=[m], basis=mb + control_keys=[m], targets=[m], basis=mb ) builder.tick() # Unfold from previous measurement to color code state. - builder.gate2("CX", rung_m) + builder.append("CX", rung_m) builder.tick() - builder.gate2("CX", rail_a) + builder.append("CX", rail_a) builder.tick() - builder.gate2("CX", rail_b) + builder.append("CX", rail_b) flows = [] discarded_outputs = [] for tile in patch.tiles: measured_tile_basis = "X" if tile.measurement_qubit in mx else "Z" for check_basis in "XZ": + flags = {*tile.flags, f'basis={check_basis}'} ps = gen.PauliString({q: check_basis for q in tile.data_set}) - additional_coords = [tile.extra_coords[0] + "XZ".index(check_basis) * 3] if first_round: if check_basis in [basis, measured_tile_basis]: flows.append( gen.Flow( end=ps, center=tile.measurement_qubit, - additional_coords=additional_coords, + flags=flags, ) ) else: discarded_outputs.append(ps) else: if check_basis == measured_tile_basis: - ms = builder.tracker.measurement_indices( - [gen.AtLayer(tile.measurement_qubit, "solo")] - ) + ms = builder.lookup_rec(tile.measurement_qubit) flows.append( gen.Flow( start=ps, measurement_indices=ms, center=tile.measurement_qubit, - additional_coords=additional_coords, + flags=flags, ) ) flows.append( @@ -150,7 +148,7 @@ def _color_code_round_chunk( end=ps, measurement_indices=ms, center=tile.measurement_qubit, - additional_coords=additional_coords, + flags=flags, ) ) else: @@ -159,7 +157,7 @@ def _color_code_round_chunk( start=ps, end=ps, center=tile.measurement_qubit, - additional_coords=additional_coords, + flags=flags, ) ) @@ -170,7 +168,6 @@ def _color_code_round_chunk( end=obs, center=0, obs_index=0, - additional_coords=(), ) ) @@ -213,7 +210,7 @@ def make_midout_color_code_circuit_chunks( basis=basis, layer_parity=rounds % 2 == 1, first_round=True, - ).inverted() + ).time_reversed() if rounds % 2 == 0: tail = [body_1, end] diff --git a/src/clorco/color_code/_midout_planar_color_code_circuits_test.py b/src/clorco/color_code/_midout_planar_color_code_circuits_test.py index b52e20e..b33c695 100644 --- a/src/clorco/color_code/_midout_planar_color_code_circuits_test.py +++ b/src/clorco/color_code/_midout_planar_color_code_circuits_test.py @@ -23,6 +23,7 @@ _color_code_round_chunk, make_midout_color_code_circuit_chunks, ) +from clorco.color_code._toric_color_code_circuits import f2c @pytest.mark.parametrize( @@ -66,7 +67,7 @@ def test_make_color_code_circuit_chunks( use_488=use_488, ) - circuit = gen.compile_chunks_into_circuit(chunks) + circuit = gen.compile_chunks_into_circuit(chunks, flow_to_extra_coords_func=f2c) circuit.detector_error_model() assert ( circuit.count_determined_measurements() @@ -302,12 +303,12 @@ def test_exact_color_code_circuit(): MX(0.001) 8 6 5 3 0 7 1 DETECTOR(1, 1, 0, 1) rec[-1] DETECTOR(1, 2, 0, 2) rec[-7] rec[-5] rec[-4] rec[-3] rec[-2] rec[-1] - DETECTOR(1, 2, 0, 5) rec[-11] rec[-10] rec[-8] + DETECTOR(1, 2, 1, 5) rec[-11] rec[-10] rec[-8] DETECTOR(1, 4, 0, 0) rec[-7] rec[-6] - DETECTOR(1, 4, 0, 3) rec[-9] + DETECTOR(1, 4, 1, 3) rec[-9] DETECTOR(2, 3, 0, 2) rec[-2] OBSERVABLE_INCLUDE(0) rec[-7] rec[-5] rec[-1] - SHIFT_COORDS(0, 0, 1) + SHIFT_COORDS(0, 0, 2) DEPOLARIZE1(0.001) 4 2 8 6 5 3 0 7 1 """ ) diff --git a/src/clorco/color_code/_mxyz_color_codes.py b/src/clorco/color_code/_mxyz_color_codes.py index fdb2770..16986fa 100644 --- a/src/clorco/color_code/_mxyz_color_codes.py +++ b/src/clorco/color_code/_mxyz_color_codes.py @@ -58,22 +58,18 @@ def make_mxyz_phenom_color_code( ancilla = -1 - 1j builder = gen.Builder.for_qubits(code.patch.data_set | {ancilla}) - builder.measure_observables_and_include( - code.entangled_observables([ancilla])[0], - ) + for k, obs in enumerate(code.entangled_observables([ancilla])[0]): + builder.measure_pauli_string(obs, key=f'obs{k}') + builder.obs_include([f'obs{k}'], obs_index=k) builder.tick() x_tiles = gen.Patch(tile for tile in code.patch.tiles if tile.basis == "X") y_tiles = x_tiles.after_basis_transform(lambda _: cast(Literal["X", "Y", "Z"], "Y")) z_tiles = x_tiles.after_basis_transform(lambda _: cast(Literal["X", "Y", "Z"], "Z")) - builder.measure_patch( - y_tiles, - save_layer=-2, - ) + for tile in y_tiles.tiles: + builder.measure_pauli_string(tile.to_data_pauli_string(), key=(tile.measurement_qubit, -2)) builder.tick() - builder.measure_patch( - z_tiles, - save_layer=-1, - ) + for tile in z_tiles.tiles: + builder.measure_pauli_string(tile.to_data_pauli_string(), key=(tile.measurement_qubit, -1)) builder.tick() round_index = 0 @@ -86,16 +82,20 @@ def append_round(out: gen.Builder, round_noise: bool) -> None: out.circuit.append( k, [builder.q2i[q] for q in gen.sorted_complex(tiles.data_set)], p ) - out.measure_patch( - tiles, - save_layer=round_index, - noise=noise.flip_result if round_noise else None, - ) for tile in tiles.tiles: - rgb = int(tile.extra_coords[0]) % 3 + builder.measure_pauli_string(tile.to_data_pauli_string(), key=(tile.measurement_qubit, round_index), noise=noise.flip_result if round_noise else None) + for tile in tiles.tiles: + if 'color=r' in tile.flags: + rgb = 0 + elif 'color=g' in tile.flags: + rgb = 1 + elif 'color=b' in tile.flags: + rgb = 2 + else: + raise NotImplementedError(f'{tile=}') m = tile.measurement_qubit out.detector( - [gen.AtLayer(m, round_index + offset) for offset in [-2, -1, 0]], + [(m, round_index + offset) for offset in [-2, -1, 0]], pos=m, extra_coords=[(rgb + round_index) % 3], ) @@ -103,19 +103,20 @@ def append_round(out: gen.Builder, round_noise: bool) -> None: out.tick() round_index += 1 - loop = builder.fork() - append_round(loop, True) - append_round(loop, True) - append_round(loop, True) - builder.circuit += loop.circuit * (rounds // 3) + prefix = builder.circuit + builder.circuit = stim.Circuit() + append_round(builder, True) + append_round(builder, True) + append_round(builder, True) + builder.circuit = prefix + builder.circuit * (rounds // 3) for _ in range(rounds % 3): append_round(builder, True) append_round(builder, False) append_round(builder, False) - builder.measure_observables_and_include( - code.entangled_observables([ancilla])[0], - ) + for k, obs in enumerate(code.entangled_observables([ancilla])[0]): + builder.measure_pauli_string(obs, key=f'obs_end{k}') + builder.obs_include([f'obs_end{k}'], obs_index=k) return builder.circuit diff --git a/src/clorco/color_code/_superdense_planar_color_code_circuits.py b/src/clorco/color_code/_superdense_planar_color_code_circuits.py index 4e116d4..e64791f 100644 --- a/src/clorco/color_code/_superdense_planar_color_code_circuits.py +++ b/src/clorco/color_code/_superdense_planar_color_code_circuits.py @@ -44,7 +44,7 @@ def make_color_code_layout_for_superdense( bases=bases[k], measurement_qubit=q + k, ordered_data_qubits=[q + d for d in order], - extra_coords=[rgb + k * 3], + flags={f'color={"rgb"[rgb]}', f'basis={bases[k]}'}, ) ) @@ -59,14 +59,9 @@ def is_in_bounds(q: complex) -> bool: filtered_tiles = [] for tile in tiles: - new_tile = gen.Tile( - bases=tile.bases, - measurement_qubit=tile.measurement_qubit, - ordered_data_qubits=[ - (q if is_in_bounds(q) else None) for q in tile.ordered_data_qubits - ], - extra_coords=tile.extra_coords, - ) + new_tile = tile.with_edits(ordered_data_qubits=[ + (q if is_in_bounds(q) else None) for q in tile.ordered_data_qubits + ]) if len(new_tile.data_set) >= 4: filtered_tiles.append(new_tile) @@ -103,7 +98,7 @@ def do_cxs( d_target: complex, inv: Callable[[complex], bool] = lambda _: False, ) -> None: - builder.gate2( + builder.append( "CX", [ (c + d_control, c + d_target)[:: -1 if inv(c) else +1] @@ -113,10 +108,10 @@ def do_cxs( ], ) - builder.gate("RX", x_ms) + builder.append("RX", x_ms) if initialize: - builder.gate(f"R{basis}", code.patch.data_set) - builder.gate("RZ", z_ms) + builder.append(f"R{basis}", code.patch.data_set) + builder.append("RZ", z_ms) builder.tick() do_cxs(x_ms, +0, +1) @@ -142,13 +137,11 @@ def do_cxs( do_cxs(x_ms, +0, +1) builder.tick() - builder.measure(x_ms, basis="X", save_layer="solo") - builder.measure(z_ms, basis="Z", save_layer="solo") + builder.append('MX', x_ms) + builder.append('MZ', z_ms) def mf(*qs): - return builder.tracker.measurement_indices( - [gen.AtLayer(q, "solo") for q in qs if q in code.patch.measure_set] - ) + return builder.lookup_recs(q for q in qs if q in code.patch.measure_set) flows = [] for tile in code.patch.tiles: @@ -160,7 +153,7 @@ def mf(*qs): start=tile.to_data_pauli_string(), measurement_indices=mf(m), center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) elif basis == "X": @@ -168,7 +161,7 @@ def mf(*qs): gen.Flow( measurement_indices=mf(m), center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) flows.append( @@ -176,7 +169,7 @@ def mf(*qs): end=tile.to_data_pauli_string(), measurement_indices=mf(m), center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) elif tile.basis == "Z": @@ -186,7 +179,7 @@ def mf(*qs): start=tile.to_data_pauli_string(), measurement_indices=mf(m), center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) elif basis == "Z": @@ -194,7 +187,7 @@ def mf(*qs): gen.Flow( measurement_indices=mf(m), center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) flows.append( @@ -202,7 +195,7 @@ def mf(*qs): end=tile.to_data_pauli_string(), measurement_indices=mf(m - 2j if m.imag > 0 else m, m + 2j), center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) @@ -242,6 +235,25 @@ def mf(*qs): ) +def f2c(flow: gen.Flow) -> list[float]: + c = 0 + if 'color=r' in flow.flags: + c += 0 + elif 'color=g' in flow.flags: + c += 1 + elif 'color=b' in flow.flags: + c += 2 + else: + raise NotImplementedError(f'{flow=}') + if 'basis=X' in flow.flags: + c += 0 + elif 'basis=Z' in flow.flags: + c += 3 + else: + raise NotImplementedError(f'{flow=}') + return [c] + + def make_superdense_color_code_circuit( *, base_data_width: int, @@ -260,12 +272,13 @@ def make_superdense_color_code_circuit( basis=basis, base_data_width=base_data_width, ) - end_round = first_round.inverted() + end_round = first_round.time_reversed() return gen.compile_chunks_into_circuit( [ first_round, gen.ChunkLoop([mid_round], repetitions=rounds - 2), end_round, - ] + ], + flow_to_extra_coords_func=f2c, ) diff --git a/src/clorco/color_code/_superdense_planar_color_code_circuits_test.py b/src/clorco/color_code/_superdense_planar_color_code_circuits_test.py index 17e2484..cd12289 100644 --- a/src/clorco/color_code/_superdense_planar_color_code_circuits_test.py +++ b/src/clorco/color_code/_superdense_planar_color_code_circuits_test.py @@ -196,7 +196,8 @@ def test_make_superdense_color_code_circuit_exact(): SHIFT_COORDS(0, 0, 1) TICK } - R 36 31 29 27 19 17 15 6 4 + M 36 + R 31 29 27 19 17 15 6 4 RX 33 25 23 21 13 11 9 3 1 TICK CX 33 36 25 31 23 29 21 27 13 19 11 17 9 15 3 6 1 4 @@ -218,34 +219,34 @@ def test_make_superdense_color_code_circuit_exact(): M 36 31 29 27 19 17 15 6 4 MX 35 34 32 30 28 26 24 22 20 18 16 14 12 10 8 7 5 2 0 33 25 23 21 13 11 9 3 1 DETECTOR(1, 0, 0, 0) rec[-14] rec[-12] rec[-11] rec[-10] rec[-2] rec[-1] - DETECTOR(1, 0, 0, 0) rec[-55] rec[-1] + DETECTOR(1, 0, 1, 0) rec[-56] rec[-1] DETECTOR(1, 2, 0, 2) rec[-15] rec[-13] rec[-12] rec[-11] rec[-1] - DETECTOR(1, 2, 0, 2) rec[-54] rec[-2] - DETECTOR(2, 0, 0, 3) rec[-46] rec[-45] rec[-29] - DETECTOR(2, 2, 0, 5) rec[-46] rec[-30] + DETECTOR(1, 2, 1, 2) rec[-55] rec[-2] + DETECTOR(2, 0, 0, 3) rec[-47] rec[-46] rec[-29] + DETECTOR(2, 2, 0, 5) rec[-47] rec[-30] DETECTOR(3, 1, 0, 1) rec[-21] rec[-18] rec[-17] rec[-15] rec[-14] rec[-12] rec[-4] - DETECTOR(3, 1, 0, 1) rec[-53] rec[-3] + DETECTOR(3, 1, 1, 1) rec[-54] rec[-3] DETECTOR(3, 3, 0, 0) rec[-22] rec[-19] rec[-18] rec[-16] rec[-15] rec[-13] rec[-5] rec[-3] - DETECTOR(3, 3, 0, 0) rec[-52] rec[-4] + DETECTOR(3, 3, 1, 0) rec[-53] rec[-4] DETECTOR(3, 5, 0, 2) rec[-23] rec[-20] rec[-19] rec[-16] rec[-4] - DETECTOR(3, 5, 0, 2) rec[-51] rec[-5] - DETECTOR(4, 1, 0, 4) rec[-43] rec[-31] - DETECTOR(4, 3, 0, 3) rec[-44] rec[-42] rec[-32] - DETECTOR(4, 5, 0, 5) rec[-43] rec[-33] + DETECTOR(3, 5, 1, 2) rec[-52] rec[-5] + DETECTOR(4, 1, 0, 4) rec[-44] rec[-31] + DETECTOR(4, 3, 0, 3) rec[-45] rec[-43] rec[-32] + DETECTOR(4, 5, 0, 5) rec[-44] rec[-33] DETECTOR(5, 0, 0, 0) rec[-26] rec[-24] rec[-21] rec[-17] rec[-7] rec[-6] - DETECTOR(5, 0, 0, 0) rec[-50] rec[-6] + DETECTOR(5, 0, 1, 0) rec[-51] rec[-6] DETECTOR(5, 2, 0, 2) rec[-27] rec[-25] rec[-24] rec[-22] rec[-21] rec[-18] rec[-8] rec[-6] - DETECTOR(5, 2, 0, 2) rec[-49] rec[-7] + DETECTOR(5, 2, 1, 2) rec[-50] rec[-7] DETECTOR(5, 4, 0, 1) rec[-25] rec[-23] rec[-22] rec[-19] rec[-7] - DETECTOR(5, 4, 0, 1) rec[-48] rec[-8] - DETECTOR(6, 0, 0, 3) rec[-41] rec[-40] rec[-34] - DETECTOR(6, 2, 0, 5) rec[-41] rec[-39] rec[-35] - DETECTOR(6, 4, 0, 4) rec[-40] rec[-36] + DETECTOR(5, 4, 1, 1) rec[-49] rec[-8] + DETECTOR(6, 0, 0, 3) rec[-42] rec[-41] rec[-34] + DETECTOR(6, 2, 0, 5) rec[-42] rec[-40] rec[-35] + DETECTOR(6, 4, 0, 4) rec[-41] rec[-36] DETECTOR(7, 1, 0, 1) rec[-28] rec[-27] rec[-26] rec[-24] - DETECTOR(7, 1, 0, 1) rec[-47] rec[-9] + DETECTOR(7, 1, 1, 1) rec[-48] rec[-9] DETECTOR(8, 1, 0, 4) rec[-37] OBSERVABLE_INCLUDE(0) rec[-28] rec[-27] rec[-26] rec[-25] rec[-24] rec[-23] rec[-22] rec[-21] rec[-20] rec[-19] rec[-18] rec[-17] rec[-16] rec[-15] rec[-14] rec[-13] rec[-12] rec[-11] rec[-10] rec[-9] rec[-8] rec[-7] rec[-5] rec[-4] rec[-3] rec[-2] - SHIFT_COORDS(0, 0, 1) + SHIFT_COORDS(0, 0, 2) TICK """ ) diff --git a/src/clorco/color_code/_toric_color_code_circuits.py b/src/clorco/color_code/_toric_color_code_circuits.py index 554d62d..4ee9a65 100644 --- a/src/clorco/color_code/_toric_color_code_circuits.py +++ b/src/clorco/color_code/_toric_color_code_circuits.py @@ -20,6 +20,25 @@ from clorco.color_code._color_code_layouts import make_toric_color_code_layout +def f2c(flow: gen.Flow) -> list[float]: + c = 0 + if 'color=r' in flow.flags: + c += 0 + elif 'color=g' in flow.flags: + c += 1 + elif 'color=b' in flow.flags: + c += 2 + else: + raise NotImplementedError(f'{flow=}') + if 'basis=X' in flow.flags: + c += 0 + elif 'basis=Z' in flow.flags: + c += 3 + else: + raise NotImplementedError(f'{flow=}') + return [c] + + def make_toric_color_code_circuit_with_magic_time_boundaries( *, rounds: int, @@ -54,10 +73,10 @@ def make_toric_color_code_circuit_with_magic_time_boundaries( assert rounds % rounds_per_chunk == 0 return gen.compile_chunks_into_circuit( [ - chunk.magic_init_chunk(), + chunk.mpp_init_chunk(), gen.ChunkLoop([chunk], repetitions=rounds // rounds_per_chunk), - chunk.magic_end_chunk(), - ] + chunk.mpp_end_chunk(), + ], flow_to_extra_coords_func=f2c, ).with_inlined_feedback() @@ -87,7 +106,7 @@ def do_cxs( d_target: complex, inv: Callable[[complex], bool] = lambda _: False, ) -> None: - builder.gate2( + builder.append( "CX", [ (wrap(c + d_control), wrap(c + d_target))[:: -1 if inv(c) else +1] @@ -95,8 +114,8 @@ def do_cxs( ], ) - builder.gate("RX", x_ms) - builder.gate("RZ", z_ms) + builder.append("RX", x_ms) + builder.append("RZ", z_ms) builder.tick() do_cxs(x_ms, +0, +1j) @@ -122,19 +141,17 @@ def do_cxs( do_cxs(x_ms, +0, +1j) builder.tick() - builder.measure(x_ms, basis="X", save_layer="solo") - builder.measure(z_ms, basis="Z", save_layer="solo") + builder.append('MX', x_ms) + builder.append('MZ', z_ms) builder.tick() def mf(*qs): - return builder.tracker.measurement_indices( - [gen.AtLayer(wrap(q), "solo") for q in qs] - ) + return builder.lookup_recs([wrap(q) for q in qs]) flows = [] for tile in code.patch.tiles: m = tile.measurement_qubit - rgb = m.real % 3 + rgb = int(m.real % 3) if tile.basis == "X": if ablate_into_matchable_code and rgb == 0: continue @@ -143,7 +160,7 @@ def mf(*qs): end=tile.to_data_pauli_string(), measurement_indices=mf(m), center=m, - additional_coords=[rgb], + flags={f'color={"rgb"[rgb]}', 'basis=X'}, ) ) flows.append( @@ -151,7 +168,7 @@ def mf(*qs): start=tile.to_data_pauli_string(), measurement_indices=mf(m), center=m, - additional_coords=[rgb], + flags={f'color={"rgb"[rgb]}', 'basis=X'}, ) ) elif tile.basis == "Z": @@ -162,7 +179,7 @@ def mf(*qs): start=tile.to_data_pauli_string(), measurement_indices=mf(m), center=m, - additional_coords=[3 + rgb], + flags={f'color={"rgb"[rgb]}', 'basis=Z'}, ) ) flows.append( @@ -170,7 +187,7 @@ def mf(*qs): end=tile.to_data_pauli_string(), measurement_indices=mf(m - 2, m + 2), center=m, - additional_coords=[3 + rgb], + flags={f'color={"rgb"[rgb]}', 'basis=Z'}, ) ) @@ -257,7 +274,7 @@ def do_cxs( d_target: complex, inv: Callable[[complex], bool] = lambda _: False, ) -> None: - builder.gate2( + builder.append( "CX", [ ( @@ -285,7 +302,7 @@ def do_cxs( do_cxs(x_ms, +0, +1j) builder.tick() builder.demolition_measure_with_feedback_passthrough( - xs=x_ms, zs=z_ms, save_layer="a" + xs=x_ms, zs=z_ms, measure_key_func=lambda e: (e, "a") ) builder.tick() do_cxs(z_ms, +1j, +0) @@ -304,7 +321,7 @@ def do_cxs( do_cxs(x_ms, +1j, +0) builder.tick() builder.demolition_measure_with_feedback_passthrough( - xs=z_ms, zs=x_ms, save_layer="b" + xs=z_ms, zs=x_ms, measure_key_func=lambda e: (e, "b") ) builder.tick() do_cxs(z_ms, +0, +1j) @@ -316,19 +333,15 @@ def do_cxs( builder.tick() def ma(*qs) -> list[int]: - return builder.tracker.measurement_indices( - [gen.AtLayer(wrap(q), "a") for q in qs] - ) + return builder.lookup_recs((wrap(q), "a") for q in qs) def mb(*qs) -> list[int]: - return builder.tracker.measurement_indices( - [gen.AtLayer(wrap(q), "b") for q in qs] - ) + return builder.lookup_recs((wrap(q), "b") for q in qs) flows = [] for tile in code.patch.tiles: m = tile.measurement_qubit - if ablate_into_matchable_code and tile.extra_coords[0] in [0, 5]: + if ablate_into_matchable_code and (tile.flags == {'color=r', 'basis=X'} or tile.flags == {'color=b', 'basis=Z'}): continue if tile.basis == "X": mids = mb(m + 1) if m.real % 2 == 1 else ma(m - 1) @@ -341,7 +354,7 @@ def mb(*qs) -> list[int]: start=tile.to_data_pauli_string(), measurement_indices=mids, center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) flows.append( @@ -349,7 +362,7 @@ def mb(*qs) -> list[int]: end=tile.to_data_pauli_string(), measurement_indices=mids, center=m, - additional_coords=tile.extra_coords, + flags=tile.flags, ) ) diff --git a/src/clorco/color_code/_toric_color_code_circuits_test.py b/src/clorco/color_code/_toric_color_code_circuits_test.py index f5854e9..2532341 100644 --- a/src/clorco/color_code/_toric_color_code_circuits_test.py +++ b/src/clorco/color_code/_toric_color_code_circuits_test.py @@ -162,11 +162,11 @@ def test_make_toric_color_code_circuit_with_magic_time_boundaries_exact_circuit( QUBIT_COORDS(5, 5) 47 QUBIT_COORDS(5, 6) 48 QUBIT_COORDS(5, 7) 49 - MPP X4*X9*X10*X11*X42*X43 Z4*Z9*Z10*Z11*Z42*Z43 X5*X8*X14*X15*X46*X47 Z5*Z8*Z14*Z15*Z46*Z47 X4*X5*X11*X14*X20*X21 Z4*Z5*Z11*Z14*Z20*Z21 X8*X9*X10*X15*X24*X25 Z8*Z9*Z10*Z15*Z24*Z25 X10*X11*X20*X25*X26*X27 Z10*Z11*Z20*Z25*Z26*Z27 X14*X15*X21*X24*X30*X31 Z14*Z15*Z21*Z24*Z30*Z31 X20*X21*X27*X30*X36*X37 Z20*Z21*Z27*Z30*Z36*Z37 X24*X25*X26*X31*X40*X41 Z24*Z25*Z26*Z31*Z40*Z41 X26*X27*X36*X41*X42*X43 Z26*Z27*Z36*Z41*Z42*Z43 X30*X31*X37*X40*X46*X47 Z30*Z31*Z37*Z40*Z46*Z47 X4*X5*X36*X37*X43*X46 Z4*Z5*Z36*Z37*Z43*Z46 X8*X9*X40*X41*X42*X47 Z8*Z9*Z40*Z41*Z42*Z47 X1*X4*X5*X8*X9 X0*X14*X21*X37*X46 Z1*Z4*Z11*Z27*Z36 Z0*Z20*Z21*Z24*Z25 - OBSERVABLE_INCLUDE(0) rec[-4] - OBSERVABLE_INCLUDE(1) rec[-3] - OBSERVABLE_INCLUDE(2) rec[-2] - OBSERVABLE_INCLUDE(3) rec[-1] + MPP X1*X4*X5*X8*X9 X0*X14*X21*X37*X46 Z1*Z4*Z11*Z27*Z36 Z0*Z20*Z21*Z24*Z25 X4*X5*X11*X14*X20*X21 X4*X5*X36*X37*X43*X46 X4*X9*X10*X11*X42*X43 Z4*Z5*Z11*Z14*Z20*Z21 Z4*Z5*Z36*Z37*Z43*Z46 Z4*Z9*Z10*Z11*Z42*Z43 X5*X8*X14*X15*X46*X47 Z5*Z8*Z14*Z15*Z46*Z47 X8*X9*X10*X15*X24*X25 X8*X9*X40*X41*X42*X47 Z8*Z9*Z10*Z15*Z24*Z25 Z8*Z9*Z40*Z41*Z42*Z47 X10*X11*X20*X25*X26*X27 Z10*Z11*Z20*Z25*Z26*Z27 X14*X15*X21*X24*X30*X31 Z14*Z15*Z21*Z24*Z30*Z31 X20*X21*X27*X30*X36*X37 Z20*Z21*Z27*Z30*Z36*Z37 X24*X25*X26*X31*X40*X41 Z24*Z25*Z26*Z31*Z40*Z41 X26*X27*X36*X41*X42*X43 Z26*Z27*Z36*Z41*Z42*Z43 X30*X31*X37*X40*X46*X47 Z30*Z31*Z37*Z40*Z46*Z47 + OBSERVABLE_INCLUDE(0) rec[-28] + OBSERVABLE_INCLUDE(1) rec[-27] + OBSERVABLE_INCLUDE(2) rec[-26] + OBSERVABLE_INCLUDE(3) rec[-25] TICK RX 2 6 12 16 18 22 28 32 34 38 44 48 R 3 7 13 17 19 23 29 33 35 39 45 49 @@ -203,32 +203,32 @@ def test_make_toric_color_code_circuit_with_magic_time_boundaries_exact_circuit( MX(0.001) 2 6 12 16 18 22 28 32 34 38 44 48 M(0.001) 3 7 13 17 19 23 29 33 35 39 45 49 DEPOLARIZE1(0.001) 2 6 12 16 18 22 28 32 34 38 44 48 3 7 13 17 19 23 29 33 35 39 45 49 4 5 8 9 10 11 14 15 20 21 24 25 26 27 30 31 36 37 40 41 42 43 46 47 - DETECTOR(0, 0, 0, 0) rec[-52] rec[-24] - DETECTOR(0, 1, 0, 3) rec[-51] rec[-12] - DETECTOR(0, 4, 0, 0) rec[-50] rec[-23] - DETECTOR(0, 5, 0, 3) rec[-49] rec[-11] - DETECTOR(1, 2, 0, 1) rec[-48] rec[-22] - DETECTOR(1, 3, 0, 4) rec[-47] rec[-10] - DETECTOR(1, 6, 0, 1) rec[-46] rec[-21] - DETECTOR(1, 7, 0, 4) rec[-45] rec[-9] - DETECTOR(2, 0, 0, 2) rec[-44] rec[-20] - DETECTOR(2, 1, 0, 5) rec[-43] rec[-8] - DETECTOR(2, 4, 0, 2) rec[-42] rec[-19] - DETECTOR(2, 5, 0, 5) rec[-41] rec[-7] - DETECTOR(3, 2, 0, 0) rec[-40] rec[-18] - DETECTOR(3, 3, 0, 3) rec[-39] rec[-6] - DETECTOR(3, 6, 0, 0) rec[-38] rec[-17] - DETECTOR(3, 7, 0, 3) rec[-37] rec[-5] - DETECTOR(4, 0, 0, 1) rec[-36] rec[-16] - DETECTOR(4, 1, 0, 4) rec[-35] rec[-4] - DETECTOR(4, 4, 0, 1) rec[-34] rec[-15] - DETECTOR(4, 5, 0, 4) rec[-33] rec[-3] - DETECTOR(5, 2, 0, 2) rec[-32] rec[-14] - DETECTOR(5, 3, 0, 5) rec[-31] rec[-2] - DETECTOR(5, 6, 0, 2) rec[-30] rec[-13] - DETECTOR(5, 7, 0, 5) rec[-29] rec[-1] + DETECTOR(0, 1, 0, 0) rec[-46] rec[-24] + DETECTOR(0, 1.5, 0, 3) rec[-43] rec[-12] + DETECTOR(0, 3.5, 0, 0) rec[-42] rec[-23] + DETECTOR(0, 4, 0, 3) rec[-41] rec[-11] + DETECTOR(0.5, 2, 0, 1) rec[-48] rec[-22] + DETECTOR(0.5, 2.5, 0, 4) rec[-45] rec[-10] + DETECTOR(0.5, 6, 0, 1) rec[-40] rec[-21] + DETECTOR(0.5, 6.5, 0, 4) rec[-38] rec[-9] + DETECTOR(1.5, 0, 0, 2) rec[-36] rec[-20] + DETECTOR(1.5, 0.5, 0, 5) rec[-35] rec[-8] + DETECTOR(1.5, 4, 0, 2) rec[-34] rec[-19] + DETECTOR(1.5, 4.5, 0, 5) rec[-33] rec[-7] + DETECTOR(2.5, 2, 0, 0) rec[-32] rec[-18] + DETECTOR(2.5, 2.5, 0, 3) rec[-31] rec[-6] + DETECTOR(2.5, 6, 0, 0) rec[-30] rec[-17] + DETECTOR(2.5, 6.5, 0, 3) rec[-29] rec[-5] + DETECTOR(3.5, 0, 0, 1) rec[-28] rec[-16] + DETECTOR(3.5, 0.5, 0, 4) rec[-27] rec[-4] + DETECTOR(3.5, 4, 0, 1) rec[-26] rec[-15] + DETECTOR(3.5, 4.5, 0, 4) rec[-25] rec[-3] + DETECTOR(2.5, 2, 1, 2) rec[-47] rec[-14] + DETECTOR(2.5, 2.5, 1, 5) rec[-44] rec[-2] + DETECTOR(2.5, 6, 1, 2) rec[-39] rec[-13] + DETECTOR(2.5, 6.5, 1, 5) rec[-37] rec[-1] OBSERVABLE_INCLUDE(3) rec[-10] rec[-9] rec[-8] rec[-7] rec[-6] rec[-5] - SHIFT_COORDS(0, 0, 1) + SHIFT_COORDS(0, 0, 2) TICK REPEAT 4 { RX 2 6 12 16 18 22 28 32 34 38 44 48 @@ -294,36 +294,36 @@ def test_make_toric_color_code_circuit_with_magic_time_boundaries_exact_circuit( SHIFT_COORDS(0, 0, 1) TICK } - MPP X4*X9*X10*X11*X42*X43 Z4*Z9*Z10*Z11*Z42*Z43 X5*X8*X14*X15*X46*X47 Z5*Z8*Z14*Z15*Z46*Z47 X4*X5*X11*X14*X20*X21 Z4*Z5*Z11*Z14*Z20*Z21 X8*X9*X10*X15*X24*X25 Z8*Z9*Z10*Z15*Z24*Z25 X10*X11*X20*X25*X26*X27 Z10*Z11*Z20*Z25*Z26*Z27 X14*X15*X21*X24*X30*X31 Z14*Z15*Z21*Z24*Z30*Z31 X20*X21*X27*X30*X36*X37 Z20*Z21*Z27*Z30*Z36*Z37 X24*X25*X26*X31*X40*X41 Z24*Z25*Z26*Z31*Z40*Z41 X26*X27*X36*X41*X42*X43 Z26*Z27*Z36*Z41*Z42*Z43 X30*X31*X37*X40*X46*X47 Z30*Z31*Z37*Z40*Z46*Z47 X4*X5*X36*X37*X43*X46 Z4*Z5*Z36*Z37*Z43*Z46 X8*X9*X40*X41*X42*X47 Z8*Z9*Z40*Z41*Z42*Z47 X1*X4*X5*X8*X9 X0*X14*X21*X37*X46 Z1*Z4*Z11*Z27*Z36 Z0*Z20*Z21*Z24*Z25 - DETECTOR(0, 0, 0, 0) rec[-52] rec[-28] - DETECTOR(0, 1, 0, 3) rec[-36] rec[-32] rec[-27] - DETECTOR(0, 4, 0, 0) rec[-51] rec[-26] - DETECTOR(0, 5, 0, 3) rec[-35] rec[-31] rec[-25] - DETECTOR(1, 2, 0, 1) rec[-50] rec[-24] - DETECTOR(1, 3, 0, 4) rec[-34] rec[-30] rec[-23] - DETECTOR(1, 6, 0, 1) rec[-49] rec[-22] - DETECTOR(1, 7, 0, 4) rec[-33] rec[-29] rec[-21] - DETECTOR(2, 0, 0, 2) rec[-48] rec[-20] - DETECTOR(2, 1, 0, 5) rec[-40] rec[-32] rec[-19] - DETECTOR(2, 4, 0, 2) rec[-47] rec[-18] - DETECTOR(2, 5, 0, 5) rec[-39] rec[-31] rec[-17] - DETECTOR(3, 2, 0, 0) rec[-46] rec[-16] - DETECTOR(3, 3, 0, 3) rec[-38] rec[-30] rec[-15] - DETECTOR(3, 6, 0, 0) rec[-45] rec[-14] - DETECTOR(3, 7, 0, 3) rec[-37] rec[-29] rec[-13] - DETECTOR(4, 0, 0, 1) rec[-44] rec[-12] - DETECTOR(4, 1, 0, 4) rec[-40] rec[-36] rec[-11] - DETECTOR(4, 4, 0, 1) rec[-43] rec[-10] - DETECTOR(4, 5, 0, 4) rec[-39] rec[-35] rec[-9] - DETECTOR(5, 2, 0, 2) rec[-42] rec[-8] - DETECTOR(5, 3, 0, 5) rec[-38] rec[-34] rec[-7] - DETECTOR(5, 6, 0, 2) rec[-41] rec[-6] - DETECTOR(5, 7, 0, 5) rec[-37] rec[-33] rec[-5] - OBSERVABLE_INCLUDE(0) rec[-4] - OBSERVABLE_INCLUDE(1) rec[-3] - OBSERVABLE_INCLUDE(2) rec[-2] - OBSERVABLE_INCLUDE(3) rec[-1] - SHIFT_COORDS(0, 0, 1) + MPP X1*X4*X5*X8*X9 X0*X14*X21*X37*X46 Z1*Z4*Z11*Z27*Z36 Z0*Z20*Z21*Z24*Z25 X4*X5*X11*X14*X20*X21 X4*X5*X36*X37*X43*X46 X4*X9*X10*X11*X42*X43 Z4*Z5*Z11*Z14*Z20*Z21 Z4*Z5*Z36*Z37*Z43*Z46 Z4*Z9*Z10*Z11*Z42*Z43 X5*X8*X14*X15*X46*X47 Z5*Z8*Z14*Z15*Z46*Z47 X8*X9*X10*X15*X24*X25 X8*X9*X40*X41*X42*X47 Z8*Z9*Z10*Z15*Z24*Z25 Z8*Z9*Z40*Z41*Z42*Z47 X10*X11*X20*X25*X26*X27 Z10*Z11*Z20*Z25*Z26*Z27 X14*X15*X21*X24*X30*X31 Z14*Z15*Z21*Z24*Z30*Z31 X20*X21*X27*X30*X36*X37 Z20*Z21*Z27*Z30*Z36*Z37 X24*X25*X26*X31*X40*X41 Z24*Z25*Z26*Z31*Z40*Z41 X26*X27*X36*X41*X42*X43 Z26*Z27*Z36*Z41*Z42*Z43 X30*X31*X37*X40*X46*X47 Z30*Z31*Z37*Z40*Z46*Z47 + OBSERVABLE_INCLUDE(0) rec[-28] + OBSERVABLE_INCLUDE(1) rec[-27] + OBSERVABLE_INCLUDE(2) rec[-26] + OBSERVABLE_INCLUDE(3) rec[-25] + DETECTOR(0.5, 2, 0, 1) rec[-50] rec[-24] + DETECTOR(2.5, 2, 0, 2) rec[-42] rec[-23] + DETECTOR(0, 1, 0, 0) rec[-52] rec[-22] + DETECTOR(0.5, 2.5, 0, 4) rec[-34] rec[-30] rec[-21] + DETECTOR(2.5, 2.5, 0, 5) rec[-38] rec[-34] rec[-20] + DETECTOR(0, 1.5, 0, 3) rec[-36] rec[-32] rec[-19] + DETECTOR(0, 3.5, 0, 0) rec[-51] rec[-18] + DETECTOR(0, 4, 0, 3) rec[-35] rec[-31] rec[-17] + DETECTOR(0.5, 6, 0, 1) rec[-49] rec[-16] + DETECTOR(2.5, 6, 0, 2) rec[-41] rec[-15] + DETECTOR(0.5, 6.5, 0, 4) rec[-33] rec[-29] rec[-14] + DETECTOR(2.5, 6.5, 0, 5) rec[-37] rec[-33] rec[-13] + DETECTOR(1.5, 0, 0, 2) rec[-48] rec[-12] + DETECTOR(1.5, 0.5, 0, 5) rec[-40] rec[-32] rec[-11] + DETECTOR(1.5, 4, 0, 2) rec[-47] rec[-10] + DETECTOR(1.5, 4.5, 0, 5) rec[-39] rec[-31] rec[-9] + DETECTOR(2.5, 2, 1, 0) rec[-46] rec[-8] + DETECTOR(2.5, 2.5, 1, 3) rec[-38] rec[-30] rec[-7] + DETECTOR(2.5, 6, 1, 0) rec[-45] rec[-6] + DETECTOR(2.5, 6.5, 1, 3) rec[-37] rec[-29] rec[-5] + DETECTOR(3.5, 0, 0, 1) rec[-44] rec[-4] + DETECTOR(3.5, 0.5, 0, 4) rec[-40] rec[-36] rec[-3] + DETECTOR(3.5, 4, 0, 1) rec[-43] rec[-2] + DETECTOR(3.5, 4.5, 0, 4) rec[-39] rec[-35] rec[-1] + SHIFT_COORDS(0, 0, 2) TICK """ ) diff --git a/src/clorco/pyramid_code/_keyed_constructions.py b/src/clorco/pyramid_code/_keyed_constructions.py index e737de4..0cc885a 100644 --- a/src/clorco/pyramid_code/_keyed_constructions.py +++ b/src/clorco/pyramid_code/_keyed_constructions.py @@ -22,6 +22,25 @@ from clorco.pyramid_code._pyramid_code_layouts import make_toric_pyramid_code_layout +def f2c(flow: gen.Flow) -> list[float]: + c = 0 + if 'color=r' in flow.flags: + c += 0 + elif 'color=g' in flow.flags: + c += 1 + elif 'color=b' in flow.flags: + c += 2 + else: + raise NotImplementedError(f'{flow=}') + if 'basis=X' in flow.flags: + c += 0 + elif 'basis=Z' in flow.flags: + c += 3 + else: + raise NotImplementedError(f'{flow=}') + return [c] + + def make_named_pyramid_code_constructions() -> ( dict[str, Callable[[Params], stim.Circuit]] ): @@ -47,14 +66,14 @@ def _make_simple_circuit( ]) if phenom: return code.make_phenom_circuit( - noise=params.noise_model, + noise=params.noise_model.idle_depolarization, rounds=params.rounds, - debug_out_dir=params.debug_out_dir, + extra_coords_func=f2c, ) assert params.rounds == 1 return code.make_code_capacity_circuit( - noise=params.noise_model.idle_noise, - debug_out_dir=params.debug_out_dir + noise=params.noise_model.idle_depolarization, + extra_coords_func=f2c, ) constructions["transit_pyramid_code"] = lambda params: _make_simple_circuit( diff --git a/src/clorco/pyramid_code/_pyramid_code_layouts.py b/src/clorco/pyramid_code/_pyramid_code_layouts.py index 5de03e2..ef7b4b9 100644 --- a/src/clorco/pyramid_code/_pyramid_code_layouts.py +++ b/src/clorco/pyramid_code/_pyramid_code_layouts.py @@ -36,7 +36,7 @@ def wrap(v: complex) -> complex: bases=b, ordered_data_qubits=[wrap(c + d) for d in [1, -1, 1j, -1j, 0]], measurement_qubit=c, - extra_coords=[y % 3 + 3 * (b == "Z")], + flags=[f'color={"rgb"[y % 3]}', f'basis={b}'], ) ) patch = gen.Patch(tiles) @@ -90,7 +90,7 @@ def make_planar_pyramid_code_layout( bases=b, ordered_data_qubits=qs, measurement_qubit=c, - extra_coords=[y % 3 + 3 * (b == "Z")], + flags=[f'color={"rgb"[y % 3]}', f'basis={b}'], ) ) patch = gen.Patch(tiles) diff --git a/src/clorco/pyramid_code/_pyramid_code_layouts_test.py b/src/clorco/pyramid_code/_pyramid_code_layouts_test.py index 9a597a4..aa4ce54 100644 --- a/src/clorco/pyramid_code/_pyramid_code_layouts_test.py +++ b/src/clorco/pyramid_code/_pyramid_code_layouts_test.py @@ -25,7 +25,7 @@ def test_make_toric_pyramid_code_layout(w: int, h: int): width=w, height=h, ) - code.check_commutation_relationships() + code.verify() assert ( len(code.patch.data_set) == len(code.patch.tiles) @@ -48,7 +48,7 @@ def test_make_planar_pyramid_code_layout(w: int, h: int): width=w, height=h, ) - code.check_commutation_relationships() + code.verify() c = code.make_code_capacity_circuit(noise=1e-3) assert len(code.patch.data_set) == len(code.patch.tiles) + max( len(code.observables_x), len(code.observables_z) diff --git a/src/clorco/rep_code/_keyed_constructions.py b/src/clorco/rep_code/_keyed_constructions.py index c365030..bb9830f 100644 --- a/src/clorco/rep_code/_keyed_constructions.py +++ b/src/clorco/rep_code/_keyed_constructions.py @@ -96,6 +96,25 @@ def _make_circuit( return circuit +def f2c(flow: gen.Flow) -> list[float]: + c = 0 + if 'color=r' in flow.flags: + c += 0 + elif 'color=g' in flow.flags: + c += 1 + elif 'color=b' in flow.flags: + c += 2 + else: + raise NotImplementedError(f'{flow=}') + if 'basis=X' in flow.flags: + c += 0 + elif 'basis=Z' in flow.flags: + c += 3 + else: + raise NotImplementedError(f'{flow=}') + return [c] + + def _simplified_noise_rep_code_constructions() -> ( dict[str, Callable[[Params], stim.Circuit]] ): @@ -115,14 +134,14 @@ def _make_simple_circuit( ) if phenom: return code.make_phenom_circuit( - noise=params.noise_model, + noise=params.noise_model.idle_depolarization, rounds=params.rounds, - debug_out_dir=params.debug_out_dir, + extra_coords_func=f2c, ) assert params.rounds == 1 return code.make_code_capacity_circuit( - noise=params.noise_model.idle_noise, - debug_out_dir=params.debug_out_dir + noise=params.noise_model.idle_depolarization, + extra_coords_func=f2c, ) for coloring in ["r", "rg", "rbrrr"]: diff --git a/src/clorco/rep_code/_rep_code_circuits.py b/src/clorco/rep_code/_rep_code_circuits.py index e1db824..86cd42c 100644 --- a/src/clorco/rep_code/_rep_code_circuits.py +++ b/src/clorco/rep_code/_rep_code_circuits.py @@ -46,23 +46,24 @@ def make_rep_code_circuit( for cur_round in range(rounds): if cur_round > 0: builder.tick() - builder.gate( + builder.append( "R", code.patch.used_set if cur_round == 0 else code.patch.measure_set ) builder.tick() - builder.gate2("CX", [(m - 0.5, m) for m in code.patch.measure_set]) + builder.append("CX", [(m - 0.5, m) for m in code.patch.measure_set]) builder.tick() - builder.gate2( + builder.append( "CX", [((m.real + 0.5) % distance, m) for m in code.patch.measure_set] ) builder.tick() - builder.measure( + builder.append( + 'MZ', code.patch.used_set if cur_round == rounds - 1 else code.patch.measure_set, - save_layer=cur_round, + measure_key_func=lambda e: (e, cur_round), ) for m in gen.sorted_complex(code.patch.measure_set): - m_key = gen.AtLayer(m, cur_round) - other_key = [] if cur_round == 0 else [gen.AtLayer(m, cur_round - 1)] + m_key = (m, cur_round) + other_key = [] if cur_round == 0 else [(m, cur_round - 1)] rc = round_colorings[cur_round % len(round_colorings)] c = color_indices[rc[int(m.real % len(rc) - 0.5)]] builder.detector([m_key] + other_key, pos=m, extra_coords=c) @@ -72,12 +73,12 @@ def make_rep_code_circuit( c = color_indices[rc[int(m.real % len(rc) - 0.5)]] builder.detector( [ - gen.AtLayer(m, rounds - 1), - gen.AtLayer((m.real + 0.5) % distance, rounds - 1), - gen.AtLayer(m - 0.5, rounds - 1), + (m, rounds - 1), + ((m.real + 0.5) % distance, rounds - 1), + (m - 0.5, rounds - 1), ], pos=m, extra_coords=c, ) - builder.obs_include([gen.AtLayer(0, rounds - 1)], obs_index=0) + builder.obs_include([(0, rounds - 1)], obs_index=0) return builder.circuit diff --git a/src/clorco/rep_code/_rep_code_layouts.py b/src/clorco/rep_code/_rep_code_layouts.py index dfee9ce..397b5de 100644 --- a/src/clorco/rep_code/_rep_code_layouts.py +++ b/src/clorco/rep_code/_rep_code_layouts.py @@ -59,13 +59,14 @@ def make_rep_code_layout( "B": 5, } for x in range(distance - (0 if toric else 1)): - c = color_indices[coloring[x % len(coloring)]] + color = coloring[x % len(coloring)].lower() + c = color_indices[color] tiles.append( gen.Tile( ordered_data_qubits=[x, (x + 1) % distance], bases="XYZ"[c % 3] if replace_basis_with_coloring else "Z", measurement_qubit=x + 0.5, - extra_coords=[c], + flags={f'color={color}', f'basis=Z'}, ) ) patch = gen.Patch(tiles) diff --git a/src/clorco/rep_code/_rep_code_layouts_test.py b/src/clorco/rep_code/_rep_code_layouts_test.py index e3cb7e0..8be2ea8 100644 --- a/src/clorco/rep_code/_rep_code_layouts_test.py +++ b/src/clorco/rep_code/_rep_code_layouts_test.py @@ -18,7 +18,7 @@ def test_make_rep_code_layout(): v = make_rep_code_layout(distance=5, coloring="rgb") - v.check_commutation_relationships() + v.verify() assert v == gen.StabilizerCode( patch=gen.Patch( tiles=[ @@ -26,36 +26,36 @@ def test_make_rep_code_layout(): ordered_data_qubits=(0, 1), measurement_qubit=0.5, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(1, 2), measurement_qubit=1.5, bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(2, 3), measurement_qubit=2.5, bases="Z", - extra_coords=(5,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(3, 4), measurement_qubit=3.5, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), ] ), observables_x=[], observables_z=[ - gen.PauliString(qubits={0: "Z"}), + gen.PauliString({0: "Z"}), ], ) v = make_rep_code_layout(distance=3, coloring="rg", toric=True) - v.check_commutation_relationships() + v.verify() assert v == gen.StabilizerCode( patch=gen.Patch( tiles=[ @@ -63,24 +63,24 @@ def test_make_rep_code_layout(): ordered_data_qubits=(0, 1), measurement_qubit=0.5, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(1, 2), measurement_qubit=1.5, bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(2, 0), measurement_qubit=2.5, bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), ] ), observables_x=[], observables_z=[ - gen.PauliString(qubits={0: "Z"}), + gen.PauliString({0: "Z"}), ], ) diff --git a/src/clorco/surface_code/_keyed_constructions.py b/src/clorco/surface_code/_keyed_constructions.py index 5dc7e6a..53790eb 100644 --- a/src/clorco/surface_code/_keyed_constructions.py +++ b/src/clorco/surface_code/_keyed_constructions.py @@ -85,13 +85,32 @@ def make_named_surface_code_constructions() -> ( return constructions +def f2c(flow: gen.Flow) -> list[float]: + c = 0 + if 'color=r' in flow.flags: + c += 0 + elif 'color=g' in flow.flags: + c += 1 + elif 'color=b' in flow.flags: + c += 2 + else: + raise NotImplementedError(f'{flow=}') + if 'basis=X' in flow.flags: + c += 0 + elif 'basis=Z' in flow.flags: + c += 3 + else: + raise NotImplementedError(f'{flow=}') + return [c] + + def _chunks_to_circuit(params: Params, chunks: list[gen.Chunk]) -> stim.Circuit: assert len(chunks) >= 2 if "magic" not in params.style: - assert not any(chunk.magic for chunk in chunks) + assert not any(inst.name == 'MPP' for chunk in chunks if isinstance(chunk, gen.Chunk) for inst in chunk.circuit) if params.debug_out_dir is not None: - patches = [chunk.end_patch() for chunk in chunks[:-1]] + patches = [chunk.end_interface().to_patch() for chunk in chunks[:-1]] changed_patches = [ patches[k] for k in range(len(patches)) @@ -103,7 +122,7 @@ def _chunks_to_circuit(params: Params, chunks: list[gen.Chunk]) -> stim.Circuit: gen.patch_svg_viewer( changed_patches, show_order=False, - available_qubits=allowed_qubits, + expected_points=allowed_qubits, ), ) @@ -114,16 +133,16 @@ def _chunks_to_circuit(params: Params, chunks: list[gen.Chunk]) -> stim.Circuit: patch_dict = {} cur_tick = 0 last_patch = gen.Patch([]) - if chunks[0].start_patch() != last_patch: - patch_dict[0] = chunks[0].start_patch() - last_patch = chunks[0].start_patch() + if chunks[0].start_interface().to_patch() != last_patch: + patch_dict[0] = chunks[0].start_interface().to_patch() + last_patch = chunks[0].start_interface().to_patch() cur_tick += 1 for c in gen.ChunkLoop(chunks, repetitions=1).flattened(): cur_tick += c.tick_count() - if c.end_patch() != last_patch: - patch_dict[cur_tick] = c.end_patch() - last_patch = c.end_patch() + if c.end_interface().to_patch() != last_patch: + patch_dict[cur_tick] = c.end_interface().to_patch() + last_patch = c.end_interface().to_patch() cur_tick += 1 gen.write_file( params.debug_out_dir / "ideal_circuit.html", @@ -140,53 +159,39 @@ def _chunks_to_circuit(params: Params, chunks: list[gen.Chunk]) -> stim.Circuit: ignore_errors_ideal_circuit.diagram("time+detector-slice-svg"), ) - body = gen.compile_chunks_into_circuit(chunks) - mpp_indices = [ - k - for k, inst in enumerate(body) - if isinstance(inst, stim.CircuitInstruction) and inst.name == "MPP" - ] - skip_mpp_head = chunks[0].magic - skip_mpp_tail = chunks[-1].magic - body_start = mpp_indices[0] + 2 if skip_mpp_head else 0 - body_end = mpp_indices[-1] if skip_mpp_tail else len(body) - magic_head = body[:body_start] - magic_tail = body[body_end:] - body = body[body_start:body_end] + body = gen.compile_chunks_into_circuit(chunks, flow_to_extra_coords_func=f2c) if params.convert_to_cz: body = gen.transpile_to_z_basis_interaction_circuit(body) if params.debug_out_dir is not None: - ideal_circuit = magic_head + body + magic_tail gen.write_file( params.debug_out_dir / "ideal_cz_circuit.html", gen.stim_circuit_html_viewer( - ideal_circuit, - patch=chunks[0].end_patch(), + body, + patch=chunks[0].end_interface().to_patch(), ), ) gen.write_file( - params.debug_out_dir / "ideal_cz_circuit.stim", ideal_circuit + params.debug_out_dir / "ideal_cz_circuit.stim", body ) gen.write_file( params.debug_out_dir / "ideal_cz_circuit_dets.svg", - ideal_circuit.diagram("time+detector-slice-svg"), + body.diagram("time+detector-slice-svg"), ) if params.noise_model is not None: - body = params.noise_model.noisy_circuit(body) - noisy_circuit = magic_head + body + magic_tail + body = params.noise_model.noisy_circuit_skipping_mpp_boundaries(body) if params.debug_out_dir is not None: gen.write_file( params.debug_out_dir / "noisy_circuit.html", gen.stim_circuit_html_viewer( - noisy_circuit, - patch=chunks[0].end_patch(), + body, + patch=chunks[0].end_interface().to_patch(), ), ) - return noisy_circuit + return body def _simplified_noise_surface_code_constructions() -> ( @@ -199,14 +204,14 @@ def _make_simple_circuit( ) -> stim.Circuit: if phenom: return code.make_phenom_circuit( - noise=params.noise_model, + noise=params.noise_model.idle_depolarization, rounds=params.rounds, - debug_out_dir=params.debug_out_dir, + extra_coords_func=f2c, ) assert params.rounds == 1 return code.make_code_capacity_circuit( - noise=params.noise_model.idle_noise, - debug_out_dir=params.debug_out_dir + noise=params.noise_model.idle_depolarization, + extra_coords_func=f2c, ) constructions["transit_surface_code"] = lambda params: _make_simple_circuit( diff --git a/src/clorco/surface_code/_surface_code_chunks.py b/src/clorco/surface_code/_surface_code_chunks.py index f1bf4c1..66cf042 100644 --- a/src/clorco/surface_code/_surface_code_chunks.py +++ b/src/clorco/surface_code/_surface_code_chunks.py @@ -37,17 +37,17 @@ def build_surface_code_round_circuit( elif isinstance(measure_data_basis, str): measure_data_basis = {q: measure_data_basis for q in patch.data_set} - out.gate("RX", measure_xs.measure_set) + out.append("RX", measure_xs.measure_set) for basis in "XYZ": qs = [q for q in init_data_basis if init_data_basis[q] == basis] if qs: - out.gate(f"R{basis}", qs) - out.gate("R", measure_zs.measure_set) + out.append(f"R{basis}", qs) + out.append("R", measure_zs.measure_set) out.tick() (num_layers,) = {len(tile.ordered_data_qubits) for tile in patch.tiles} for k in range(num_layers): - out.gate2( + out.append( "CX", [ (tile.measurement_qubit, tile.ordered_data_qubits[k])[ @@ -59,12 +59,12 @@ def build_surface_code_round_circuit( ) out.tick() - out.measure(measure_xs.measure_set, basis="X", save_layer=save_layer) + out.append('MX', measure_xs.measure_set, measure_key_func=lambda e: (e, save_layer)) for basis in "XYZ": qs = [q for q in measure_data_basis if measure_data_basis[q] == basis] if qs: - out.measure(qs, basis=basis, save_layer=save_layer) - out.measure(measure_zs.measure_set, basis="Z", save_layer=save_layer) + out.append(f'M{basis}', qs, measure_key_func=lambda e: (e, save_layer)) + out.append('MZ', measure_zs.measure_set, measure_key_func=lambda e: (e, save_layer)) def standard_surface_code_chunk( @@ -116,12 +116,10 @@ def standard_surface_code_chunk( gen.Flow( center=tile.measurement_qubit, start=from_prev, - measurement_indices=out.tracker.measurement_indices( - [gen.AtLayer(tile.measurement_qubit, save_layer)] + measurement_indices=out.lookup_recs( + [(tile.measurement_qubit, save_layer)] ), - additional_coords=[ - (tile.measurement_qubit.real % 2 == 0.5) + 3 * (tile.basis == "Z") - ], + flags={f'color={"rgb"[tile.measurement_qubit.real % 2 == 0.5]}', f'basis={tile.basis}'} ) ) @@ -145,16 +143,12 @@ def standard_surface_code_chunk( gen.Flow( center=tile.measurement_qubit, end=to_next, - measurement_indices=out.tracker.measurement_indices( - [ - gen.AtLayer(q, save_layer) - for q in tile.used_set - if q in measure_data_basis or q == tile.measurement_qubit - ] + measurement_indices=out.lookup_recs( + (q, save_layer) + for q in tile.used_set + if q in measure_data_basis or q == tile.measurement_qubit ), - additional_coords=[ - (tile.measurement_qubit.real % 2 == 0.5) + 3 * (tile.basis == "Z") - ], + flags={f'color={"rgb"[tile.measurement_qubit.real % 2 == 0.5]}', f'basis={tile.basis}'} ) ) @@ -171,7 +165,7 @@ def standard_surface_code_chunk( if end_obs.pop(q) != measure_data_basis[q]: raise ValueError("wrong measure basis for obs") measure_indices.extend( - out.tracker.measurement_indices([gen.AtLayer(q, save_layer)]) + out.lookup_recs([(q, save_layer)]) ) flows.append( diff --git a/src/clorco/surface_code/_surface_code_chunks_test.py b/src/clorco/surface_code/_surface_code_chunks_test.py index 570f145..59ea680 100644 --- a/src/clorco/surface_code/_surface_code_chunks_test.py +++ b/src/clorco/surface_code/_surface_code_chunks_test.py @@ -15,6 +15,7 @@ import stim import gen +from clorco.surface_code._keyed_constructions import f2c from clorco.surface_code._surface_code_chunks import standard_surface_code_chunk from clorco.surface_code._surface_code_patches import make_xtop_qubit_patch @@ -23,9 +24,9 @@ def test_magic_init_surface_code_chunk(): p = make_xtop_qubit_patch(diameter=3) obs = gen.PauliString({0: "X", 1j: "X", 2j: "X"}) c = standard_surface_code_chunk(p, obs=obs) - c2 = c.magic_init_chunk() + c2 = c.mpp_init_chunk() c2.verify() - c3 = c.magic_end_chunk() + c3 = c.mpp_end_chunk() c3.verify() assert gen.compile_chunks_into_circuit([c2, c3]) == stim.Circuit( """ @@ -38,28 +39,20 @@ def test_magic_init_surface_code_chunk(): QUBIT_COORDS(2, 0) 6 QUBIT_COORDS(2, 1) 7 QUBIT_COORDS(2, 2) 8 - QUBIT_COORDS(-0.5, 1.5) 9 - QUBIT_COORDS(0.5, -0.5) 10 - QUBIT_COORDS(0.5, 0.5) 11 - QUBIT_COORDS(0.5, 1.5) 12 - QUBIT_COORDS(1.5, 0.5) 13 - QUBIT_COORDS(1.5, 1.5) 14 - QUBIT_COORDS(1.5, 2.5) 15 - QUBIT_COORDS(2.5, 0.5) 16 - MPP Z1*Z2 X0*X3 Z0*Z1*Z3*Z4 X1*X2*X4*X5 X3*X4*X6*X7 Z4*Z5*Z7*Z8 X5*X8 Z6*Z7 X0*X1*X2 - OBSERVABLE_INCLUDE(0) rec[-1] - TICK - MPP Z1*Z2 X0*X3 Z0*Z1*Z3*Z4 X1*X2*X4*X5 X3*X4*X6*X7 Z4*Z5*Z7*Z8 X5*X8 Z6*Z7 X0*X1*X2 - DETECTOR(-0.5, 1.5, 0, 3) rec[-18] rec[-9] - DETECTOR(0.5, -0.5, 0, 1) rec[-17] rec[-8] - DETECTOR(0.5, 0.5, 0, 4) rec[-16] rec[-7] - DETECTOR(0.5, 1.5, 0, 1) rec[-15] rec[-6] - DETECTOR(1.5, 0.5, 0, 0) rec[-14] rec[-5] - DETECTOR(1.5, 1.5, 0, 3) rec[-13] rec[-4] - DETECTOR(1.5, 2.5, 0, 0) rec[-12] rec[-3] - DETECTOR(2.5, 0.5, 0, 4) rec[-11] rec[-2] - OBSERVABLE_INCLUDE(0) rec[-1] - SHIFT_COORDS(0, 0, 1) + MPP X0*X1*X2 X0*X3 Z0*Z1*Z3*Z4 X1*X2*X4*X5 Z1*Z2 X3*X4*X6*X7 Z4*Z5*Z7*Z8 X5*X8 Z6*Z7 + OBSERVABLE_INCLUDE(0) rec[-9] + TICK + MPP X0*X1*X2 X0*X3 Z0*Z1*Z3*Z4 X1*X2*X4*X5 Z1*Z2 X3*X4*X6*X7 Z4*Z5*Z7*Z8 X5*X8 Z6*Z7 + OBSERVABLE_INCLUDE(0) rec[-9] + DETECTOR(0, 0, 0) rec[-17] rec[-8] + DETECTOR(0, 0, 1) rec[-16] rec[-7] + DETECTOR(0, 1, 0) rec[-15] rec[-6] + DETECTOR(0, 1, 1) rec[-14] rec[-5] + DETECTOR(1, 0, 0) rec[-13] rec[-4] + DETECTOR(1, 1, 0) rec[-12] rec[-3] + DETECTOR(1, 2, 0) rec[-11] rec[-2] + DETECTOR(2, 0, 0) rec[-10] rec[-1] + SHIFT_COORDS(0, 0, 2) TICK """ ) @@ -76,7 +69,7 @@ def test_verify_normal_surface_code_chunk(): c2.verify() c3.verify() c4.verify() - circuit = gen.compile_chunks_into_circuit([c1, c2, c3]) + circuit = gen.compile_chunks_into_circuit([c1, c2, c3], flow_to_extra_coords_func=f2c) circuit.detector_error_model(decompose_errors=True) assert ( len( @@ -169,12 +162,12 @@ def test_verify_normal_surface_code_chunk(): DETECTOR(1.5, 1.5, 0, 3) rec[-19] rec[-2] DETECTOR(1.5, 2.5, 0, 0) rec[-22] rec[-14] DETECTOR(2.5, 0.5, 0, 4) rec[-18] rec[-1] - DETECTOR(0.5, -0.5, 0, 1) rec[-17] rec[-13] rec[-10] - DETECTOR(0.5, 1.5, 0, 1) rec[-16] rec[-12] rec[-11] rec[-9] rec[-8] - DETECTOR(1.5, 0.5, 0, 0) rec[-15] rec[-10] rec[-9] rec[-7] rec[-6] - DETECTOR(1.5, 2.5, 0, 0) rec[-14] rec[-8] rec[-5] + DETECTOR(0.5, -0.5, 1, 1) rec[-17] rec[-13] rec[-10] + DETECTOR(0.5, 1.5, 1, 1) rec[-16] rec[-12] rec[-11] rec[-9] rec[-8] + DETECTOR(1.5, 0.5, 1, 0) rec[-15] rec[-10] rec[-9] rec[-7] rec[-6] + DETECTOR(1.5, 2.5, 1, 0) rec[-14] rec[-8] rec[-5] OBSERVABLE_INCLUDE(0) rec[-13] rec[-12] rec[-11] - SHIFT_COORDS(0, 0, 1) + SHIFT_COORDS(0, 0, 2) TICK """ ) diff --git a/src/clorco/surface_code/_surface_code_layouts.py b/src/clorco/surface_code/_surface_code_layouts.py index 00aa3e0..ae7add1 100644 --- a/src/clorco/surface_code/_surface_code_layouts.py +++ b/src/clorco/surface_code/_surface_code_layouts.py @@ -15,6 +15,11 @@ import gen +UL, UR, DL, DR = [e * 0.5 for e in [-1 - 1j, +1 - 1j, -1 + 1j, +1 + 1j]] +Order_Z = [UL, UR, DL, DR] +Order_ᴎ = [UL, DL, UR, DR] + + def make_surface_code_layout( *, width: int, @@ -40,7 +45,7 @@ def make_surface_code_layout( continue if not (0 < m.imag < height - 1) and b == "Z": continue - order = gen.Order_Z if b == "X" else gen.Order_ᴎ + order = Order_Z if b == "X" else Order_ᴎ data_qubits = [m + d for d in order] tile = gen.Tile( ordered_data_qubits=[ @@ -49,7 +54,7 @@ def make_surface_code_layout( ], bases=b, measurement_qubit=m, - extra_coords=["XZ".index(b) * 3 + (x % 2) * 2], + flags={f'basis={b}', f'color={"rgb"[(x % 2) * 2]}'}, ) if sum(e is not None for e in tile.ordered_data_qubits) > 0: tiles.append(tile) @@ -93,13 +98,13 @@ def wrap(c: complex) -> complex: for y in range(height): m = wrap(x + 1j * y + 0.5 + 0.5j) b = "XZ"[(x + y) % 2] - order = gen.Order_ᴎ if b == "X" else gen.Order_Z + order = Order_ᴎ if b == "X" else Order_Z tiles.append( gen.Tile( ordered_data_qubits=[wrap(m + d) for d in order], bases=b, measurement_qubit=m, - extra_coords=["XZ".index(b) * 3 + x % 2], + flags={f'basis={b}', f'color={"rgb"[x % 2]}'}, ) ) patch = gen.Patch(tiles) diff --git a/src/clorco/surface_code/_surface_code_layouts_test.py b/src/clorco/surface_code/_surface_code_layouts_test.py index 6a250c7..fb83a1b 100644 --- a/src/clorco/surface_code/_surface_code_layouts_test.py +++ b/src/clorco/surface_code/_surface_code_layouts_test.py @@ -25,7 +25,7 @@ def test_make_surface_code_layout(): height=4, ) assert len(code.observables_x) == len(code.observables_z) == 1 - code.check_commutation_relationships() + code.verify() assert ( len(code.make_code_capacity_circuit(noise=1e-3).shortest_graphlike_error()) == 3 ) @@ -34,67 +34,67 @@ def test_make_surface_code_layout(): ordered_data_qubits=(None, None, 0j, 1j), measurement_qubit=(-0.5 + 0.5j), bases="Z", - extra_coords=(5,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(None, None, 2j, 3j), measurement_qubit=(-0.5 + 2.5j), bases="Z", - extra_coords=(5,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(0j, (1 + 0j), 1j, (1 + 1j)), measurement_qubit=(0.5 + 0.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=(1j, 2j, (1 + 1j), (1 + 2j)), measurement_qubit=(0.5 + 1.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(2j, (1 + 2j), 3j, (1 + 3j)), measurement_qubit=(0.5 + 2.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=(None, None, (1 + 0j), (2 + 0j)), measurement_qubit=(1.5 - 0.5j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((1 + 0j), (1 + 1j), (2 + 0j), (2 + 1j)), measurement_qubit=(1.5 + 0.5j), bases="Z", - extra_coords=(5,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((1 + 1j), (2 + 1j), (1 + 2j), (2 + 2j)), measurement_qubit=(1.5 + 1.5j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((1 + 2j), (1 + 3j), (2 + 2j), (2 + 3j)), measurement_qubit=(1.5 + 2.5j), bases="Z", - extra_coords=(5,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((1 + 3j), (2 + 3j), None, None), measurement_qubit=(1.5 + 3.5j), bases="X", - extra_coords=(2,), + flags={'color=b', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((2 + 1j), (2 + 2j), None, None), measurement_qubit=(2.5 + 1.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), ) @@ -105,7 +105,7 @@ def test_make_toric_surface_code_layout(): height=6, ) assert len(code.observables_x) == len(code.observables_z) == 2 - code.check_commutation_relationships() + code.verify() assert ( len(code.make_code_capacity_circuit(noise=1e-3).shortest_graphlike_error()) == 4 ) @@ -114,144 +114,144 @@ def test_make_toric_surface_code_layout(): ordered_data_qubits=(0j, 1j, (1 + 0j), (1 + 1j)), measurement_qubit=(0.5 + 0.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=(1j, (1 + 1j), 2j, (1 + 2j)), measurement_qubit=(0.5 + 1.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(2j, 3j, (1 + 2j), (1 + 3j)), measurement_qubit=(0.5 + 2.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=(3j, (1 + 3j), 4j, (1 + 4j)), measurement_qubit=(0.5 + 3.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=(4j, 5j, (1 + 4j), (1 + 5j)), measurement_qubit=(0.5 + 4.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=(5j, (1 + 5j), 0j, (1 + 0j)), measurement_qubit=(0.5 + 5.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((1 + 0j), (2 + 0j), (1 + 1j), (2 + 1j)), measurement_qubit=(1.5 + 0.5j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'} ), gen.Tile( ordered_data_qubits=((1 + 1j), (1 + 2j), (2 + 1j), (2 + 2j)), measurement_qubit=(1.5 + 1.5j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'} ), gen.Tile( ordered_data_qubits=((1 + 2j), (2 + 2j), (1 + 3j), (2 + 3j)), measurement_qubit=(1.5 + 2.5j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'} ), gen.Tile( ordered_data_qubits=((1 + 3j), (1 + 4j), (2 + 3j), (2 + 4j)), measurement_qubit=(1.5 + 3.5j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'} ), gen.Tile( ordered_data_qubits=((1 + 4j), (2 + 4j), (1 + 5j), (2 + 5j)), measurement_qubit=(1.5 + 4.5j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'} ), gen.Tile( ordered_data_qubits=((1 + 5j), (1 + 0j), (2 + 5j), (2 + 0j)), measurement_qubit=(1.5 + 5.5j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'} ), gen.Tile( ordered_data_qubits=((2 + 0j), (2 + 1j), (3 + 0j), (3 + 1j)), measurement_qubit=(2.5 + 0.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((2 + 1j), (3 + 1j), (2 + 2j), (3 + 2j)), measurement_qubit=(2.5 + 1.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((2 + 2j), (2 + 3j), (3 + 2j), (3 + 3j)), measurement_qubit=(2.5 + 2.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((2 + 3j), (3 + 3j), (2 + 4j), (3 + 4j)), measurement_qubit=(2.5 + 3.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((2 + 4j), (2 + 5j), (3 + 4j), (3 + 5j)), measurement_qubit=(2.5 + 4.5j), bases="X", - extra_coords=(0,), + flags={'color=r', 'basis=X'}, ), gen.Tile( ordered_data_qubits=((2 + 5j), (3 + 5j), (2 + 0j), (3 + 0j)), measurement_qubit=(2.5 + 5.5j), bases="Z", - extra_coords=(3,), + flags={'color=r', 'basis=Z'}, ), gen.Tile( ordered_data_qubits=((3 + 0j), 0j, (3 + 1j), 1j), measurement_qubit=(3.5 + 0.5j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'} ), gen.Tile( ordered_data_qubits=((3 + 1j), (3 + 2j), 1j, 2j), measurement_qubit=(3.5 + 1.5j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'} ), gen.Tile( ordered_data_qubits=((3 + 2j), 2j, (3 + 3j), 3j), measurement_qubit=(3.5 + 2.5j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'} ), gen.Tile( ordered_data_qubits=((3 + 3j), (3 + 4j), 3j, 4j), measurement_qubit=(3.5 + 3.5j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'} ), gen.Tile( ordered_data_qubits=((3 + 4j), 4j, (3 + 5j), 5j), measurement_qubit=(3.5 + 4.5j), bases="Z", - extra_coords=(4,), + flags={'color=g', 'basis=Z'} ), gen.Tile( ordered_data_qubits=((3 + 5j), (3 + 0j), 5j, 0j), measurement_qubit=(3.5 + 5.5j), bases="X", - extra_coords=(1,), + flags={'color=g', 'basis=X'} ), ) diff --git a/src/clorco/surface_code/_surface_code_patches.py b/src/clorco/surface_code/_surface_code_patches.py index 5f990ce..45f7055 100644 --- a/src/clorco/surface_code/_surface_code_patches.py +++ b/src/clorco/surface_code/_surface_code_patches.py @@ -62,6 +62,12 @@ def surface_code_patch( return gen.Patch(tiles) +def checkerboard_basis(q: complex) -> str: + """Classifies a coordinate as X type or Z type according to a checkerboard.""" + is_x = int(q.real + q.imag) & 1 == 0 + return "X" if is_x else "Z" + + def rectangular_surface_code_patch( *, width: int, @@ -85,7 +91,7 @@ def is_boundary(m: complex, *, b: str) -> bool: return surface_code_patch( possible_data_qubits=[x + 1j * y for x in range(width) for y in range(height)], - basis=gen.checkerboard_basis, + basis=checkerboard_basis, is_boundary_x=functools.partial(is_boundary, b="X"), is_boundary_z=functools.partial(is_boundary, b="Z"), order_func=order_func, @@ -139,7 +145,7 @@ def order_func(m: complex) -> list[complex]: def make_xtop_qubit_patch(*, diameter: int) -> gen.Patch: def order_func(m: complex) -> list[complex]: - if gen.checkerboard_basis(m) == "X": + if checkerboard_basis(m) == "X": return ORDER_S else: return ORDER_N diff --git a/src/clorco/surface_code/_transversal_cnot.py b/src/clorco/surface_code/_transversal_cnot.py index 786387b..2f2aa81 100644 --- a/src/clorco/surface_code/_transversal_cnot.py +++ b/src/clorco/surface_code/_transversal_cnot.py @@ -42,10 +42,10 @@ def make_transversal_cnot_surface_code_circuit( # Initial round. head = gen.Builder.for_qubits(combo.used_set | ancillas) - builder = head.fork() - tail = head.fork() + + builder = gen.Builder(q2i=head.q2i, circuit=stim.Circuit(), tracker=head.tracker) + tail = gen.Builder(q2i=head.q2i, circuit=stim.Circuit(), tracker=head.tracker) if basis == "MagicEPR": - head.gate("RY", combo.used_set) head.measure_pauli_string(xl * gen.PauliString({-1: "X"}), key="OBS_INIT1") head.measure_pauli_string(zl * gen.PauliString({-1: "Z"}), key="OBS_INIT2") head.measure_pauli_string(xr * gen.PauliString({-2: "X"}), key="OBS_INIT3") @@ -54,7 +54,8 @@ def make_transversal_cnot_surface_code_circuit( head.obs_include(["OBS_INIT2"], obs_index=1) head.obs_include(["OBS_INIT3"], obs_index=2) head.obs_include(["OBS_INIT4"], obs_index=3) - head.measure_patch(combo, sorted_by_basis=True, save_layer="init") + for tile in sorted(combo.tiles, key=lambda e: e.basis): + head.measure_pauli_string(tile.to_data_pauli_string(), key=(tile.measurement_qubit, "init")) pad_rounds += 1 else: build_surface_code_round_circuit( @@ -67,7 +68,7 @@ def make_transversal_cnot_surface_code_circuit( m = tile.measurement_qubit if tile.basis == basis: builder.detector( - [gen.AtLayer(m, "init")], + [(m, "init")], pos=m, extra_coords=[0 + 3 * (tile.basis == "Z")], ) @@ -75,7 +76,7 @@ def make_transversal_cnot_surface_code_circuit( builder.tick() # Padding rounds until transition. - loop = builder.fork() + loop = gen.Builder(q2i=builder.q2i, circuit=stim.Circuit(), tracker=builder.tracker) build_surface_code_round_circuit( patch=combo, save_layer="before", @@ -84,7 +85,7 @@ def make_transversal_cnot_surface_code_circuit( for tile in combo.tiles: m = tile.measurement_qubit loop.detector( - [gen.AtLayer(m, "init"), gen.AtLayer(m, "before")], + [(m, "init"), (m, "before")], pos=m, extra_coords=[0 + 3 * (tile.basis == "Z")], ) @@ -93,7 +94,7 @@ def make_transversal_cnot_surface_code_circuit( builder.circuit += loop.circuit * (pad_rounds - 1) # Transition round. - builder.gate2("CX", [(q, q + offset) for q in left.patch.data_set]) + builder.append("CX", [(q, q + offset) for q in left.patch.data_set]) builder.tick() build_surface_code_round_circuit( patch=combo, @@ -109,7 +110,7 @@ def make_transversal_cnot_surface_code_circuit( else: tile_offsets = [(0, "before"), (0, "transition")] builder.detector( - [gen.AtLayer(m + d, layer) for d, layer in tile_offsets], + [(m + d, layer) for d, layer in tile_offsets], pos=m, extra_coords=[1 + (m in left.patch.measure_set) + 3 * (tile.basis == "Z")], ) @@ -117,7 +118,7 @@ def make_transversal_cnot_surface_code_circuit( builder.tick() # Padding rounds until measurement round. - loop = builder.fork() + loop = gen.Builder(q2i=builder.q2i, circuit=stim.Circuit(), tracker=builder.tracker) build_surface_code_round_circuit( patch=combo, save_layer="after", @@ -126,7 +127,7 @@ def make_transversal_cnot_surface_code_circuit( for tile in combo.tiles: m = tile.measurement_qubit loop.detector( - [gen.AtLayer(m, "transition"), gen.AtLayer(m, "after")], + [(m, "transition"), (m, "after")], pos=m, extra_coords=[0 + 3 * (tile.basis == "Z")], ) @@ -136,11 +137,12 @@ def make_transversal_cnot_surface_code_circuit( # Final measurement round. if basis == "MagicEPR": - tail.measure_patch(combo, sorted_by_basis=True, save_layer="end") + for tile in sorted(combo.tiles, key=lambda e: e.basis): + tail.measure_pauli_string(tile.to_data_pauli_string(), key=(tile.measurement_qubit, "end")) for tile in combo.tiles: m = tile.measurement_qubit tail.detector( - [gen.AtLayer(m, "after"), gen.AtLayer(m, "end")], + [(m, "after"), (m, "end")], pos=m, extra_coords=[0 + 3 * (tile.basis == "Z")], ) @@ -162,7 +164,7 @@ def make_transversal_cnot_surface_code_circuit( for tile in combo.tiles: m = tile.measurement_qubit builder.detector( - [gen.AtLayer(m, "after"), gen.AtLayer(m, "end")], + [(m, "after"), (m, "end")], pos=m, extra_coords=[0 + 3 * (tile.basis == "Z")], ) @@ -171,16 +173,16 @@ def make_transversal_cnot_surface_code_circuit( m = tile.measurement_qubit if tile.basis == basis: builder.detector( - [gen.AtLayer(q, "end") for q in tile.used_set], + [(q, "end") for q in tile.used_set], pos=m, extra_coords=[0 + 3 * (tile.basis == "Z")], ) if basis == "X": - builder.obs_include([gen.AtLayer(q, "end") for q in xl.qubits], obs_index=0) - builder.obs_include([gen.AtLayer(q, "end") for q in xr.qubits], obs_index=1) + builder.obs_include([(q, "end") for q in xl.qubits], obs_index=0) + builder.obs_include([(q, "end") for q in xr.qubits], obs_index=1) elif basis == "Z": - builder.obs_include([gen.AtLayer(q, "end") for q in zl.qubits], obs_index=0) - builder.obs_include([gen.AtLayer(q, "end") for q in zr.qubits], obs_index=1) + builder.obs_include([(q, "end") for q in zl.qubits], obs_index=0) + builder.obs_include([(q, "end") for q in zr.qubits], obs_index=1) else: raise NotImplementedError(f"{basis=}") diff --git a/src/clorco/surface_code/_transversal_cnot_test.py b/src/clorco/surface_code/_transversal_cnot_test.py index 731468f..1841059 100644 --- a/src/clorco/surface_code/_transversal_cnot_test.py +++ b/src/clorco/surface_code/_transversal_cnot_test.py @@ -49,10 +49,6 @@ def test_make_transversal_cnot_surface_code_circuit(d: int, b: Any): if b == "MagicEPR": assert gen.count_measurement_layers(c) == 6 assert c.num_observables == 4 - assert ( - c.count_determined_measurements() - == c.num_detectors + c.num_observables + 6 * (d % 2 == 0) - ) else: assert gen.count_measurement_layers(c) == 4 assert c.num_observables == 2 @@ -104,7 +100,6 @@ def test_make_transversal_cnot_surface_code_circuit_exact(): QUBIT_COORDS(6.5, 0.5) 33 QUBIT_COORDS(6.5, 1.5) 34 QUBIT_COORDS(7.5, 1.5) 35 - RY 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 MPP X1*X2*X3*X4 Z1*Z2*Z5*Z8 X0*X11*X12*X13 Z0*Z11*Z14*Z17 OBSERVABLE_INCLUDE(0) rec[-4] OBSERVABLE_INCLUDE(1) rec[-3] diff --git a/src/clorco/surface_code/_xz_surface_code_memory_circuits_test.py b/src/clorco/surface_code/_xz_surface_code_memory_circuits_test.py index 3e4aab5..859a4c8 100644 --- a/src/clorco/surface_code/_xz_surface_code_memory_circuits_test.py +++ b/src/clorco/surface_code/_xz_surface_code_memory_circuits_test.py @@ -214,12 +214,12 @@ def test_exact_circuit_x(): DETECTOR(1.5, 1.5, 0, 3) rec[-19] rec[-2] DETECTOR(1.5, 2.5, 0, 0) rec[-22] rec[-14] DETECTOR(2.5, 0.5, 0, 4) rec[-18] rec[-1] - DETECTOR(0.5, -0.5, 0, 1) rec[-17] rec[-13] rec[-10] - DETECTOR(0.5, 1.5, 0, 1) rec[-16] rec[-12] rec[-11] rec[-9] rec[-8] - DETECTOR(1.5, 0.5, 0, 0) rec[-15] rec[-10] rec[-9] rec[-7] rec[-6] - DETECTOR(1.5, 2.5, 0, 0) rec[-14] rec[-8] rec[-5] + DETECTOR(0.5, -0.5, 1, 1) rec[-17] rec[-13] rec[-10] + DETECTOR(0.5, 1.5, 1, 1) rec[-16] rec[-12] rec[-11] rec[-9] rec[-8] + DETECTOR(1.5, 0.5, 1, 0) rec[-15] rec[-10] rec[-9] rec[-7] rec[-6] + DETECTOR(1.5, 2.5, 1, 0) rec[-14] rec[-8] rec[-5] OBSERVABLE_INCLUDE(0) rec[-13] rec[-12] rec[-11] - SHIFT_COORDS(0, 0, 1) + SHIFT_COORDS(0, 0, 2) DEPOLARIZE1(0.001) 10 12 13 15 0 1 2 3 4 5 6 7 8 9 11 14 16 """ ) diff --git a/src/gen/__init__.py b/src/gen/__init__.py index 153c7db..60376e8 100644 --- a/src/gen/__init__.py +++ b/src/gen/__init__.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from gen._circuit_util import ( gates_used_by_circuit, gate_counts_for_circuit, @@ -31,44 +17,27 @@ sorted_complex, StabilizerCode, Tile, + KeyedPauliString, ) from gen._flows import ( Chunk, ChunkLoop, + ChunkReflow, Flow, compile_chunks_into_circuit, magic_measure_for_flows, - FlowStabilizerVerifier, + ChunkInterface, ) from gen._layers import ( transpile_to_z_basis_interaction_circuit, -) -from gen._surf import ( - ClosedCurve, - CssObservableBoundaryPair, - StepSequenceOutline, - int_points_on_line, - int_points_inside_polygon, - checkerboard_basis, - Order_Z, - Order_ᴎ, - Order_N, - Order_S, - PatchOutline, - layer_begin, - layer_loop, - layer_transition, - layer_end, - layer_single_shot, - surface_code_patch, - PathOutline, - build_patch_to_patch_surface_code_transition_rounds, - PatchTransitionOutline, - StepOutline, + LayerCircuit, + ResetLayer, + InteractLayer, ) from gen._util import ( - stim_circuit_with_transformed_coords, estimate_qubit_count_during_postselection, + stim_circuit_with_transformed_coords, + xor_sorted, write_file, ) from gen._viz_circuit_html import ( @@ -76,6 +45,12 @@ ) from gen._viz_patch_svg import ( patch_svg_viewer, - is_colinear, + is_collinear, svg_path_directions_for_tile, ) +from gen._viz_gltf_3d import ( + ColoredLineData, + ColoredTriangleData, + gltf_model_from_colored_triangle_data, + viz_3d_gltf_model_html, +) diff --git a/src/gen/_circuit_util.py b/src/gen/_circuit_util.py index c9b0370..ed05f4f 100644 --- a/src/gen/_circuit_util.py +++ b/src/gen/_circuit_util.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import collections from typing import Counter diff --git a/src/gen/_circuit_util_test.py b/src/gen/_circuit_util_test.py index e0d11b5..7056dbf 100644 --- a/src/gen/_circuit_util_test.py +++ b/src/gen/_circuit_util_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim import gen diff --git a/src/gen/_core/__init__.py b/src/gen/_core/__init__.py index 61c1c17..82b95ad 100644 --- a/src/gen/_core/__init__.py +++ b/src/gen/_core/__init__.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from gen._core._builder import ( Builder, ) @@ -41,3 +27,6 @@ from gen._core._pauli_string import ( PauliString, ) +from gen._core._keyed_pauli_string import ( + KeyedPauliString, +) diff --git a/src/gen/_core/_builder.py b/src/gen/_core/_builder.py index f2f66d8..9e32e9f 100644 --- a/src/gen/_core/_builder.py +++ b/src/gen/_core/_builder.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from typing import Iterable, Callable, Any, TYPE_CHECKING from typing import Sequence @@ -21,9 +7,6 @@ from gen._core._pauli_string import PauliString from gen._core._util import complex_key, sorted_complex -if TYPE_CHECKING: - import gen - SYMMETRIC_GATES = { "CZ", @@ -39,6 +22,7 @@ "SQRT_XX_DAG", "SQRT_YY_DAG", "SQRT_ZZ_DAG", + "DEPOLARIZE2", } @@ -60,16 +44,6 @@ def __init__( self.circuit = circuit self.tracker = tracker - def copy(self) -> "Builder": - """Returns a Builder with independent copies of this builder's circuit and tracking data.""" - return Builder( - q2i=dict(self.q2i), circuit=self.circuit.copy(), tracker=self.tracker.copy() - ) - - def fork(self) -> "Builder": - """Returns a Builder with the same underlying tracking but which appends into a different circuit.""" - return Builder(q2i=self.q2i, circuit=stim.Circuit(), tracker=self.tracker) - @staticmethod def for_qubits( qubits: Iterable[complex], @@ -87,149 +61,59 @@ def for_qubits( tracker=MeasurementTracker(), ) - def gate(self, name: str, qubits: Iterable[complex], arg: Any = None) -> None: - assert name not in [ - "CZ", - "ZCZ", - "XCX", - "YCY", - "ISWAP", - "ISWAP_DAG", - "SWAP", - "M", - "MX", - "MY", - ] - qubits = sorted_complex(qubits) - if not qubits: - return - self.circuit.append(name, [self.q2i[q] for q in qubits], arg) + def lookup_rec(self, key: Any) -> list[int]: + return self.tracker.lookup_recs([key]) - def gate2(self, name: str, pairs: Iterable[tuple[complex, complex]]) -> None: - pairs = sorted( - pairs, key=lambda pair: (complex_key(pair[0]), complex_key(pair[1])) - ) - if name == "XCZ": - pairs = [pair[::-1] for pair in pairs] - name = "CX" - if name == "YCZ": - pairs = [pair[::-1] for pair in pairs] - name = "CY" - if name == "SWAPCX": - pairs = [pair[::-1] for pair in pairs] - name = "CXSWAP" - if name in SYMMETRIC_GATES: - pairs = [sorted_complex(pair) for pair in pairs] - if not pairs: - return - self.circuit.append(name, [self.q2i[q] for pair in pairs for q in pair]) + def lookup_recs(self, keys: Iterable[Any]) -> list[int]: + return self.tracker.lookup_recs(keys) - def shift_coords(self, *, dp: complex = 0, dt: int): - self.circuit.append("SHIFT_COORDS", [], [dp.real, dp.imag, dt]) + def append(self, + gate: str, + targets: Iterable[complex | Sequence[complex]], + *, + arg: Any = None, + measure_key_func: Callable[[complex | tuple[complex, complex]], Any] = lambda e: e) -> None: + if not targets: + return - def measure_stabilizer_code( - self, - code: "gen.StabilizerCode", - *, - save_layer: Any, - det_cmp_layer: Any | None = None, - noise: float | None = None, - sorted_by_basis: bool = False, - observables_first: bool = False, - ancilla_qubits_for_xz_observable_pairs: Sequence[complex], - ) -> None: - m_obs = lambda: self.measure_observables_and_include( - observables=code.entangled_observables( - ancilla_qubits_for_xz_observable_pairs - )[0], - save_layer=save_layer, - noise=noise, - ) - m_det = lambda: self.measure_patch( - patch=code.patch, - save_layer=save_layer, - cmp_layer=det_cmp_layer, - noise=noise, - sorted_by_basis=sorted_by_basis, - ) - if observables_first: - m_obs() - m_det() + data = stim.gate_data(gate) + if data.is_two_qubit_gate: + for target in targets: + if not hasattr(target, '__len__') or len(target) != 2 or target[0] not in self.q2i or target[1] not in self.q2i: + raise ValueError(f"{gate=} is a two-qubit gate, but {target=} isn't two complex numbers in q2i.") + + # Canonicalize gate and target pairs. + targets = [tuple(pair) for pair in targets] + targets = sorted(targets, key=lambda pair: (self.q2i[pair[0]], self.q2i[pair[1]])) + if gate in SYMMETRIC_GATES: + targets = [sorted(pair, key=self.q2i.__getitem__) for pair in targets] + elif gate == "XCZ": + targets = [pair[::-1] for pair in targets] + gate = "CX" + elif gate == "YCZ": + targets = [pair[::-1] for pair in targets] + gate = "CY" + elif gate == "SWAPCX": + targets = [pair[::-1] for pair in targets] + gate = "CXSWAP" + + self.circuit.append(gate, [self.q2i[q] for pair in targets for q in pair], arg) + elif data.is_single_qubit_gate: + for target in targets: + if target not in self.q2i: + raise ValueError(f"{gate=} is a single-qubit gate, but {target=} isn't in indexed.") + targets = sorted(targets, key=self.q2i.__getitem__) + + self.circuit.append(gate, [self.q2i[q] for q in targets], arg) else: - m_det() - m_obs() + raise NotImplementedError(f'{gate=}') - def measure_observables_and_include( - self, - observables: Iterable["gen.PauliString | None"], - *, - save_layer: Any | None = None, - noise: float | None = None, - ) -> None: - for obs_index, obs in enumerate(observables): - if obs is None: - continue - key = ( - None if save_layer is None else AtLayer(f"obs_{obs_index}", save_layer) - ) - self.measure_pauli_string( - obs, - key=key, - noise=noise, - ) - self.circuit.append("OBSERVABLE_INCLUDE", stim.target_rec(-1), [obs_index]) - - def measure_patch( - self, - patch: "gen.Patch", - *, - save_layer: Any, - cmp_layer: Any | None = None, - noise: float | None = None, - sorted_by_basis: bool = False, - ) -> None: - """Directly measures the stabilizers in a patch using MPP. + if data.produces_measurements: + for target in targets: + self.tracker.record_measurement(key=measure_key_func(target)) - Args: - patch: The patch to get stabilizers (tiles) from. - save_layer: The layer used when saving results to the tracker. The - measurement for a tile is saved under the key - `gen.AtLayer(tile.measurement_qubit, save_layer)`. - cmp_layer: If set to None, does nothing. If set to something else, - adds detectors comparing the new layer's measurements to this - layer's measurements. - noise: The probability of measurement results being wrong. If set to - None, does nothing. If set to a float, adds it as an argument - to the MPP instruction. - sorted_by_basis: Sorts the tiles by basis when deciding what order - to perform measurements in. This can be important when making - sure measurement offsets line up when entering and exiting - loops. Extremely hacky. - """ - if sorted_by_basis: - from gen._core._patch import Patch - - patch = Patch(sorted(patch.tiles, key=lambda t: t.basis), do_not_sort=True) - for tile in patch.tiles: - self.measure_pauli_string( - PauliString( - { - tile.ordered_data_qubits[k]: tile.bases[k] - for k in range(len(tile.ordered_data_qubits)) - if tile.ordered_data_qubits[k] is not None - } - ), - key=AtLayer(tile.measurement_qubit, save_layer), - noise=noise, - ) - if cmp_layer is not None: - for tile in patch.tiles: - m = tile.measurement_qubit - self.detector( - [AtLayer(m, save_layer), AtLayer(m, cmp_layer)], - pos=m, - extra_coords=tile.extra_coords, - ) + def shift_coords(self, *, dp: complex = 0, dt: int): + self.circuit.append("SHIFT_COORDS", [], [dp.real, dp.imag, dt]) def demolition_measure_with_feedback_passthrough( self, @@ -237,8 +121,7 @@ def demolition_measure_with_feedback_passthrough( ys: Iterable[complex] = (), zs: Iterable[complex] = (), *, - tracker_key: Callable[[complex], Any] = lambda e: e, - save_layer: Any, + measure_key_func: Callable[[complex], Any] = lambda e: e, ) -> None: """Performs demolition measurements that look like measurements w.r.t. detectors. @@ -248,45 +131,24 @@ def demolition_measure_with_feedback_passthrough( programmatically create the detectors using the passthrough measurements, and then they can be automatically converted. """ - self.measure( - qubits=xs, basis="X", tracker_key=tracker_key, save_layer=save_layer - ) - self.measure( - qubits=ys, basis="Y", tracker_key=tracker_key, save_layer=save_layer - ) - self.measure( - qubits=zs, basis="Z", tracker_key=tracker_key, save_layer=save_layer - ) + self.append("MX", xs, measure_key_func=measure_key_func) + self.append("MY", ys, measure_key_func=measure_key_func) + self.append("MZ", zs, measure_key_func=measure_key_func) self.tick() - self.gate("RX", xs) - self.gate("RY", ys) - self.gate("RZ", zs) + self.append("RX", xs) + self.append("RY", ys) + self.append("RZ", zs) for qs, b in [(xs, "Z"), (ys, "X"), (zs, "X")]: for q in qs: self.classical_paulis( - control_keys=[AtLayer(tracker_key(q), save_layer)], + control_keys=[measure_key_func(q)], targets=[q], basis=b, ) - def measure( - self, - qubits: Iterable[complex], - *, - basis: str = "Z", - tracker_key: Callable[[complex], Any] = lambda e: e, - save_layer: Any, - ) -> None: - qubits = sorted_complex(qubits) - if not qubits: - return - self.circuit.append(f"M{basis}", [self.q2i[q] for q in qubits]) - for q in qubits: - self.tracker.record_measurement(AtLayer(tracker_key(q), save_layer)) - def measure_pauli_string( self, - observable: "gen.PauliString", + observable: PauliString, *, noise: float | None = None, key: Any | None, @@ -313,6 +175,8 @@ def measure_pauli_string( targets.append(stim.target_combiner()) if targets: + if noise == 0: + noise = None targets.pop() self.circuit.append("MPP", targets, noise) if key is not None: @@ -344,7 +208,8 @@ def detector( if ignore_non_existent: keys = [k for k in keys if k in self.tracker.recorded] targets = self.tracker.current_measurement_record_targets_for(keys) - self.circuit.append("DETECTOR", targets, coords) + if targets is not None: + self.circuit.append("DETECTOR", targets, coords) def obs_include(self, keys: Iterable[Any], *, obs_index: int) -> None: ms = self.tracker.current_measurement_record_targets_for(keys) diff --git a/src/gen/_core/_builder_test.py b/src/gen/_core/_builder_test.py index fc75d0c..f3902c8 100644 --- a/src/gen/_core/_builder_test.py +++ b/src/gen/_core/_builder_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim import gen @@ -26,65 +12,3 @@ def test_builder_init(): QUBIT_COORDS(3, 2) 2 """ ) - - -def test_measure_code(): - perfect_code = gen.StabilizerCode( - patch=gen.Patch( - [ - gen.Tile( - bases="XZZX", - measurement_qubit=k + 1j, - ordered_data_qubits=[(k + j) % 5 for j in range(4)], - ) - for k in range(4) - ] - ), - observables_x=[gen.PauliString.from_xyzs(xs=range(5))], - observables_z=[gen.PauliString.from_xyzs(zs=range(5))], - ) - - builder = gen.Builder.for_qubits([1j, 0, 1, 2, 3, 4]) - builder.measure_stabilizer_code( - perfect_code, - save_layer="init", - observables_first=True, - ancilla_qubits_for_xz_observable_pairs=[1j], - ) - assert builder.circuit == stim.Circuit( - """ - QUBIT_COORDS(0, 0) 0 - QUBIT_COORDS(0, 1) 1 - QUBIT_COORDS(1, 0) 2 - QUBIT_COORDS(2, 0) 3 - QUBIT_COORDS(3, 0) 4 - QUBIT_COORDS(4, 0) 5 - MPP X0*X1*X2*X3*X4*X5 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP Z0*Z1*Z2*Z3*Z4*Z5 - OBSERVABLE_INCLUDE(1) rec[-1] - MPP X0*Z2*Z3*X4 X2*Z3*Z4*X5 X0*X3*Z4*Z5 Z0*X2*X4*Z5 - """ - ) - - builder.circuit.clear() - builder.measure_stabilizer_code( - perfect_code, - save_layer="end", - det_cmp_layer="init", - observables_first=False, - ancilla_qubits_for_xz_observable_pairs=[1j], - ) - assert builder.circuit == stim.Circuit( - """ - MPP X0*Z2*Z3*X4 X2*Z3*Z4*X5 X0*X3*Z4*Z5 Z0*X2*X4*Z5 - DETECTOR(0, 1, 0) rec[-8] rec[-4] - DETECTOR(1, 1, 0) rec[-7] rec[-3] - DETECTOR(2, 1, 0) rec[-6] rec[-2] - DETECTOR(3, 1, 0) rec[-5] rec[-1] - MPP X0*X1*X2*X3*X4*X5 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP Z0*Z1*Z2*Z3*Z4*Z5 - OBSERVABLE_INCLUDE(1) rec[-1] - """ - ) diff --git a/src/gen/_core/_keyed_pauli_string.py b/src/gen/_core/_keyed_pauli_string.py new file mode 100644 index 0000000..730cf99 --- /dev/null +++ b/src/gen/_core/_keyed_pauli_string.py @@ -0,0 +1,36 @@ +import dataclasses +from typing import Literal, Callable + +from gen._core._pauli_string import PauliString + + +@dataclasses.dataclass(frozen=True) +class KeyedPauliString: + key: int + pauli_string: PauliString + + @property + def qubits(self) -> dict[complex, Literal['X', 'Y', 'Z']]: + return self.pauli_string.qubits + + def __lt__(self, other) -> bool: + if isinstance(other, PauliString): + return True + if isinstance(other, KeyedPauliString): + return (self.key, self.pauli_string) < (other.key, other.pauli_string) + return NotImplemented + + def __gt__(self, other) -> bool: + if isinstance(other, PauliString): + return False + if isinstance(other, KeyedPauliString): + return (self.key, self.pauli_string) > (other.key, other.pauli_string) + return NotImplemented + + def with_transformed_coords( + self, transform: Callable[[complex], complex] + ) -> "KeyedPauliString": + return KeyedPauliString(key=self.key, pauli_string=self.pauli_string.with_transformed_coords(transform)) + + def __str__(self): + return f'(key={self.key}) {self.pauli_string}' \ No newline at end of file diff --git a/src/gen/_core/_measurement_tracker.py b/src/gen/_core/_measurement_tracker.py index a4ced0c..cd77476 100644 --- a/src/gen/_core/_measurement_tracker.py +++ b/src/gen/_core/_measurement_tracker.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses from typing import Iterable, Any @@ -49,17 +35,20 @@ def record_measurement(self, key: Any) -> None: self.next_measurement_index += 1 def make_measurement_group(self, sub_keys: Iterable[Any], *, key: Any) -> None: - self._rec(key, self.measurement_indices(sub_keys)) + self._rec(key, self.lookup_recs(sub_keys)) def record_obstacle(self, key: Any) -> None: self._rec(key, None) - def measurement_indices(self, keys: Iterable[Any]) -> list[int]: + def lookup_recs(self, keys: Iterable[Any]) -> list[int] | None: result = set() for key in keys: if key not in self.recorded: raise ValueError(f"No such measurement: {key=}") - for v in self.recorded[key]: + r = self.recorded[key] + if r is None: + return None + for v in r: if v is None: raise ValueError(f"Obstacle at {key=}") if v in result: @@ -70,7 +59,9 @@ def measurement_indices(self, keys: Iterable[Any]) -> list[int]: def current_measurement_record_targets_for( self, keys: Iterable[Any] - ) -> list[stim.GateTarget]: + ) -> list[stim.GateTarget] | None: t0 = self.next_measurement_index - times = self.measurement_indices(keys) + times = self.lookup_recs(keys) + if times is None: + return None return [stim.target_rec(t - t0) for t in sorted(times)] diff --git a/src/gen/_core/_noise.py b/src/gen/_core/_noise.py index 336ccc6..e637e7c 100644 --- a/src/gen/_core/_noise.py +++ b/src/gen/_core/_noise.py @@ -1,18 +1,4 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Iterator, AbstractSet, Any +from typing import Iterator, AbstractSet, DefaultDict, Any import collections @@ -25,6 +11,7 @@ "QUBIT_COORDS", "SHIFT_COORDS", "TICK", + "MPAD", } OP_TO_MEASURE_BASES = { "M": "Z", @@ -44,7 +31,8 @@ class NoiseRule: def __init__( self, *, - after: dict[str, float], + before: dict[str, float | tuple[float, ...]] | None = None, + after: dict[str, float | tuple[float, ...]] | None = None, flip_result: float = 0, ): """ @@ -57,14 +45,25 @@ def __init__( flip_result: The probability that a measurement result should be reported incorrectly. Only valid when applied to operations that produce measurement results. """ + if after is None: + after = {} + if before is None: + before = {} if not (0 <= flip_result <= 1): raise ValueError(f"not (0 <= {flip_result=} <= 1)") - for k, p in after.items(): + for k, p in [*after.items(), *before.items()]: gate_data = stim.gate_data(k) if gate_data.produces_measurements or not gate_data.is_noisy_gate: raise ValueError(f"not a pure noise channel: {k} from {after=}") - if not (0 <= p <= 1): - raise ValueError(f"not (0 <= {p} <= 1) from {after=}") + if gate_data.num_parens_arguments_range == range(1, 2): + if not isinstance(p, (int, float)) or not (0 <= p <= 1): + raise ValueError(f"not a probability: {p!r}") + else: + if len(p) not in gate_data.num_parens_arguments_range: + raise ValueError(f"Wrong number of arguments {p!r} for gate {k!r}") + if not isinstance(p, (list, tuple)) or not (0 <= sum(p) <= 1): + raise ValueError(f"not a tuple of disjoint probabilities: {p!r}") + self.before = before self.after = after self.flip_result = flip_result @@ -73,7 +72,8 @@ def append_noisy_version_of( *, split_op: stim.CircuitInstruction, out_during_moment: stim.Circuit, - after_moments: collections.defaultdict[Any, stim.Circuit], + before_moments: DefaultDict[Any, stim.Circuit], + after_moments: DefaultDict[Any, stim.Circuit], immune_qubit_indices: AbstractSet[int], ) -> None: targets = split_op.targets_copy() @@ -96,6 +96,8 @@ def append_noisy_version_of( out_during_moment.append(split_op.name, targets, args) raw_targets = [t.value for t in targets if not t.is_combiner] + for op_name, arg in self.before.items(): + before_moments[(op_name, arg)].append(op_name, raw_targets, arg) for op_name, arg in self.after.items(): after_moments[(op_name, arg)].append(op_name, raw_targets, arg) @@ -103,7 +105,7 @@ def append_noisy_version_of( class NoiseModel: def __init__( self, - idle_noise: float | NoiseRule | None = None, + idle_depolarization: float = 0, tick_noise: NoiseRule | None = None, additional_depolarization_waiting_for_m_or_r: float = 0, gate_rules: dict[str, NoiseRule] | None = None, @@ -113,12 +115,7 @@ def __init__( any_clifford_2q_rule: NoiseRule | None = None, allow_multiple_uses_of_a_qubit_in_one_tick: bool = False, ): - if isinstance(idle_noise, float): - if idle_noise == 0: - idle_noise = None - else: - idle_noise = NoiseRule(after={'DEPOLARIZE1': idle_noise}) - self.idle_noise: NoiseRule | None = idle_noise + self.idle_depolarization = idle_depolarization self.tick_noise = tick_noise self.additional_depolarization_waiting_for_m_or_r = ( additional_depolarization_waiting_for_m_or_r @@ -144,7 +141,7 @@ def si1000(p: float) -> "NoiseModel": the measurement. """ return NoiseModel( - idle_noise=p / 10, + idle_depolarization=p / 10, additional_depolarization_waiting_for_m_or_r=2 * p, any_clifford_1q_rule=NoiseRule(after={"DEPOLARIZE1": p / 10}), any_clifford_2q_rule=NoiseRule(after={"DEPOLARIZE2": p}), @@ -170,7 +167,7 @@ def uniform_depolarizing(p: float) -> "NoiseModel": the input qubit. The input qubit is depolarized. """ return NoiseModel( - idle_noise=p, + idle_depolarization=p, any_clifford_1q_rule=NoiseRule(after={"DEPOLARIZE1": p}), any_clifford_2q_rule=NoiseRule(after={"DEPOLARIZE2": p}), measure_rules={ @@ -233,11 +230,12 @@ def _noise_rule_for_split_operation( split_op=stim.CircuitInstruction(m_name, split_op.targets_copy()) ) return NoiseRule( + before=r_noise.before if r_noise is not None else {}, after=r_noise.after if r_noise is not None else {}, flip_result=m_noise.flip_result if m_noise is not None else 0, ) - raise ValueError(f"No noise (or lack of noise) specified for {split_op=}.") + raise ValueError(f"No noise (or lack of noise) specified for '{split_op}'.") def _append_idle_error( self, @@ -286,9 +284,8 @@ def _append_idle_error( - clifford_qubits_set - immune_qubit_indices ) - if idle and self.idle_noise is not None: - for k, v in self.idle_noise.after.items(): - out.append(k, idle, v) + if idle and self.idle_depolarization: + out.append("DEPOLARIZE1", idle, self.idle_depolarization) waiting_for_mr = sorted( system_qubit_indices - collapse_qubits_set - immune_qubit_indices @@ -303,6 +300,8 @@ def _append_idle_error( ) if self.tick_noise is not None: + for k, p in self.tick_noise.before.items(): + out.append(k, system_qubit_indices - immune_qubit_indices, p) for k, p in self.tick_noise.after.items(): out.append(k, system_qubit_indices - immune_qubit_indices, p) @@ -314,18 +313,24 @@ def _append_noisy_moment( system_qubits_indices: AbstractSet[int], immune_qubit_indices: AbstractSet[int], ) -> None: + before = collections.defaultdict(stim.Circuit) after = collections.defaultdict(stim.Circuit) + grow = stim.Circuit() for split_op in moment_split_ops: rule = self._noise_rule_for_split_operation(split_op=split_op) if rule is None: - out.append(split_op) + grow.append(split_op) else: rule.append_noisy_version_of( split_op=split_op, - out_during_moment=out, + out_during_moment=grow, + before_moments=before, after_moments=after, immune_qubit_indices=immune_qubit_indices, ) + for k in sorted(before.keys()): + out += before[k] + out += grow for k in sorted(after.keys()): out += after[k] @@ -336,6 +341,43 @@ def _append_noisy_moment( immune_qubit_indices=immune_qubit_indices, ) + def noisy_circuit_skipping_mpp_boundaries( + self, + circuit: stim.Circuit, + *, + immune_qubit_indices: AbstractSet[int] | None = None, + ) -> stim.Circuit: + """Adds noise to the circuit except for MPP operations at the start/end. + + Divides the circuit into three parts: mpp_start, body, mpp_end. The mpp + sections grow from the ends of the circuit until they hit an instruction + that's not an annotation or an MPP. Then body is the remaining circuit + between the two ends. Noise is added to the body, and then the pieces + are reassembled. + """ + allowed = { + 'TICK', + 'OBSERVABLE_INCLUDE', + 'DETECTOR', + 'MPP', + 'QUBIT_COORDS', + 'SHIFT_COORDS', + } + start = 0 + end = len(circuit) + while start < len(circuit) and circuit[start].name in allowed: + start += 1 + while end > 0 and circuit[end - 1].name in allowed: + end -= 1 + while end < len(circuit) and circuit[end].name != 'MPP': + end += 1 + while end > 0 and circuit[end - 1].name == 'TICK': + end -= 1 + if end <= start: + raise ValueError("end <= start") + noisy = self.noisy_circuit(circuit[start:end], immune_qubit_indices=immune_qubit_indices) + return circuit[:start] + noisy + circuit[end:] + def noisy_circuit( self, circuit: stim.Circuit, @@ -430,13 +472,17 @@ def _split_targets_if_needed( yield from _split_out_immune_targets_assuming_single_qubit_gate( op, immune_qubit_indices ) + elif gate_data.is_two_qubit_gate: + yield from _split_out_immune_targets_assuming_two_qubit_gate( + op, immune_qubit_indices + ) else: raise NotImplementedError(f"{op=}") def _split_out_immune_targets_assuming_single_qubit_gate( - op: stim.CircuitInstruction, - immune_qubit_indices: AbstractSet[int], + op: stim.CircuitInstruction, + immune_qubit_indices: AbstractSet[int], ) -> list[stim.CircuitInstruction]: if immune_qubit_indices: args = op.gate_args_copy() @@ -446,6 +492,21 @@ def _split_out_immune_targets_assuming_single_qubit_gate( yield op +def _split_out_immune_targets_assuming_two_qubit_gate( + op: stim.CircuitInstruction, + immune_qubit_indices: AbstractSet[int], +) -> list[stim.CircuitInstruction]: + if immune_qubit_indices: + args = op.gate_args_copy() + targets = op.targets_copy() + for k in range(len(targets)): + t1 = targets[k] + t2 = targets[k + 1] + yield stim.CircuitInstruction(op.name, [t1, t2], args) + else: + yield op + + def _split_targets_if_needed_clifford_2q( op: stim.CircuitInstruction, immune_qubit_indices: AbstractSet[int], diff --git a/src/gen/_core/_noise_test.py b/src/gen/_core/_noise_test.py index 70cf9c0..3217c44 100644 --- a/src/gen/_core/_noise_test.py +++ b/src/gen/_core/_noise_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim import gen diff --git a/src/gen/_core/_patch.py b/src/gen/_core/_patch.py index 675225a..89a8520 100644 --- a/src/gen/_core/_patch.py +++ b/src/gen/_core/_patch.py @@ -1,21 +1,8 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import functools import pathlib -from typing import Iterable, Callable, Literal +from typing import Iterable, Callable, Literal, Union +import gen from gen._core._util import sorted_complex, min_max_complex from gen._core._tile import Tile from gen._util import write_file @@ -37,6 +24,15 @@ def __init__(self, tiles: Iterable[Tile], *, do_not_sort: bool = False): else: self.tiles = tuple(sorted_complex(tiles, key=lambda e: e.measurement_qubit)) + def with_edits( + self, + *, + tiles: Iterable[Tile] | None = None, + ) -> 'Patch': + return gen.Patch( + tiles=self.tiles if tiles is None else tiles, + ) + def with_transformed_coords( self, coord_transform: Callable[[complex], complex] ) -> "Patch": @@ -100,39 +96,35 @@ def write_svg( self, path: str | pathlib.Path, *, - split_xz: bool = False, - other: Iterable["Patch"] | "Patch" = (), + other: Union['gen.Patch', 'gen.StabilizerCode', Iterable[Union['gen.Patch', 'gen.StabilizerCode']]] = (), show_order: bool | Literal["undirected", "3couplerspecial"] = False, show_measure_qubits: bool = False, show_data_qubits: bool = False, - system_qubits: Iterable[complex] = (), + expected_points: Iterable[complex] = (), + show_coords: bool = True, opacity: float = 1, - wraparound_clip: bool = False, + show_obs: bool = False, + rows: int | None = None, + cols: int | None = None, + tile_color_func: Callable[[Tile], str] | None = None ) -> None: from gen._viz_patch_svg import patch_svg_viewer - patches = [self] + ([other] if isinstance(other, Patch) else list(other)) - if split_xz: - patches = [ - p2 - for p in patches - for p2 in [ - p, - p.with_only_x_tiles(), - p.with_only_y_tiles(), - p.with_only_z_tiles(), - ] - if p2.tiles - ] + from gen._core._stabilizer_code import StabilizerCode + patches = [self] + ([other] if isinstance(other, (Patch, StabilizerCode)) else list(other)) viewer = patch_svg_viewer( patches=patches, show_measure_qubits=show_measure_qubits, show_data_qubits=show_data_qubits, show_order=show_order, - available_qubits=system_qubits, + expected_points=expected_points, opacity=opacity, - wraparound_clip=wraparound_clip + show_coords=show_coords, + show_obs=show_obs, + rows=rows, + cols=cols, + tile_color_func=tile_color_func, ) write_file(path, viewer) diff --git a/src/gen/_core/_patch_test.py b/src/gen/_core/_patch_test.py index 0ed0419..e239df3 100644 --- a/src/gen/_core/_patch_test.py +++ b/src/gen/_core/_patch_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim from gen._core._builder import Builder diff --git a/src/gen/_core/_pauli_string.py b/src/gen/_core/_pauli_string.py index ab25e72..a3727cc 100644 --- a/src/gen/_core/_pauli_string.py +++ b/src/gen/_core/_pauli_string.py @@ -1,18 +1,4 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Callable, Literal, TYPE_CHECKING, cast, Iterable, Dict +from typing import Callable, Literal, TYPE_CHECKING, cast, Iterable, Dict, Any, Union import stim @@ -20,15 +6,78 @@ if TYPE_CHECKING: import gen + from gen._core import KeyedPauliString + + +_multiplication_table: dict[Literal['X', 'Y', 'Z'] | None, dict[Literal['X', 'Y', 'Z'] | None, Literal['X', 'Y', 'Z'] | None]] = { + None: {None: None, "X": "X", "Y": "Y", "Z": "Z"}, + "X": {None: "X", "X": None, "Y": "Z", "Z": "Y"}, + "Y": {None: "Y", "X": "Z", "Y": None, "Z": "X"}, + "Z": {None: "Z", "X": "Y", "Y": "X", "Z": None}, +} class PauliString: """A qubit-to-pauli mapping.""" - def __init__(self, qubits: dict[complex, Literal["X", "Y", "Z"]]): - self.qubits = {q: qubits[q] for q in sorted_complex(qubits.keys())} + def __init__( + self, + mapping: Union[dict[complex, Literal["X", "Y", "Z"]], dict[Literal["X", "Y", "Z"], complex | Iterable[complex]], 'PauliString', 'KeyedPauliString', None] = None, + *, + xs: Iterable[complex] = (), + ys: Iterable[complex] = (), + zs: Iterable[complex] = (), + ): + self.qubits: dict[complex, Literal["X", "Y", "Z"]] = {} + + from gen._core import KeyedPauliString + if isinstance(mapping, (PauliString, KeyedPauliString)) and not xs and not ys and not zs: + self.qubits = dict(mapping.qubits) + self._hash = mapping._hash if isinstance(mapping, PauliString) else mapping.pauli_string._hash + return + + for q in xs: + self._mul_term(q, 'X') + for q in ys: + self._mul_term(q, 'Y') + for q in zs: + self._mul_term(q, 'Z') + if mapping is not None: + if isinstance(mapping, (PauliString, KeyedPauliString)): + mapping = mapping.qubits + for k, v in mapping.items(): + if isinstance(k, str): + assert k == 'X' or k == 'Y' or k == 'Z' + b = cast(Literal['X', 'Y', 'Z'], k) + if isinstance(v, (int, float, complex)): + self._mul_term(v, b) + else: + for q in v: + assert isinstance(q, (int, float, complex)) + self._mul_term(q, b) + elif isinstance(v, str): + assert v == 'X' or v == 'Y' or v == 'Z' + assert isinstance(k, (int, float, complex)) + b = cast(Literal['X', 'Y', 'Z'], v) + self._mul_term(k, b) + + self.qubits = {complex(q): self.qubits[q] for q in sorted_complex(self.qubits.keys())} self._hash: int = hash(tuple(self.qubits.items())) + @property + def pauli_string(self) -> 'PauliString': + """Duck-typing compatibility with KeyedPauliString.""" + return self + + def keyed(self, key: int) -> 'KeyedPauliString': + from gen._core import KeyedPauliString + return KeyedPauliString(key=key, pauli_string=self) + + def _mul_term(self, q: complex, b: Literal["X", "Y", "Z"]): + new_b = _multiplication_table[self.qubits.pop(q, None)][b] + if new_b is not None: + self.qubits[q] = new_b + @staticmethod def from_stim_pauli_string(stim_pauli_string: stim.PauliString) -> "PauliString": return PauliString( @@ -49,45 +98,8 @@ def from_tile_data(tile: "gen.Tile") -> "PauliString": } ) - @staticmethod - def from_xyzs( - *, - xs: Iterable[complex] = (), - ys: Iterable[complex] = (), - zs: Iterable[complex] = (), - ) -> "PauliString": - qs: dict[complex, Literal["X", "Y", "Z"]] = {} - for q in xs: - qs[q] = "X" - for q in ys: - p = qs.get(q) - if p is None: - qs[q] = "Y" - elif p == "X": - qs[q] = "Z" - else: - raise NotImplementedError(f"{p=}") - for q in zs: - p = qs.get(q) - if p is None: - qs[q] = "Z" - elif p == "X": - qs[q] = "Y" - elif p == "Y": - qs[q] = "X" - else: - raise NotImplementedError(f"{p=}") - return PauliString(qs) - - @staticmethod - def from_b2q( - b2q: dict[Literal["X", "Y", "Z"], Iterable[complex]], - ) -> "PauliString": - return PauliString.from_xyzs( - xs=b2q.get("X", ()), - ys=b2q.get("Y", ()), - zs=b2q.get("Z", ()), - ) + def with_basis(self, basis: Literal['X', 'Y', 'Z']) -> 'PauliString': + return PauliString({q: basis for q in self.qubits.keys()}) def __bool__(self) -> bool: return bool(self.qubits) @@ -109,7 +121,8 @@ def __mul__(self, other: "PauliString") -> "PauliString": return PauliString(result) def __repr__(self) -> str: - return f"gen.PauliString(qubits={self.qubits!r})" + s = {q: self.qubits[q] for q in sorted_complex(self.qubits)} + return f"gen.PauliString({s!r})" def __str__(self) -> str: return "*".join( @@ -117,12 +130,18 @@ def __str__(self) -> str: ) def with_xz_flipped(self) -> "PauliString": - return PauliString( - { - q: "Z" if p == "X" else "X" if p == "Z" else p - for q, p in self.qubits.items() - } - ) + remap = {'X': 'Z', 'Y': 'Y', 'Z': 'X'} + return PauliString({ + q: remap[p] + for q, p in self.qubits.items() + }) + + def with_xy_flipped(self) -> "PauliString": + remap = {'X': 'Y', 'Y': 'X', 'Z': 'Z'} + return PauliString({ + q: remap[p] + for q, p in self.qubits.items() + }) def commutes(self, other: "PauliString") -> bool: return not self.anticommutes(other) @@ -156,3 +175,11 @@ def __eq__(self, other) -> bool: if not isinstance(other, PauliString): return NotImplemented return self.qubits == other.qubits + + def _sort_key(self) -> Any: + return tuple((q.real, q.imag, p) for q, p in self.qubits.items()) + + def __lt__(self, other) -> bool: + if not isinstance(other, PauliString): + return NotImplemented + return self._sort_key() < other._sort_key() diff --git a/src/gen/_core/_pauli_string_test.py b/src/gen/_core/_pauli_string_test.py index 199eab2..f393217 100644 --- a/src/gen/_core/_pauli_string_test.py +++ b/src/gen/_core/_pauli_string_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from gen._core._pauli_string import PauliString diff --git a/src/gen/_core/_stabilizer_code.py b/src/gen/_core/_stabilizer_code.py index d2ef34c..a4a4c64 100644 --- a/src/gen/_core/_stabilizer_code.py +++ b/src/gen/_core/_stabilizer_code.py @@ -1,29 +1,26 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +import collections +import functools +import itertools import pathlib -from typing import Iterable, Literal, Any, Callable, Sequence +from typing import Iterable, Literal, Any, Callable, Sequence, Union, \ + TYPE_CHECKING import stim +import gen from gen._core._builder import Builder, AtLayer -from gen._core._noise import NoiseRule, NoiseModel +from gen._core._noise import NoiseRule from gen._core._patch import Patch from gen._core._pauli_string import PauliString from gen._core._util import sorted_complex +from gen._core._keyed_pauli_string import KeyedPauliString from gen._util import write_file +if TYPE_CHECKING: + from gen._flows._chunk import Chunk + from gen._flows._flow import Flow + from gen._flows._chunk_interface import ChunkInterface + class StabilizerCode: """This class stores the stabilizers and observables of a stabilizer code. @@ -38,28 +35,193 @@ class StabilizerCode: The stabilizers are defined by the 'tiles' of the code's 'patch'. Each tile defines data qubits and a measurement qubit. The measurement qubit is also a very loose concept; it may literally represent a single ancilla qubit used - for measuring the stabilizer. Or it may be more like a unique key identifying - the tile, with no relation to any real qubit. + for measuring the stabilizer or it may be more like a unique key identifying + the tile with no relation to any real qubit. """ def __init__( self, *, - patch: Patch, - observables_x: Iterable[PauliString], - observables_z: Iterable[PauliString], + patch: Patch | None = None, + observables_x: Iterable[PauliString] = (), + observables_z: Iterable[PauliString] = (), ): - self.patch = patch - self.observables_x = tuple(observables_x) - self.observables_z = tuple(observables_z) + self.patch = Patch([]) if patch is None else patch + self.observables_x: tuple[PauliString, ...] = tuple(observables_x) + self.observables_z: tuple[PauliString, ...] = tuple(observables_z) + + def x_basis_subset(self) -> 'StabilizerCode': + return StabilizerCode( + patch=self.patch.with_only_x_tiles(), + observables_x=self.observables_x, + ) + + def z_basis_subset(self) -> 'StabilizerCode': + return StabilizerCode( + patch=self.patch.with_only_z_tiles(), + observables_x=self.observables_x if not self.observables_z else (), + observables_z=self.observables_z, + ) + + @property + def tiles(self) -> tuple['gen.Tile', ...]: + return self.patch.tiles + + def find_distance(self, *, max_search_weight: int) -> int: + return len(self.find_logical_error(max_search_weight=max_search_weight)) + + def find_logical_error(self, *, max_search_weight: int) -> list[stim.ExplainedError]: + circuit = self.make_code_capacity_circuit(noise=1e-3) + if max_search_weight == 2: + return circuit.shortest_graphlike_error() + return circuit.search_for_undetectable_logical_errors( + dont_explore_edges_with_degree_above=max_search_weight, + dont_explore_detection_event_sets_with_size_above=max_search_weight, + dont_explore_edges_increasing_symptom_degree=False, + canonicalize_circuit_errors=True, + ) + + def with_observables_from_basis(self, basis: Literal['X', 'Y', 'Z']) -> 'StabilizerCode': + if basis == 'X': + return gen.StabilizerCode( + patch=self.patch, + observables_x=self.observables_x, + observables_z=[], + ) + elif basis == 'Y': + return gen.StabilizerCode( + patch=self.patch, + observables_x=[x * z for x, z in zip(self.observables_x, self.observables_z)], + observables_z=[], + ) + elif basis == 'Z': + return gen.StabilizerCode( + patch=self.patch, + observables_x=[], + observables_z=self.observables_z, + ) + else: + raise NotImplementedError(f'{basis=}') + + def mpp_init_chunk(self) -> 'Chunk': + return self.mpp_chunk(flow_style='init') + + def mpp_end_chunk(self) -> 'Chunk': + return self.mpp_chunk(flow_style='end') + + def mpp_chunk( + self, + *, + noise: float | NoiseRule | None = None, + flow_style: Literal['passthrough', 'end', 'init'] = 'passthrough', + resolve_anticommutations: bool = False, + ) -> 'Chunk': + assert flow_style in ['init', 'end', 'passthrough'] + if resolve_anticommutations: + observables, immune = self.entangled_observables(ancilla_qubits_for_xz_pairs=None) + immune = set(immune) + else: + observables = self.observables_x + self.observables_z + immune = set() + + from gen._flows import Flow, Chunk + builder = Builder.for_qubits(self.data_set | immune) + flows = [] + discards = [] + + if noise is None or noise == 0: + noise = NoiseRule() + elif isinstance(noise, (float, int)): + noise = NoiseRule(before={'DEPOLARIZE1': noise}, flip_result=noise) + + for k, obs in enumerate(observables): + if flow_style != 'passthrough': + builder.measure_pauli_string(obs, key=f'obs{k}') + flows.append(Flow( + center=-1, + start=None if flow_style == 'init' else obs, + end=None if flow_style == 'end' else obs, + measurement_indices=builder.lookup_rec(f'obs{k}') if flow_style != 'passthrough' else (), + obs_index=k, + )) + + for gate, strength in noise.before.items(): + builder.append(gate, self.data_set, arg=strength) + for m, tile in enumerate(self.patch.tiles): + if tile.data_set: + ps = tile.to_data_pauli_string() + builder.measure_pauli_string(ps, key=f'det{m}', noise=noise.flip_result) + if flow_style != 'init': + flows.append(Flow( + center=tile.measurement_qubit, + start=ps, + measurement_indices=builder.lookup_rec(f'det{m}'), + flags=tile.flags, + )) + if flow_style != 'end': + flows.append(Flow( + center=tile.measurement_qubit, + end=ps, + measurement_indices=builder.lookup_rec(f'det{m}'), + flags=tile.flags, + )) + for gate, strength in noise.after.items(): + builder.append(gate, self.data_set, arg=strength) + + return Chunk( + circuit=builder.circuit, + q2i=builder.q2i, + flows=flows, + discarded_inputs=discards, + ) + + def as_interface(self) -> 'ChunkInterface': + from gen._flows._chunk_interface import ChunkInterface + ports = [] + for tile in self.patch.tiles: + if tile.data_set: + ports.append(tile.to_data_pauli_string()) + for k, ps in enumerate(self.observables_x): + ports.append(KeyedPauliString(pauli_string=ps, key=k)) + for k, ps in enumerate(self.observables_z): + ports.append(KeyedPauliString(pauli_string=ps, key=k)) + return ChunkInterface(ports=ports, discards=[]) + + def with_edits( + self, + *, + patch: Patch | None = None, + observables_x: Iterable[PauliString] | None = None, + observables_z: Iterable[PauliString] | None = None, + ) -> 'StabilizerCode': + return StabilizerCode( + patch=self.patch if patch is None else patch, + observables_x=self.observables_x if observables_x is None else observables_x, + observables_z=self.observables_z if observables_z is None else observables_z, + ) + + @functools.cached_property + def data_set(self) -> frozenset[complex]: + result = set(self.patch.data_set) + for obs in self.observables_x, self.observables_z: + for e in obs: + result |= e.qubits.keys() + return frozenset(result) + + @functools.cached_property + def measure_set(self) -> frozenset[complex]: + return self.patch.measure_set + + @functools.cached_property + def used_set(self) -> frozenset[complex]: + result = set(self.patch.used_set) + for obs in self.observables_x, self.observables_z: + for e in obs: + result |= e.qubits.keys() + return frozenset(result) @staticmethod def from_patch_with_inferred_observables(patch: Patch) -> "StabilizerCode": - """Creates a code by finding degrees of freedom leftover from stabilizers. - - If there are M linearly independent stabilizers covering N qubits, then the - returned code will have N-M observables. - """ q2i = {q: i for i, q in enumerate(sorted_complex(patch.data_set))} i2q = {i: q for q, i in q2i.items()} @@ -97,6 +259,20 @@ def from_patch_with_inferred_observables(patch: Patch) -> "StabilizerCode": return StabilizerCode(patch=patch, observables_x=obs_xs, observables_z=obs_zs) + def with_epr_observables(self) -> 'StabilizerCode': + r = min([e.real for e in self.patch.used_set], default=0) + new_obs_x = [] + new_obs_z = [] + for k in range(min(len(self.observables_x), len(self.observables_z))): + if self.observables_x[k].anticommutes(self.observables_z[k]): + new_obs_x.append(self.observables_x[k] * PauliString({r - 0.25 - 0.25j: 'X'})) + new_obs_z.append(self.observables_z[k] * PauliString({r - 0.25 - 0.25j: 'Z'})) + r -= 1 + else: + new_obs_x.append(self.observables_x[k]) + new_obs_z.append(self.observables_z[k]) + return StabilizerCode(patch=self.patch, observables_x=new_obs_x, observables_z=new_obs_z) + def entangled_observables( self, ancilla_qubits_for_xz_pairs: Sequence[complex] | None ) -> tuple[list[PauliString], list[complex]]: @@ -130,8 +306,33 @@ def entangled_observables( observables.append(obs) return observables, list(ancilla_qubits_for_xz_pairs) - def check_commutation_relationships(self) -> None: - """Verifies observables and stabilizers relate as a stabilizer code.""" + def verify(self) -> None: + """Verifies observables and stabilizers relate as a stabilizer code. + + All stabilizers should commute with each other. + All stabilizers should commute with all observables. + Same-index X and Z observables should anti-commute. + All other observable pairs should commute. + """ + + q2tiles = collections.defaultdict(list) + for tile in self.patch.tiles: + for q in tile.data_set: + q2tiles[q].append(tile) + for tile1 in self.patch.tiles: + overlapping = { + tile2 + for q in tile1.data_set + for tile2 in q2tiles[q] + } + for tile2 in overlapping: + t1 = tile1.to_data_pauli_string() + t2 = tile2.to_data_pauli_string() + if not t1.commutes(t2): + raise ValueError( + f"Tile stabilizer {t1=} anticommutes with tile stabilizer {t2=}." + ) + for tile in self.patch.tiles: t = tile.to_data_pauli_string() for obs in self.observables_x + self.observables_z: @@ -158,6 +359,13 @@ def check_commutation_relationships(self) -> None: f"Unpaired observables should commute: {obs1=}, {obs2=}." ) + def with_xz_flipped(self) -> 'StabilizerCode': + return StabilizerCode( + patch=self.patch.with_xz_flipped(), + observables_x=[obs_x.with_xz_flipped() for obs_x in self.observables_x], + observables_z=[obs_z.with_xz_flipped() for obs_z in self.observables_z], + ) + def write_svg( self, path: str | pathlib.Path, @@ -167,33 +375,36 @@ def write_svg( show_data_qubits: bool = True, system_qubits: Iterable[complex] = (), opacity: float = 1, + show_coords: bool = True, + show_obs: bool = True, + other: Union[None, 'StabilizerCode', 'Patch', Iterable[Union['StabilizerCode', 'Patch']]] = None, + tile_color_func: Callable[['gen.Tile'], str] | None = None, + rows: int | None = None, + cols: int | None = None, + find_logical_err_max_weight: int | None = None, ) -> None: - if not system_qubits: - if show_measure_qubits and show_data_qubits: - system_qubits = self.patch.used_set - elif show_measure_qubits: - system_qubits = self.patch.measure_set - elif show_data_qubits: - system_qubits = self.patch.data_set - - obs_patches = [] - for k in range(max(len(self.observables_x), len(self.observables_z))): - obs_tiles = [] - if k < len(self.observables_x): - obs_tiles.append(self.observables_x[k].to_tile()) - if k < len(self.observables_z): - obs_tiles.append(self.observables_z[k].to_tile()) - obs_patches.append(Patch(obs_tiles)) - - self.patch.write_svg( - path=path, - show_order=show_order, - show_data_qubits=show_data_qubits, + flat = [self] + if isinstance(other, (StabilizerCode, Patch)): + flat.append(other) + elif other is not None: + flat.extend(other) + + from gen._viz_patch_svg import patch_svg_viewer + viewer = patch_svg_viewer( + patches=flat, + show_obs=show_obs, show_measure_qubits=show_measure_qubits, - system_qubits=system_qubits, + show_data_qubits=show_data_qubits, + show_order=show_order, + find_logical_err_max_weight=find_logical_err_max_weight, + expected_points=system_qubits, opacity=opacity, - other=obs_patches, + show_coords=show_coords, + tile_color_func=tile_color_func, + cols=cols, + rows=rows, ) + write_file(path, viewer) def with_transformed_coords( self, coord_transform: Callable[[complex], complex] @@ -212,133 +423,31 @@ def make_code_capacity_circuit( self, *, noise: float | NoiseRule, - debug_out_dir: pathlib.Path | None = None, + extra_coords_func: Callable[['Flow'], Iterable[float]] = lambda _: (), ) -> stim.Circuit: - """Creates a circuit implementing this code with a code capacity noise model. - - A code capacity noise model represents transmission over a noisy network - with a noiseless sender and noiseless receiver. There is no noise from - applying operations or measuring stabilizers, and there aren't multiple rounds. - Encoding is done perfectly, then noise is applied to every data qubit, then - decoding is done perfectly. - - Args: - noise: Noise to apply to each data qubit, between the perfect encoding and - perfect decoding. If this is a float, it refers to the strength of a - `DEPOLARIZE1` noise channel. If it's a noise rule, the `after` specifies - the noise to apply to each data qubit (whereas any `flip_result` noise - specified by the rule will have no effect). - debug_out_dir: A location to write files useful for debugging, like a - picture of the stabilizers. - - Returns: - A stim circuit that encodes the code perfectly, applies code capacity noise, - then decodes the code. The circuit will check all observables simultaneously, - using noiseless ancilla qubits if necessary in order to turn anticommuting - observable pairs into commuting observables. - """ if isinstance(noise, (int, float)): noise = NoiseRule(after={"DEPOLARIZE1": noise}) - assert noise.flip_result == 0 - if debug_out_dir is not None: - self.write_svg(debug_out_dir / "code.svg") - self.patch.without_wraparound_tiles().write_svg( - debug_out_dir / "patch.svg", show_order=False - ) - circuit = _make_code_capacity_circuit_for_stabilizer_code( - code=self, - noise=noise, - ) - if debug_out_dir is not None: - from gen._viz_circuit_html import stim_circuit_html_viewer - - write_file(debug_out_dir / "detslice.svg", circuit.diagram("detslice-svg")) - write_file( - debug_out_dir / "graph.html", circuit.diagram("match-graph-3d-html") - ) - write_file( - debug_out_dir / "ideal_circuit.html", - stim_circuit_html_viewer(circuit, patch=self.patch), - ) - write_file( - debug_out_dir / "circuit.html", - stim_circuit_html_viewer(circuit.without_noise(), patch=self.patch), - ) - return circuit + if noise.flip_result: + raise ValueError(f"{noise=} includes measurement noise.") + chunk1 = self.mpp_chunk(noise=NoiseRule(after=noise.after), flow_style='init', resolve_anticommutations=True) + chunk3 = self.mpp_chunk(noise=NoiseRule(before=noise.before), flow_style='end', resolve_anticommutations=True) + from gen._flows._flow_util import compile_chunks_into_circuit + return compile_chunks_into_circuit([chunk1, chunk3], flow_to_extra_coords_func=extra_coords_func) def make_phenom_circuit( self, *, - noise: float | NoiseRule | NoiseModel, + noise: float | NoiseRule, rounds: int, - debug_out_dir: pathlib.Path | None = None, + extra_coords_func: Callable[['gen.Flow'], Iterable[float]] = lambda _: (), ) -> stim.Circuit: - """Creates a circuit implementing this code with a phenomenological noise model. - - A phenomenological noise model applies noise to data qubits between layers of - stabilizer measurements and to the measurement results produced by those - measurements. There is no noise accumulated between stabilizer measurements - in the same layer, or within one stabilizer measurement (like would happen - in a full circuit noise model where it was decomposed into multiple gates). - - Args: - noise: If this is a float, it refers to the strength of a `DEPOLARIZE1` noise - channel applied between each round and also the probability of flipping - each measurement result. If it's a noise rule, its `after` specifies - the noise to apply to each data qubit between each round, and its - `flip_result` is the probability of flipping each measurement result. - rounds: The number of times that the patch stabilizers are noisily measured. - There is an additional noiseless layer of measurements at the start and - at the end, to terminate the problem. Note that data noise is applied - both between normal rounds, and also between a round and one of these - special start/end layers. This means measurement noise is applied `rounds` - times, whereas between-round measurement is applied `rounds+1` times. So - code capacity noise occurs at `rounds=0`. - - TODO: redefine this so code cap noise is at rounds=1. - debug_out_dir: A location to write files useful for debugging, like a - picture of the stabilizers. - - Returns: - A stim circuit that encodes the code perfectly, performs R rounds of - phenomenological noise. then decodes the code perfect;y. The circuit - will check all observables simultaneously, using noiseless ancilla qubits - if necessary in order to turn anticommuting observable pairs into commuting observables. - """ - - if isinstance(noise, NoiseModel): - noise = NoiseRule( - after=noise.idle_noise.after, - flip_result=(noise.any_measurement_rule or noise.measure_rules['Z']).flip_result, - ) if isinstance(noise, (int, float)): noise = NoiseRule(after={"DEPOLARIZE1": noise}, flip_result=noise) - if debug_out_dir is not None: - self.write_svg(debug_out_dir / "code.svg") - self.patch.without_wraparound_tiles().write_svg( - debug_out_dir / "patch.svg", show_order=False - ) - circuit = _make_phenom_circuit_for_stabilizer_code( - code=self, - noise=noise, - rounds=rounds, - ) - if debug_out_dir is not None: - from gen._viz_circuit_html import stim_circuit_html_viewer - - write_file(debug_out_dir / "detslice.svg", circuit.diagram("detslice-svg")) - write_file( - debug_out_dir / "graph.html", circuit.diagram("match-graph-3d-html") - ) - write_file( - debug_out_dir / "ideal_circuit.html", - stim_circuit_html_viewer(circuit, patch=self.patch), - ) - write_file( - debug_out_dir / "circuit.html", - stim_circuit_html_viewer(circuit.without_noise(), patch=self.patch), - ) - return circuit + chunk1 = self.mpp_chunk(noise=NoiseRule(after=noise.after), flow_style='init', resolve_anticommutations=True) + chunk2 = self.mpp_chunk(noise=noise, resolve_anticommutations=True) + chunk3 = self.mpp_chunk(noise=NoiseRule(before=noise.before), flow_style='end', resolve_anticommutations=True) + from gen._flows._flow_util import compile_chunks_into_circuit + return compile_chunks_into_circuit([chunk1, chunk2 * rounds, chunk3], flow_to_extra_coords_func=extra_coords_func) def __repr__(self) -> str: def indented(x: str) -> str: @@ -368,75 +477,3 @@ def __eq__(self, other) -> bool: def __ne__(self, other) -> bool: return not (self == other) - - -def _make_phenom_circuit_for_stabilizer_code( - *, - code: StabilizerCode, - noise: NoiseRule, - suggested_ancilla_qubits: list[complex] | None = None, - rounds: int, -) -> stim.Circuit: - observables, immune = code.entangled_observables( - ancilla_qubits_for_xz_pairs=suggested_ancilla_qubits, - ) - immune = set(immune) - builder = Builder.for_qubits(code.patch.data_set | immune) - - for k, obs in enumerate(observables): - builder.measure_pauli_string(obs, key=f"OBS_START{k}") - builder.obs_include([f"OBS_START{k}"], obs_index=k) - builder.measure_patch(code.patch, save_layer="init") - builder.tick() - - loop = builder.fork() - for k, p in noise.after.items(): - loop.circuit.append( - k, [builder.q2i[q] for q in sorted_complex(code.patch.data_set - immune)], p - ) - loop.measure_patch( - code.patch, save_layer="loop", cmp_layer="init", noise=noise.flip_result - ) - loop.shift_coords(dt=1) - loop.tick() - builder.circuit += loop.circuit * rounds - - builder.measure_patch(code.patch, save_layer="end", cmp_layer="loop") - for k, obs in enumerate(observables): - builder.measure_pauli_string(obs, key=f"OBS_END{k}") - builder.obs_include([f"OBS_END{k}"], obs_index=k) - - return builder.circuit - - -def _make_code_capacity_circuit_for_stabilizer_code( - *, - code: StabilizerCode, - noise: NoiseRule, - suggested_ancilla_qubits: list[complex] | None = None, -) -> stim.Circuit: - assert noise.flip_result == 0 - observables, immune = code.entangled_observables( - ancilla_qubits_for_xz_pairs=suggested_ancilla_qubits, - ) - immune = set(immune) - builder = Builder.for_qubits(code.patch.data_set | immune) - - for k, obs in enumerate(observables): - builder.measure_pauli_string(obs, key=f"OBS_START{k}") - builder.obs_include([f"OBS_START{k}"], obs_index=k) - builder.measure_patch(code.patch, save_layer="init") - builder.tick() - - for k, p in noise.after.items(): - builder.circuit.append( - k, [builder.q2i[q] for q in sorted_complex(code.patch.data_set - immune)], p - ) - builder.tick() - - builder.measure_patch(code.patch, save_layer="end", cmp_layer="init") - for k, obs in enumerate(observables): - builder.measure_pauli_string(obs, key=f"OBS_END{k}") - builder.obs_include([f"OBS_END{k}"], obs_index=k) - - return builder.circuit diff --git a/src/gen/_core/_stabilizer_code_test.py b/src/gen/_core/_stabilizer_code_test.py index 7915093..b807b18 100644 --- a/src/gen/_core/_stabilizer_code_test.py +++ b/src/gen/_core/_stabilizer_code_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim import gen @@ -47,38 +33,35 @@ def test_make_phenom_circuit_for_stabilizer_code(): ).make_phenom_circuit( noise=gen.NoiseRule(flip_result=0.125, after={"DEPOLARIZE1": 0.25}), rounds=100, - ) == stim.Circuit( - """ + ) == stim.Circuit(""" QUBIT_COORDS(0, -1) 0 QUBIT_COORDS(0, 0) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(1, 0) 3 QUBIT_COORDS(1, 1) 4 - MPP X0*X1*X2 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP Z0*Z1*Z3 - OBSERVABLE_INCLUDE(1) rec[-1] - MPP X1*X3 Z1*Z2*Z3*Z4 X2*X4 + MPP X0*X1*X2 Z0*Z1*Z3 X1*X3 Z1*Z2*Z3*Z4 X2*X4 + DEPOLARIZE1(0.25) 1 2 3 4 + OBSERVABLE_INCLUDE(0) rec[-5] + OBSERVABLE_INCLUDE(1) rec[-4] TICK REPEAT 100 { - DEPOLARIZE1(0.25) 1 2 3 4 MPP(0.125) X1*X3 Z1*Z2*Z3*Z4 X2*X4 + DEPOLARIZE1(0.25) 1 2 3 4 DETECTOR(0.5, 0, 0) rec[-6] rec[-3] DETECTOR(0.5, 0.5, 0) rec[-5] rec[-2] DETECTOR(0.5, 1, 0) rec[-4] rec[-1] SHIFT_COORDS(0, 0, 1) TICK } - MPP X1*X3 Z1*Z2*Z3*Z4 X2*X4 - DETECTOR(0.5, 0, 0) rec[-6] rec[-3] - DETECTOR(0.5, 0.5, 0) rec[-5] rec[-2] - DETECTOR(0.5, 1, 0) rec[-4] rec[-1] - MPP X0*X1*X2 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP Z0*Z1*Z3 - OBSERVABLE_INCLUDE(1) rec[-1] - """ - ) + MPP X0*X1*X2 Z0*Z1*Z3 X1*X3 Z1*Z2*Z3*Z4 X2*X4 + OBSERVABLE_INCLUDE(0) rec[-5] + OBSERVABLE_INCLUDE(1) rec[-4] + DETECTOR(0.5, 0, 0) rec[-8] rec[-3] + DETECTOR(0.5, 0.5, 0) rec[-7] rec[-2] + DETECTOR(0.5, 1, 0) rec[-6] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + """) def test_make_code_capacity_circuit_for_stabilizer_code(): @@ -110,31 +93,26 @@ def test_make_code_capacity_circuit_for_stabilizer_code(): observables_z=[obs_z], ).make_code_capacity_circuit( noise=gen.NoiseRule(after={"DEPOLARIZE1": 0.25}), - ) == stim.Circuit( - """ + ) == stim.Circuit(""" QUBIT_COORDS(0, -1) 0 QUBIT_COORDS(0, 0) 1 QUBIT_COORDS(0, 1) 2 QUBIT_COORDS(1, 0) 3 QUBIT_COORDS(1, 1) 4 - MPP X0*X1*X2 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP Z0*Z1*Z3 - OBSERVABLE_INCLUDE(1) rec[-1] - MPP X1*X3 Z1*Z2*Z3*Z4 X2*X4 - TICK + MPP X0*X1*X2 Z0*Z1*Z3 X1*X3 Z1*Z2*Z3*Z4 X2*X4 DEPOLARIZE1(0.25) 1 2 3 4 + OBSERVABLE_INCLUDE(0) rec[-5] + OBSERVABLE_INCLUDE(1) rec[-4] TICK - MPP X1*X3 Z1*Z2*Z3*Z4 X2*X4 - DETECTOR(0.5, 0, 0) rec[-6] rec[-3] - DETECTOR(0.5, 0.5, 0) rec[-5] rec[-2] - DETECTOR(0.5, 1, 0) rec[-4] rec[-1] - MPP X0*X1*X2 - OBSERVABLE_INCLUDE(0) rec[-1] - MPP Z0*Z1*Z3 - OBSERVABLE_INCLUDE(1) rec[-1] - """ - ) + MPP X0*X1*X2 Z0*Z1*Z3 X1*X3 Z1*Z2*Z3*Z4 X2*X4 + OBSERVABLE_INCLUDE(0) rec[-5] + OBSERVABLE_INCLUDE(1) rec[-4] + DETECTOR(0.5, 0, 0) rec[-8] rec[-3] + DETECTOR(0.5, 0.5, 0) rec[-7] rec[-2] + DETECTOR(0.5, 1, 0) rec[-6] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + """) def test_from_patch_with_inferred_observables(): @@ -156,5 +134,5 @@ def test_from_patch_with_inferred_observables(): ] ) ) - code.check_commutation_relationships() + code.verify() assert len(code.observables_x) == len(code.observables_z) == 1 diff --git a/src/gen/_core/_tile.py b/src/gen/_core/_tile.py index 4c3a001..42a07ea 100644 --- a/src/gen/_core/_tile.py +++ b/src/gen/_core/_tile.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import functools from typing import Iterable, Callable, Literal, TYPE_CHECKING from typing import cast @@ -33,7 +19,7 @@ def __init__( bases: str, measurement_qubit: complex, ordered_data_qubits: Iterable[complex | None], - extra_coords: Iterable[float] = (), + flags: Iterable[str] = (), ): """ Args: @@ -47,15 +33,14 @@ def __init__( that they are interacted with. Some entries may be None, indicating that no data qubit is interacted with during the corresponding interaction layer. - extra_coords: Extra coordinate data that can be used for custom - purposes. """ + assert isinstance(bases, str) self.ordered_data_qubits = tuple(ordered_data_qubits) self.measurement_qubit = measurement_qubit if len(bases) == 1: bases *= len(self.ordered_data_qubits) self.bases: str = bases - self.extra_coords = tuple(extra_coords) + self.flags: frozenset[str] = frozenset(flags) if len(self.bases) != len(self.ordered_data_qubits): raise ValueError("len(self.bases_2) != len(self.data_qubits_order)") @@ -71,22 +56,40 @@ def to_data_pauli_string(self) -> "gen.PauliString": ) def with_data_qubit_cleared(self, q: complex) -> "Tile": + return self.with_edits(ordered_data_qubits=[ + None if d == q else d for d in self.ordered_data_qubits + ]) + + def with_edits( + self, + *, + bases: str | None = None, + measurement_qubit: complex | None = None, + ordered_data_qubits: Iterable[complex] | None = None, + extra_coords: Iterable[float] | None = None, + flags: Iterable[str] = None, + ) -> 'Tile': + if ordered_data_qubits is not None: + ordered_data_qubits = tuple(ordered_data_qubits) + if len(ordered_data_qubits) != len(self.ordered_data_qubits) and bases is None: + if self.basis is None: + raise ValueError("Changed data qubit count of non-uniform basis tile.") + bases = self.basis + return Tile( - bases=self.bases, - measurement_qubit=self.measurement_qubit, - ordered_data_qubits=[ - None if d == q else d for d in self.ordered_data_qubits - ], + bases=self.bases if bases is None else bases, + measurement_qubit=self.measurement_qubit if measurement_qubit is None else measurement_qubit, + ordered_data_qubits=self.ordered_data_qubits if ordered_data_qubits is None else ordered_data_qubits, + flags=self.flags if flags is None else flags, ) + def with_bases(self, bases: str) -> "Tile": + return self.with_edits(bases=bases) + with_basis = with_bases + def with_xz_flipped(self) -> "Tile": f = {"X": "Z", "Y": "Y", "Z": "X"} - return Tile( - bases="".join(f[e] for e in self.bases), - measurement_qubit=self.measurement_qubit, - ordered_data_qubits=self.ordered_data_qubits, - extra_coords=self.extra_coords, - ) + return self.with_bases("".join(f[e] for e in self.bases)) def __eq__(self, other): if not isinstance(other, Tile): @@ -95,7 +98,6 @@ def __eq__(self, other): self.ordered_data_qubits == other.ordered_data_qubits and self.measurement_qubit == other.measurement_qubit and self.bases == other.bases - and self.extra_coords == other.extra_coords ) def __ne__(self, other): @@ -108,7 +110,7 @@ def __hash__(self): self.ordered_data_qubits, self.measurement_qubit, self.bases, - self.extra_coords, + self.flags, ) ) @@ -116,8 +118,8 @@ def __repr__(self): b = self.basis or self.bases extra = ( "" - if not self.extra_coords - else f"\n extra_coords={self.extra_coords!r}," "" + if not self.flags + else f"\n flags={sorted(self.flags)!r}," ) return f"""gen.Tile( ordered_data_qubits={self.ordered_data_qubits!r}, @@ -128,28 +130,21 @@ def __repr__(self): def with_transformed_coords( self, coord_transform: Callable[[complex], complex] ) -> "Tile": - return Tile( - bases=self.bases, + return self.with_edits( ordered_data_qubits=[ None if d is None else coord_transform(d) for d in self.ordered_data_qubits ], measurement_qubit=coord_transform(self.measurement_qubit), - extra_coords=self.extra_coords, ) def after_basis_transform( self, basis_transform: Callable[[Literal["X", "Y", "Z"]], Literal["X", "Y", "Z"]], ) -> "Tile": - return Tile( - bases="".join( - basis_transform(cast(Literal["X", "Y", "Z"], e)) for e in self.bases - ), - ordered_data_qubits=self.ordered_data_qubits, - measurement_qubit=self.measurement_qubit, - extra_coords=self.extra_coords, - ) + return self.with_bases("".join( + basis_transform(cast(Literal["X", "Y", "Z"], e)) for e in self.bases + )) @functools.cached_property def data_set(self) -> frozenset[complex]: @@ -162,6 +157,9 @@ def used_set(self) -> frozenset[complex]: @functools.cached_property def basis(self) -> Literal["X", "Y", "Z"] | None: bs = {b for q, b in zip(self.ordered_data_qubits, self.bases) if q is not None} + if len(bs) == 0: + # Fallback to including ejected qubits. + bs = set(self.bases) if len(bs) != 1: return None return next(iter(bs)) diff --git a/src/gen/_core/_tile_test.py b/src/gen/_core/_tile_test.py index 7e978d2..5ea6f54 100644 --- a/src/gen/_core/_tile_test.py +++ b/src/gen/_core/_tile_test.py @@ -1,16 +1,5 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +import functools +from typing import Iterable, Optional, Callable from gen._core._tile import Tile diff --git a/src/gen/_core/_util.py b/src/gen/_core/_util.py index 1dc29cc..85a90e9 100644 --- a/src/gen/_core/_util.py +++ b/src/gen/_core/_util.py @@ -1,18 +1,4 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Callable, Iterable, TypeVar, Any +from typing import Callable, Iterable, TypeVar, Any, Optional TItem = TypeVar("TItem") @@ -28,7 +14,7 @@ def sorted_complex( def min_max_complex( - coords: Iterable[complex], *, default: complex | None = None + coords: Iterable[complex], *, default: Optional[complex] = None ) -> tuple[complex, complex]: """Computes the bounding box of a collection of complex numbers. diff --git a/src/gen/_core/_util_test.py b/src/gen/_core/_util_test.py index c4d5fc4..dd58b82 100644 --- a/src/gen/_core/_util_test.py +++ b/src/gen/_core/_util_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import pytest from gen._core._util import min_max_complex, sorted_complex diff --git a/src/gen/_flows/__init__.py b/src/gen/_flows/__init__.py index 8ed085c..c2f008b 100644 --- a/src/gen/_flows/__init__.py +++ b/src/gen/_flows/__init__.py @@ -1,21 +1,15 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from gen._flows._chunk import ( Chunk, +) +from gen._flows._chunk_loop import ( ChunkLoop, ) +from gen._flows._chunk_reflow import ( + ChunkReflow, +) +from gen._flows._chunk_interface import ( + ChunkInterface, +) from gen._flows._flow import ( Flow, ) @@ -23,6 +17,3 @@ compile_chunks_into_circuit, magic_measure_for_flows, ) -from gen._flows._flow_verifier import ( - FlowStabilizerVerifier, -) diff --git a/src/gen/_flows/_chunk.py b/src/gen/_flows/_chunk.py index 0e902b1..57df46d 100644 --- a/src/gen/_flows/_chunk.py +++ b/src/gen/_flows/_chunk.py @@ -1,28 +1,19 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import pathlib -from typing import Iterable, Callable, Union +from typing import Iterable, Callable, Optional, Any, Literal, TYPE_CHECKING, Union import sinter import stim -from gen._core._patch import Patch -from gen._core._tile import Tile -from gen._flows._flow import Flow, PauliString +from gen._core import Patch, StabilizerCode, KeyedPauliString, PauliString, NoiseModel +from gen._flows._circuit_util import circuit_with_xz_flipped, circuit_to_dem_target_measurement_records_map +from gen._flows._flow import Flow +from gen._flows._test_util import assert_has_same_set_of_items_as from gen._util import stim_circuit_with_transformed_coords, write_file +if TYPE_CHECKING: + from gen._flows._chunk_loop import ChunkLoop + from gen._flows._chunk_interface import ChunkInterface + class Chunk: """A circuit chunk with accompanying stabilizer flow assertions.""" @@ -32,9 +23,8 @@ def __init__( circuit: stim.Circuit, q2i: dict[complex, int], flows: Iterable[Flow], - magic: bool = False, - discarded_inputs: Iterable[PauliString] = (), - discarded_outputs: Iterable[PauliString] = (), + discarded_inputs: Iterable[PauliString | KeyedPauliString] = (), + discarded_outputs: Iterable[PauliString | KeyedPauliString] = (), ): """ Args: @@ -53,24 +43,240 @@ def __init__( will fail when attempting to combine this chunk with a following chunk that has those stabilizers from the anticommuting basis flowing in. - magic: Whether or not the circuit is relying on magical noiseless - operations like noiselessly measuring entire observables. This - is useful when testing and debugging and initially building - circuits, but prevents them from being run on hardware. """ + from gen._flows._flow_util import solve_flow_auto_measurements + self.q2i = q2i - self.magic = magic self.circuit = circuit - self.flows = tuple(flows) - self.discarded_inputs = discarded_inputs - self.discarded_outputs = discarded_outputs + self.flows = solve_flow_auto_measurements(flows=flows, circuit=circuit, q2i=q2i) + self.discarded_inputs = tuple(discarded_inputs) + self.discarded_outputs = tuple(discarded_outputs) + assert all(isinstance(e, (PauliString, KeyedPauliString)) for e in self.discarded_inputs) + assert all(isinstance(e, (PauliString, KeyedPauliString)) for e in self.discarded_outputs) + + def __add__(self, other: 'Chunk') -> 'Chunk': + from gen._flows._flow_util import compile_chunks_into_circuit + new_circuit = compile_chunks_into_circuit([self, other], ignore_errors=True) + new_q2i = {x + 1j*y: i for i, (x, y) in new_circuit.get_final_qubit_coordinates().items()} + + end2flow = {} + for flow in self.flows: + if flow.end: + end2flow[(flow.end, flow.obs_index)] = flow + for key in self.discarded_outputs: + end2flow[key] = 'discard' + + nm1 = self.circuit.num_measurements + nm2 = other.circuit.num_measurements + + new_flows = [] + new_discarded_outputs = list(other.discarded_outputs) + new_discarded_inputs = list(self.discarded_inputs) + + for key in self.discarded_inputs: + prev_flow = end2flow.pop(key) + if prev_flow is None: + raise ValueError("Incompatible chunks.") + if prev_flow != 'discard' and prev_flow.start: + new_discarded_inputs.append((prev_flow.start, prev_flow.obs_index)) + + for flow in other.flows: + if not flow.start: + new_flows.append(flow.with_edits( + measurement_indices=[m % nm2 + nm1 for m in flow.measurement_indices], + )), + continue + + prev_flow = end2flow.pop((flow.start, flow.obs_index)) + if prev_flow is None: + raise ValueError("Incompatible chunks.") + + if prev_flow == 'discard': + if flow.end: + new_discarded_outputs.append((flow.end, flow.obs_index)) + continue + + new_flows.append(flow.with_edits( + start=prev_flow.start, + measurement_indices=[m % nm1 for m in prev_flow.measurement_indices] + [m % nm2 + nm1 for m in flow.measurement_indices], + flags=flow.flags | prev_flow.flags, + )) + + return Chunk( + circuit=new_circuit, + q2i=new_q2i, + flows=new_flows, + discarded_inputs=new_discarded_inputs, + discarded_outputs=new_discarded_outputs, + ) + + def __repr__(self) -> str: + lines = ['gen.Chunk('] + lines.append(f' q2i={self.q2i!r},'), + lines.append(f' circuit={self.circuit!r},'.replace('\n', '\n ')), + if self.flows: + lines.append(f' flows={self.flows!r},'), + if self.discarded_inputs: + lines.append(f' discarded_inputs={self.discarded_inputs!r},'), + if self.discarded_outputs: + lines.append(f' discarded_outputs={self.discarded_outputs!r},'), + lines.append(')') + return '\n'.join(lines) + + def with_noise(self, noise: NoiseModel | float) -> 'Chunk': + if isinstance(noise, float): + noise = NoiseModel.uniform_depolarizing(1e-3) + return self.with_edits( + circuit=noise.noisy_circuit(self.circuit), + ) + + def with_obs_flows_as_det_flows(self) -> 'Chunk': + return self.with_edits( + flows=[ + flow.with_edits(obs_index=None) + for flow in self.flows + ], + ) + + @staticmethod + def from_circuit_with_mpp_boundaries(circuit: stim.Circuit) -> 'Chunk': + allowed = { + 'TICK', + 'OBSERVABLE_INCLUDE', + 'DETECTOR', + 'MPP', + 'QUBIT_COORDS', + 'SHIFT_COORDS', + } + start = 0 + end = len(circuit) + while start < len(circuit) and circuit[start].name in allowed: + start += 1 + while end > 0 and circuit[end - 1].name in allowed: + end -= 1 + while end < len(circuit) and circuit[end].name != 'MPP': + end += 1 + while end > 0 and circuit[end - 1].name == 'TICK': + end -= 1 + if end <= start: + raise ValueError("end <= start") + + prefix, body, suffix = circuit[:start], circuit[start:end], circuit[end:] + start_tick = prefix.num_ticks + end_tick = start_tick + body.num_ticks + 1 + c = stim.Circuit() + c += prefix + c.append('TICK') + c += body + c.append('TICK') + c += suffix + det_regions = c.detecting_regions(ticks=[start_tick, end_tick]) + records = circuit_to_dem_target_measurement_records_map(c) + pn = prefix.num_measurements + record_range = range(pn, pn + body.num_measurements) + + q2i = {qr + qi*1j: i for i, (qr, qi) in circuit.get_final_qubit_coordinates().items()} + i2q = {i: q for q, i in q2i.items()} + dropped_detectors = set() + + flows = [] + for target, items in det_regions.items(): + if target.is_relative_detector_id(): + dropped_detectors.add(target.val) + start_ps: stim.PauliString = items.get(start_tick, stim.PauliString(0)) + start = PauliString({i2q[i]: '_XYZ'[start_ps[i]] for i in start_ps.pauli_indices()}) + + end_ps: stim.PauliString = items.get(end_tick, stim.PauliString(0)) + end = PauliString({i2q[i]: '_XYZ'[end_ps[i]] for i in end_ps.pauli_indices()}) + + flows.append(Flow( + start=start, + end=end, + measurement_indices=[m - record_range.start for m in records[target] if m in record_range], + obs_index=None if target.is_relative_detector_id() else target.val, + center=(sum(start.qubits.keys()) + sum(end.qubits.keys())) / (len(start.qubits) + len(end.qubits)), + )) + + kept = stim.Circuit() + num_d = prefix.num_detectors + for inst in body.flattened(): + if inst.name == 'DETECTOR': + if num_d not in dropped_detectors: + kept.append(inst) + num_d += 1 + elif inst.name != 'OBSERVABLE_INCLUDE': + kept.append(inst) + + return Chunk( + q2i=q2i, + flows=flows, + circuit=kept, + ) + + def _interface( + self, + side: Literal['start', 'end'], + *, + skip_discards: bool = False, + skip_passthroughs: bool = False, + ) -> tuple[PauliString | KeyedPauliString, ...]: + if side == 'start': + include_start = True + include_end = False + elif side == 'end': + include_start = False + include_end = True + else: + raise NotImplementedError(f'{side=}') + + result = [] + for flow in self.flows: + if include_start and flow.start and not (skip_passthroughs and flow.end): + result.append((flow.start, flow.obs_index)) + if include_end and flow.end and not (skip_passthroughs and flow.start): + result.append((flow.end, flow.obs_index)) + if include_start and not skip_discards: + result.extend(self.discarded_inputs) + if include_end and not skip_discards: + result.extend(self.discarded_outputs) + + result_set = set() + collisions = set() + for item in result: + if item in result_set: + collisions.add(item) + result_set.add(item) + + if collisions: + msg = [f"{side} interface had collisions:"] + for a, b in sorted(collisions): + msg.append(f" {a}, obs_index={b}") + raise ValueError("\n".join(msg)) + + return tuple(sorted(result_set)) + + def with_edits( + self, + *, + circuit: stim.Circuit | None = None, + q2i: dict[complex, int] | None = None, + flows: Iterable[Flow] | None = None, + discarded_inputs: Iterable[PauliString | KeyedPauliString] | None = None, + discarded_outputs: Iterable[PauliString | KeyedPauliString] | None = None, + ) -> 'Chunk': + return Chunk( + circuit=self.circuit if circuit is None else circuit, + q2i=self.q2i if q2i is None else q2i, + flows=self.flows if flows is None else flows, + discarded_inputs=self.discarded_inputs if discarded_inputs is None else discarded_inputs, + discarded_outputs=self.discarded_outputs if discarded_outputs is None else discarded_outputs, + ) def __eq__(self, other): if not isinstance(other, Chunk): return NotImplemented return ( self.q2i == other.q2i - and self.magic == other.magic and self.circuit == other.circuit and self.flows == other.flows and self.discarded_inputs == other.discarded_inputs @@ -78,23 +284,22 @@ def __eq__(self, other): ) def write_viewer( - self, path: str | pathlib.Path, *, patch: Patch | None = None + self, path: str | pathlib.Path, *, patch: Optional[Patch] = None ) -> None: from gen import stim_circuit_html_viewer if patch is None: - patch = self.start_patch() - if len(patch.tiles) == 0: - patch = self.end_patch() + patch = self.start_interface() + if len(patch.ports) == 0: + patch = self.end_interface() write_file(path, stim_circuit_html_viewer(self.circuit, patch=patch)) def with_flows_postselected( self, flow_predicate: Callable[[Flow], bool] - ) -> "Chunk": + ) -> 'Chunk': return Chunk( circuit=self.circuit, q2i=self.q2i, - magic=self.magic, flows=[ flow.postselected() if flow_predicate(flow) else flow for flow in self.flows @@ -104,13 +309,33 @@ def with_flows_postselected( ) def __mul__(self, other: int) -> "ChunkLoop": + from gen._flows._chunk_loop import ChunkLoop return ChunkLoop([self], repetitions=other) def with_repetitions(self, repetitions: int) -> "ChunkLoop": + from gen._flows._chunk_loop import ChunkLoop return ChunkLoop([self], repetitions=repetitions) - def verify(self): + def verify( + self, + *, + expected_in: Union['ChunkInterface', 'StabilizerCode', None] = None, + expected_out: Union['ChunkInterface', 'StabilizerCode', None] = None, + should_measure_all_code_stabilizers: bool = False, + ): """Checks that this chunk's circuit actually implements its flows.""" + __tracebackhide__ = True + + assert not should_measure_all_code_stabilizers or expected_in is not None or should_measure_all_code_stabilizers is not None + assert isinstance(self.circuit, stim.Circuit) + assert isinstance(self.q2i, dict) + assert isinstance(self.flows, tuple) + assert isinstance(self.discarded_inputs, tuple) + assert isinstance(self.discarded_outputs, tuple) + assert all(isinstance(e, Flow) for e in self.flows) + assert all(isinstance(e, (PauliString, KeyedPauliString)) for e in self.discarded_inputs) + assert all(isinstance(e, (PauliString, KeyedPauliString)) for e in self.discarded_outputs) + for key, group in sinter.group_by( self.flows, key=lambda flow: (flow.start, flow.obs_index) ).items(): @@ -122,223 +347,155 @@ def verify(self): if key[0] and len(group) > 1: raise ValueError(f"Multiple flows with same non-empty end: {group}") - from gen._flows._flow_verifier import FlowStabilizerVerifier - - FlowStabilizerVerifier.verify(self) + stim_flows = [] + for flow in self.flows: + inp = stim.PauliString(len(self.q2i)) + out = stim.PauliString(len(self.q2i)) + for q, p in flow.start.qubits.items(): + inp[self.q2i[q]] = p + for q, p in flow.end.qubits.items(): + out[self.q2i[q]] = p + stim_flows.append(stim.Flow( + input=inp, + output=out, + measurements=flow.measurement_indices, + )) + if not self.circuit.has_all_flows(stim_flows, unsigned=True): + msg = ["Circuit lacks the following flows:"] + for k in range(len(stim_flows)): + if not self.circuit.has_flow(stim_flows[k], unsigned=True): + msg.append(' ' + str(self.flows[k])) + raise ValueError('\n'.join(msg)) + + if expected_in is not None: + if isinstance(expected_in, StabilizerCode): + expected_in = expected_in.as_interface() + assert_has_same_set_of_items_as( + self.start_interface().with_discards_as_ports().ports, + expected_in.with_discards_as_ports().ports, + "self.start_interface().with_discards_as_ports().ports", + "expected_in.with_discards_as_ports().ports", + ) + if should_measure_all_code_stabilizers: + assert_has_same_set_of_items_as( + self.start_interface(skip_passthroughs=True).without_discards().without_keyed().ports, + expected_in.without_discards().without_keyed().ports, + "self.start_interface(skip_passthroughs=True).without_discards().without_keyed().ports", + "expected_in.without_discards().without_keyed().ports", + ) + else: + # Creating the interface checks for collisions + self.start_interface() + + if expected_out is not None: + if isinstance(expected_out, StabilizerCode): + expected_out = expected_out.as_interface() + assert_has_same_set_of_items_as( + self.end_interface().with_discards_as_ports().ports, + expected_out.with_discards_as_ports().ports, + "self.end_interface().with_discards_as_ports().ports", + "expected_out.with_discards_as_ports().ports", + ) + if should_measure_all_code_stabilizers: + assert_has_same_set_of_items_as( + self.end_interface(skip_passthroughs=True).without_discards().without_keyed().ports, + expected_out.without_discards().without_keyed().ports, + "self.end_interface(skip_passthroughs=True).without_discards().without_keyed().ports", + "expected_out.without_discards().without_keyed().ports", + ) + else: + # Creating the interface checks for collisions + self.end_interface() - def inverted(self) -> "Chunk": + def time_reversed(self) -> 'Chunk': """Checks that this chunk's circuit actually implements its flows.""" - from gen._flows._flow_verifier import FlowStabilizerVerifier - return FlowStabilizerVerifier.invert(self) + stim_flows = [] + for flow in self.flows: + inp = stim.PauliString(len(self.q2i)) + out = stim.PauliString(len(self.q2i)) + for q, p in flow.start.qubits.items(): + inp[self.q2i[q]] = p + for q, p in flow.end.qubits.items(): + out[self.q2i[q]] = p + stim_flows.append(stim.Flow( + input=inp, + output=out, + measurements=flow.measurement_indices, + )) + rev_circuit, rev_flows = self.circuit.time_reversed_for_flows(stim_flows) + nm = rev_circuit.num_measurements + return Chunk( + circuit=rev_circuit, + q2i=self.q2i, + flows=[ + Flow( + center=flow.center, + start=flow.end, + end=flow.start, + measurement_indices=[m + nm for m in rev_flow.measurements_copy()], + flags=flow.flags, + obs_index=flow.obs_index, + ) + for flow, rev_flow in zip(self.flows, rev_flows, strict=True) + ], + discarded_inputs=self.discarded_outputs, + discarded_outputs=self.discarded_inputs, + ) - def with_xz_flipped(self) -> "Chunk": + def with_xz_flipped(self) -> 'Chunk': return Chunk( q2i=self.q2i, - magic=self.magic, circuit=circuit_with_xz_flipped(self.circuit), flows=[flow.with_xz_flipped() for flow in self.flows], - discarded_inputs=[p.with_xz_flipped() for p in self.discarded_inputs], - discarded_outputs=[p.with_xz_flipped() for p in self.discarded_outputs], + discarded_inputs=[(p.with_xz_flipped(), k) for p, k in self.discarded_inputs], + discarded_outputs=[(p.with_xz_flipped(), k) for p, k in self.discarded_outputs], ) def with_transformed_coords( self, transform: Callable[[complex], complex] - ) -> "Chunk": + ) -> 'Chunk': return Chunk( q2i={transform(q): i for q, i in self.q2i.items()}, - magic=self.magic, circuit=stim_circuit_with_transformed_coords(self.circuit, transform), flows=[flow.with_transformed_coords(transform) for flow in self.flows], - discarded_inputs=[ - p.with_transformed_coords(transform) for p in self.discarded_inputs - ], - discarded_outputs=[ - p.with_transformed_coords(transform) for p in self.discarded_outputs - ], + discarded_inputs=[p.with_transformed_coords(transform) for p in self.discarded_inputs], + discarded_outputs=[p.with_transformed_coords(transform) for p in self.discarded_outputs], ) - def magic_init_chunk(self) -> "Chunk": - """Returns a chunk that initializes the stabilizers needed by this one. - - The stabilizers are initialized using direct measurement by MPP, with - no care for connectivity or physical limitations of hardware. - """ - from gen._flows._flow_util import magic_init_for_chunk - - return magic_init_for_chunk(self) - - def magic_end_chunk(self) -> "Chunk": - """Returns a chunk that terminates the stabilizers produced by this one. - - The stabilizers are initialized using direct measurement by MPP, with - no care for connectivity or physical limitations of hardware. - """ - from gen._flows._flow_util import magic_measure_for_chunk - - return magic_measure_for_chunk(self) - - def _boundary_patch(self, end: bool) -> Patch: - tiles = [] - for flow in self.flows: - r = flow.end if end else flow.start - if r.qubits and flow.obs_index is None: - tiles.append( - Tile( - ordered_data_qubits=r.qubits.keys(), - bases="".join(r.qubits.values()), - measurement_qubit=list(r.qubits.keys())[0], - ) - ) - return Patch(tiles) - - def start_patch(self) -> Patch: - return self._boundary_patch(False) - - def flattened(self) -> list["Chunk"]: + def flattened(self) -> list['Chunk']: + """This is here for duck-type compatibility with ChunkLoop.""" return [self] - def end_patch(self) -> Patch: - return self._boundary_patch(True) - - def tick_count(self) -> int: - return self.circuit.num_ticks + 1 - - -class ChunkLoop: - def __init__(self, chunks: Iterable[Union[Chunk, "ChunkLoop"]], repetitions: int): - self.chunks = tuple(chunks) - self.repetitions = repetitions - - @property - def magic(self) -> bool: - return any(c.magic for c in self.chunks) - - def verify(self): - for c in self.chunks: - c.verify() - for k in range(len(self.chunks)): - before: Chunk = self.chunks[k - 1] - after: Chunk = self.chunks[k] - after_in = {} - before_out = {} - for flow in before.flows: - if flow.end: - before_out[flow.end] = flow.obs_index - for flow in after.flows: - if flow.start: - after_in[flow.start] = flow.obs_index - for ps in before.discarded_outputs: - after_in.pop(ps) - for ps in after.discarded_inputs: - before_out.pop(ps) - if after_in != before_out: - raise ValueError("Flows don't match between chunks.") - - def __mul__(self, other: int) -> "ChunkLoop": - return self.with_repetitions(other * self.repetitions) - - def with_repetitions(self, new_repetitions: int) -> "ChunkLoop": - return ChunkLoop(chunks=self.chunks, repetitions=new_repetitions) - - def magic_init_chunk(self) -> "Chunk": - return self.chunks[0].magic_init_chunk() + def mpp_init_chunk(self) -> 'Chunk': + return self.start_interface().mpp_init_chunk() - def magic_end_chunk(self) -> "Chunk": - return self.chunks[-1].magic_end_chunk() + def mpp_end_chunk(self) -> 'Chunk': + return self.end_interface().mpp_end_chunk() - def start_patch(self) -> Patch: - return self.chunks[0].start_patch() + def start_interface(self, *, skip_passthroughs: bool = False) -> 'ChunkInterface': + from gen._flows._chunk_interface import ChunkInterface + return ChunkInterface( + ports=[ + flow.key_start + for flow in self.flows + if flow.start + if not (skip_passthroughs and flow.end) + ], + discards=self.discarded_inputs, + ) - def end_patch(self) -> Patch: - return self.chunks[-1].end_patch() + def end_interface(self, *, skip_passthroughs: bool = False) -> 'ChunkInterface': + from gen._flows._chunk_interface import ChunkInterface + return ChunkInterface( + ports=[ + flow.key_end + for flow in self.flows + if flow.end + if not (skip_passthroughs and flow.start) + ], + discards=self.discarded_outputs, + ) def tick_count(self) -> int: - return sum(e.tick_count() for e in self.chunks) * self.repetitions - - def flattened(self) -> list["Chunk"]: - return [e for c in self.chunks for e in c.flattened()] - - -XZ_FLIPPED = { - "I": "I", - "X": "Z", - "Y": "Y", - "Z": "X", - "C_XYZ": "C_ZYX", - "C_ZYX": "C_XYZ", - "H": "H", - "H_XY": "H_YZ", - "H_XZ": "H_XZ", - "H_YZ": "H_XY", - "S": "SQRT_X", - "SQRT_X": "S", - "SQRT_X_DAG": "S_DAG", - "SQRT_Y": "SQRT_Y", - "SQRT_Y_DAG": "SQRT_Y_DAG", - "S_DAG": "SQRT_X_DAG", - "CX": "XCZ", - "CY": "XCY", - "CZ": "XCX", - "ISWAP": None, - "ISWAP_DAG": None, - "SQRT_XX": "SQRT_ZZ", - "SQRT_XX_DAG": "SQRT_ZZ_DAG", - "SQRT_YY": "SQRT_YY", - "SQRT_YY_DAG": "SQRT_YY_DAG", - "SQRT_ZZ": "SQRT_XX", - "SQRT_ZZ_DAG": "SQRT_XX_DAG", - "SWAP": "SWAP", - "XCX": "CZ", - "XCY": "CY", - "XCZ": "CX", - "YCX": "YCZ", - "YCY": "YCY", - "YCZ": "YCX", - "DEPOLARIZE1": "DEPOLARIZE1", - "DEPOLARIZE2": "DEPOLARIZE2", - "E": None, - "ELSE_CORRELATED_ERROR": None, - "PAULI_CHANNEL_1": None, - "PAULI_CHANNEL_2": None, - "X_ERROR": "Z_ERROR", - "Y_ERROR": "Y_ERROR", - "Z_ERROR": "X_ERROR", - "M": "MX", - "MPP": None, - "MR": "MRX", - "MRX": "MRZ", - "MRY": "MRY", - "MX": "M", - "MY": "MY", - "R": "RX", - "RX": "R", - "RY": "RY", - "DETECTOR": "DETECTOR", - "OBSERVABLE_INCLUDE": "OBSERVABLE_INCLUDE", - "QUBIT_COORDS": "QUBIT_COORDS", - "SHIFT_COORDS": "SHIFT_COORDS", - "TICK": "TICK", -} - - -def circuit_with_xz_flipped(circuit: stim.Circuit) -> stim.Circuit: - result = stim.Circuit() - for inst in circuit: - if isinstance(inst, stim.CircuitRepeatBlock): - result.append( - stim.CircuitRepeatBlock( - body=circuit_with_xz_flipped(inst.body_copy()), - repeat_count=inst.repeat_count, - ) - ) - else: - other = XZ_FLIPPED.get(inst.name) - if other is None: - raise NotImplementedError(f"{inst=}") - result.append( - stim.CircuitInstruction( - other, inst.targets_copy(), inst.gate_args_copy() - ) - ) - return result + return self.circuit.num_ticks + 1 diff --git a/src/gen/_flows/_chunk_interface.py b/src/gen/_flows/_chunk_interface.py new file mode 100644 index 0000000..e417e11 --- /dev/null +++ b/src/gen/_flows/_chunk_interface.py @@ -0,0 +1,209 @@ +import functools +import pathlib +from typing import Iterable, Literal, TYPE_CHECKING, Union, Callable + +from gen._core import Patch, Tile, StabilizerCode, Builder, complex_key, KeyedPauliString +from gen._flows._flow import Flow, PauliString + +if TYPE_CHECKING: + from gen._flows._chunk import Chunk + + +class ChunkInterface: + """Specifies a set of stabilizers and observables that a chunk can consume or prepare.""" + + def __init__( + self, + ports: Iterable[PauliString | KeyedPauliString], + *, + discards: Iterable[PauliString | KeyedPauliString], + ): + self.ports = frozenset(ports) + self.discards = frozenset(discards) + + @functools.cached_property + def used_set(self) -> frozenset[complex]: + return frozenset( + q + for port in self.ports | self.discards + for q in port.qubits.keys() + ) + + def write_svg( + self, + path: str | pathlib.Path, + *, + show_order: bool | Literal["undirected", "3couplerspecial"] = False, + show_measure_qubits: bool = False, + show_data_qubits: bool = True, + system_qubits: Iterable[complex] = (), + opacity: float = 1, + show_coords: bool = True, + show_obs: bool = True, + other: Union[None, 'StabilizerCode', 'Patch', Iterable[Union['StabilizerCode', 'Patch']]] = None, + tile_color_func: Callable[['Tile'], str] | None = None, + rows: int | None = None, + cols: int | None = None, + find_logical_err_max_weight: int | None = None, + ) -> None: + flat = [self] + if isinstance(other, (StabilizerCode, Patch)): + flat.append(other) + elif other is not None: + flat.extend(other) + + from gen._viz_patch_svg import patch_svg_viewer + from gen._util import write_file + viewer = patch_svg_viewer( + patches=flat, + show_obs=show_obs, + show_measure_qubits=show_measure_qubits, + show_data_qubits=show_data_qubits, + show_order=show_order, + find_logical_err_max_weight=find_logical_err_max_weight, + expected_points=system_qubits, + opacity=opacity, + show_coords=show_coords, + tile_color_func=tile_color_func, + cols=cols, + rows=rows, + ) + write_file(path, viewer) + + def without_discards(self) -> 'ChunkInterface': + return self.with_edits(discards=()) + + def without_keyed(self) -> 'ChunkInterface': + return ChunkInterface( + ports=[ + port + for port in self.ports + if isinstance(port, PauliString) + ], + discards=[ + discard + for discard in self.discards + if isinstance(discard, PauliString) + ], + ) + + def with_discards_as_ports(self) -> 'ChunkInterface': + return self.with_edits(discards=(), ports=self.ports | self.discards) + + def with_anonymized_keys(self) -> 'ChunkInterface': + return self.with_edits( + ports=[ + port.pauli_string if isinstance(port, KeyedPauliString) else port + for port in self.ports + ], + discards=[ + discard.pauli_string if isinstance(discard, KeyedPauliString) else discard + for discard in self.discards + ], + ) + + def __repr__(self) -> str: + lines = ['gen.ChunkInterface('] + + lines.append(f' ports=['), + for port in sorted(self.ports): + lines.append(f' {port!r},') + lines.append(' ],') + + if self.discards: + lines.append(f' discards=['), + for discard in sorted(self.discards): + lines.append(f' {discard!r},') + lines.append(' ],') + + lines.append(')') + return '\n'.join(lines) + + def __str__(self) -> str: + lines = [] + for port in sorted(self.ports): + lines.append(str(port)) + for discard in sorted(self.discards): + lines.append(f'discard {discard}') + return '\n'.join(lines) + + def with_edits( + self, + *, + ports: Iterable[PauliString | KeyedPauliString] | None = None, + discards: Iterable[PauliString | KeyedPauliString] | None = None, + ) -> 'ChunkInterface': + return ChunkInterface( + ports=self.ports if ports is None else ports, + discards=self.discards if discards is None else discards, + ) + + def __eq__(self, other): + if not isinstance(other, ChunkInterface): + return NotImplemented + return ( + self.ports == other.ports + and self.discards == other.discards + ) + + @functools.cached_property + def data_set(self) -> frozenset[complex]: + return frozenset( + q + for pauli_string_list in [self.ports, self.discards] + for ps in pauli_string_list + for q in ps.qubits + ) + + def _mpp_chunk(self, *, direction: Literal['in', 'out']) -> 'Chunk': + builder = Builder.for_qubits(self.data_set) + + for port in sorted(self.ports): + builder.measure_pauli_string(port, key=port) + flows = [] + for port in sorted(self.ports): + flows.append(Flow( + start=port.pauli_string if direction == 'in' else None, + end=port.pauli_string if direction == 'out' else None, + center=min(port.qubits, key=complex_key), + measurement_indices=builder.lookup_rec(port), + obs_index=port.key if isinstance(port, KeyedPauliString) else None, + )) + + from gen._flows._chunk import Chunk + return Chunk( + circuit=builder.circuit, + q2i=builder.q2i, + flows=flows, + discarded_inputs=self.discards if direction == 'in' else (), + discarded_outputs=self.discards if direction == 'out' else (), + ) + + def mpp_init_chunk(self) -> 'Chunk': + return self._mpp_chunk(direction='out') + + def mpp_end_chunk(self) -> 'Chunk': + return self._mpp_chunk(direction='in') + + def to_patch(self) -> Patch: + return Patch(tiles=[ + Tile( + bases=''.join(port.qubits.values()), + ordered_data_qubits=port.qubits.keys(), + measurement_qubit=min(port.qubits, key=complex_key), + ) + for pauli_string_list in [self.ports, self.discards] + for port in pauli_string_list + if isinstance(port, PauliString) + ]) + + def to_code(self) -> StabilizerCode: + return StabilizerCode( + patch=self.to_patch(), + observables_x=[ + port.pauli_string + for pauli_string_list in [self.ports, self.discards] + for port in pauli_string_list + if isinstance(port, KeyedPauliString) + ], + ) diff --git a/src/gen/_flows/_chunk_loop.py b/src/gen/_flows/_chunk_loop.py new file mode 100644 index 0000000..b381fda --- /dev/null +++ b/src/gen/_flows/_chunk_loop.py @@ -0,0 +1,61 @@ +from typing import Iterable, Union, TYPE_CHECKING + +from gen._core import Patch + +if TYPE_CHECKING: + from gen._flows._chunk import Chunk + + +class ChunkLoop: + def __init__(self, chunks: Iterable[Union['Chunk', 'ChunkLoop']], repetitions: int): + self.chunks = tuple(chunks) + self.repetitions = repetitions + + @property + def magic(self) -> bool: + return any(c.magic for c in self.chunks) + + def verify(self): + for c in self.chunks: + c.verify() + for k in range(len(self.chunks)): + before: Chunk = self.chunks[k - 1] + after: Chunk = self.chunks[k] + after_in = {} + before_out = {} + for flow in before.flows: + if flow.end: + before_out[flow.end] = flow.obs_index + for flow in after.flows: + if flow.start: + after_in[flow.start] = flow.obs_index + for ps in before.discarded_outputs: + after_in.pop(ps) + for ps in after.discarded_inputs: + before_out.pop(ps) + if after_in != before_out: + raise ValueError("Flows don't match between chunks.") + + def __mul__(self, other: int) -> "ChunkLoop": + return self.with_repetitions(other * self.repetitions) + + def with_repetitions(self, new_repetitions: int) -> "ChunkLoop": + return ChunkLoop(chunks=self.chunks, repetitions=new_repetitions) + + def mpp_init_chunk(self) -> "Chunk": + return self.chunks[0].mpp_init_chunk() + + def mpp_end_chunk(self) -> "Chunk": + return self.chunks[-1].mpp_end_chunk() + + def start_patch(self) -> Patch: + return self.chunks[0].start_patch() + + def end_patch(self) -> Patch: + return self.chunks[-1].end_patch() + + def tick_count(self) -> int: + return sum(e.tick_count() for e in self.chunks) * self.repetitions + + def flattened(self) -> list["Chunk"]: + return [e for c in self.chunks for e in c.flattened()] diff --git a/src/gen/_flows/_chunk_reflow.py b/src/gen/_flows/_chunk_reflow.py new file mode 100644 index 0000000..1569671 --- /dev/null +++ b/src/gen/_flows/_chunk_reflow.py @@ -0,0 +1,176 @@ +import functools +from typing import Iterable, Callable, TYPE_CHECKING + +from gen._core import Patch, Tile, StabilizerCode, KeyedPauliString, PauliString +from gen._flows._test_util import assert_has_same_set_of_items_as + +if TYPE_CHECKING: + from gen._flows._chunk import Chunk + from gen._flows._chunk_interface import ChunkInterface + + +class ChunkReflow: + def __init__( + self, + out2in: dict[PauliString | KeyedPauliString, list[PauliString | KeyedPauliString]], + discard_in: Iterable[PauliString | KeyedPauliString] = (), + ): + self.out2in = out2in + self.discard_in = tuple(discard_in) + assert isinstance(self.out2in, dict) + for k, vs in self.out2in.items(): + assert isinstance(k, (PauliString, KeyedPauliString)), k + assert isinstance(vs, list) + for v in vs: + assert isinstance(v, (PauliString, KeyedPauliString)) + + def with_transformed_coords( + self, transform: Callable[[complex], complex] + ) -> "ChunkReflow": + return ChunkReflow( + out2in={ + kp.with_transformed_coords(transform): [ + vp.with_transformed_coords(transform) + for vp in vs + ] + for kp, vs in self.out2in.items() + }, + discard_in=[ + kp.with_transformed_coords(transform) + for kp in self.discard_in + ] + ) + + def start_interface(self) -> 'ChunkInterface': + from gen._flows._chunk_interface import ChunkInterface + return ChunkInterface( + ports={v for vs in self.out2in.values() for v in vs}, + discards=self.discard_in, + ) + + def end_interface(self) -> 'ChunkInterface': + from gen._flows._chunk_interface import ChunkInterface + return ChunkInterface( + ports=self.out2in.keys(), + discards=self.discard_in, + ) + + def start_code(self) -> StabilizerCode: + tiles = [] + xs = [] + zs = [] + for ps, obs in self.removed_inputs: + if obs is None: + tiles.append( + Tile( + ordered_data_qubits=ps.qubits.keys(), + bases="".join(ps.qubits.values()), + measurement_qubit=list(ps.qubits.keys())[0], + ) + ) + else: + bases = set(ps.qubits.values()) + if bases == {'X'}: + xs.append(ps) + else: + zs.append(ps) + return StabilizerCode(patch=Patch(tiles), observables_x=xs, observables_z=zs) + + def start_patch(self) -> Patch: + tiles = [] + for ps, obs in self.removed_inputs: + if obs is None: + tiles.append( + Tile( + ordered_data_qubits=ps.qubits.keys(), + bases="".join(ps.qubits.values()), + measurement_qubit=list(ps.qubits.keys())[0], + ) + ) + return Patch(tiles) + + def end_patch(self) -> Patch: + tiles = [] + for ps, obs in self.out2in.keys(): + if obs is None: + tiles.append( + Tile( + ordered_data_qubits=ps.qubits.keys(), + bases="".join(ps.qubits.values()), + measurement_qubit=list(ps.qubits.keys())[0], + ) + ) + return Patch(tiles) + + def mpp_init_chunk(self) -> 'Chunk': + return self.start_interface().mpp_init_chunk() + + def mpp_end_chunk(self, *, kept_obs_basis: str | None = None) -> 'Chunk': + return self.end_interface().mpp_end_chunk() + + @functools.cached_property + def removed_inputs(self) -> frozenset[PauliString | KeyedPauliString]: + return frozenset( + v + for vs in self.out2in.values() + for v in vs + ) | frozenset(self.discard_in) + + def verify( + self, + *, + expected_in: StabilizerCode | None = None, + expected_out: StabilizerCode | None = None, + ): + assert isinstance(self.out2in, dict) + for k, vs in self.out2in.items(): + assert isinstance(k, (PauliString, KeyedPauliString)), k + assert isinstance(vs, list) + for v in vs: + assert isinstance(v, (PauliString, KeyedPauliString)) + + for k, vs in self.out2in.items(): + acc = PauliString({}) + for v in vs: + acc *= PauliString(v) + if acc != PauliString(k): + lines = ["A reflow output wasn't equal to the product of its inputs."] + lines.append(f" Output: {k}") + lines.append(f" Difference: {PauliString(k) * acc}") + lines.append(f" Inputs:") + for v in vs: + lines.append(f" {v}") + raise ValueError('\n'.join(lines)) + + if expected_in is not None: + if isinstance(expected_in, StabilizerCode): + expected_in = expected_in.as_interface() + assert_has_same_set_of_items_as( + self.start_interface().with_discards_as_ports().ports, + expected_in.with_discards_as_ports().ports, + "self.start_interface().with_discards_as_ports().ports", + "expected_in.with_discards_as_ports().ports", + ) + + if expected_out is not None: + if isinstance(expected_out, StabilizerCode): + expected_out = expected_out.as_interface() + assert_has_same_set_of_items_as( + self.end_interface().with_discards_as_ports().ports, + expected_out.with_discards_as_ports().ports, + "self.end_interface().with_discards_as_ports().ports", + "expected_out.with_discards_as_ports().ports", + ) + + if len(self.out2in) != len(self.removed_inputs): + msg = [] + msg.append("Number of outputs != number of distinct inputs.") + msg.append("Outputs {") + for ps, obs in self.out2in: + msg.append(f" {ps}, obs={obs}") + msg.append("}") + msg.append("Distinct inputs {") + for ps, obs in self.removed_inputs: + msg.append(f" {ps}, obs={obs}") + msg.append("}") + raise ValueError('\n'.join(msg)) diff --git a/src/gen/_flows/_chunk_test.py b/src/gen/_flows/_chunk_test.py index 236065f..2d82077 100644 --- a/src/gen/_flows/_chunk_test.py +++ b/src/gen/_flows/_chunk_test.py @@ -1,19 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Iterable, Callable - import stim import gen @@ -21,13 +5,11 @@ def test_inverse_flows(): chunk = gen.Chunk( - circuit=stim.Circuit( - """ + circuit=stim.Circuit(""" R 0 1 2 3 4 CX 2 0 M 0 - """ - ), + """), q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, flows=[ gen.Flow( @@ -39,7 +21,7 @@ def test_inverse_flows(): ], ) - inverted = chunk.inverted() + inverted = chunk.time_reversed() inverted.verify() assert len(inverted.flows) == len(chunk.flows) assert inverted.circuit == stim.Circuit( @@ -65,12 +47,12 @@ def test_inverse_circuit(): flows=[], ) - inverted = chunk.inverted() + inverted = chunk.time_reversed() inverted.verify() assert len(inverted.flows) == len(chunk.flows) assert inverted.circuit == stim.Circuit( """ - R 0 + M 0 X 1 CX 3 4 2 0 M 4 3 2 1 0 @@ -110,3 +92,385 @@ def test_with_flows_postselected(): ).postselected() ], ) + + +def test_reflow(): + xx = gen.PauliString({0: 'X', 1: 'X'}) + yy = gen.PauliString({0: 'Y', 1: 'Y'}) + zz = gen.PauliString({0: 'Z', 1: 'Z'}) + chunk1 = gen.Chunk( + q2i={0: 0, 1: 1}, + circuit=stim.Circuit(""" + MPP X0*X1 + MPP Z0*Z1 + """), + flows=[ + gen.Flow( + end=xx, + measurement_indices=[0], + center=0, + ), + gen.Flow( + end=zz, + measurement_indices=[1], + center=0, + ), + ], + ) + chunk2 = gen.Chunk( + q2i={0: 0, 1: 1}, + circuit=stim.Circuit(""" + MPP Y0*Y1 + """), + flows=[ + gen.Flow( + start=yy, + measurement_indices=[0], + center=0, + ), + ], + discarded_inputs=[xx] + ) + reflow = gen.ChunkReflow({yy: [xx, zz], xx: [xx]}) + chunk1.verify() + chunk2.verify() + reflow.verify() + gen.compile_chunks_into_circuit([chunk1, reflow, chunk2]) + + +def test_from_circuit_with_mpp_boundaries_simple(): + chunk = gen.Chunk.from_circuit_with_mpp_boundaries(stim.Circuit(""" + QUBIT_COORDS(1, 2) 0 + MPP X0 + H 0 + MPP Z0 + DETECTOR rec[-1] rec[-2] + """)) + chunk.verify() + assert chunk == gen.Chunk( + q2i={1 + 2j: 0}, + flows=[ + gen.Flow( + start=gen.PauliString(xs=[1 + 2j]), + end=gen.PauliString(zs=[1 + 2j]), + measurement_indices=(), + obs_index=None, + center=1 + 2j, + ) + ], + circuit=stim.Circuit(""" + H 0 + """), + ) + + chunk = gen.Chunk.from_circuit_with_mpp_boundaries(stim.Circuit(""" + QUBIT_COORDS(1, 2) 0 + MR 0 + MR 0 + DETECTOR rec[-1] + """)) + chunk.verify() + assert chunk == gen.Chunk( + q2i={1 + 2j: 0}, + flows=[], + circuit=stim.Circuit(""" + MR 0 + MR 0 + DETECTOR rec[-1] + """), + ) + + chunk = gen.Chunk.from_circuit_with_mpp_boundaries(stim.Circuit(""" + QUBIT_COORDS(1, 2) 0 + QUBIT_COORDS(1, 3) 1 + MPP X0 + TICK + CX 0 1 + TICK + H 0 + MX 1 + TICK + MPP Z0 + DETECTOR rec[-1] rec[-2] rec[-3] + """)) + chunk.verify() + assert chunk == gen.Chunk( + q2i={1 + 2j: 0, 1 + 3j: 1}, + flows=[ + gen.Flow( + start=gen.PauliString(xs=[1 + 2j]), + end=gen.PauliString(zs=[1 + 2j]), + measurement_indices=(0,), + obs_index=None, + center=1 + 2j, + ) + ], + circuit=stim.Circuit(""" + CX 0 1 + TICK + H 0 + MX 1 + """), + ) + + chunk = gen.Chunk.from_circuit_with_mpp_boundaries(stim.Circuit(""" + QUBIT_COORDS(1, 2) 0 + QUBIT_COORDS(1, 3) 1 + MPP X0 + OBSERVABLE_INCLUDE(0) rec[-1] + TICK + CX 0 1 + TICK + MX 1 + OBSERVABLE_INCLUDE(0) rec[-1] + H 0 + TICK + MPP Z0 + OBSERVABLE_INCLUDE(0) rec[-1] + """)) + chunk.verify() + assert chunk == gen.Chunk( + q2i={1 + 2j: 0, 1 + 3j: 1}, + flows=[ + gen.Flow( + start=gen.PauliString(xs=[1 + 2j]), + end=gen.PauliString(zs=[1 + 2j]), + measurement_indices=(0,), + obs_index=0, + center=1 + 2j, + ) + ], + circuit=stim.Circuit(""" + CX 0 1 + TICK + MX 1 + H 0 + """), + ) + + +def test_from_circuit_with_mpp_boundaries_complex(): + chunk = gen.Chunk.from_circuit_with_mpp_boundaries(stim.Circuit(""" + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 0) 2 + QUBIT_COORDS(1, 1) 3 + QUBIT_COORDS(1, 2) 4 + QUBIT_COORDS(2, 0) 5 + QUBIT_COORDS(2, 1) 6 + QUBIT_COORDS(2, 2) 7 + QUBIT_COORDS(2, 3) 8 + QUBIT_COORDS(3, 0) 9 + QUBIT_COORDS(3, 1) 10 + QUBIT_COORDS(3, 2) 11 + QUBIT_COORDS(3, 3) 12 + QUBIT_COORDS(4, 0) 13 + QUBIT_COORDS(4, 1) 14 + QUBIT_COORDS(4, 2) 15 + QUBIT_COORDS(4, 3) 16 + QUBIT_COORDS(5, 3) 17 + #!pragma POLYGON(0,0,1,0.25) 11 15 9 6 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 8 17 15 11 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + TICK + R 0 9 15 11 6 3 17 8 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + DETECTOR(2, 2, 0) rec[-8] + DETECTOR(2, 0, 0) rec[-7] + DETECTOR(4, 3, 0) rec[-6] + DETECTOR(4, 1, 0) rec[-5] + TICK + R 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + MY 17 + DETECTOR(2, 2, 1) rec[-9] + DETECTOR(2, 0, 1) rec[-8] + DETECTOR(4, 3, 1) rec[-7] + DETECTOR(4, 1, 1) rec[-6] + DETECTOR(1, 2, 1) rec[-5] rec[-13] + DETECTOR(1, 0, 1) rec[-4] rec[-12] + DETECTOR(3, 1, 1) rec[-3] rec[-11] + DETECTOR(3, 3, 1) rec[-2] rec[-10] + TICK + #!pragma POLYGON(0,0,1,0.25) 11 15 9 6 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + #!pragma POLYGON(1,1,0,0.25) 9 13 11 6 + TICK + R 13 + RX 14 10 5 2 7 1 + S 15 11 8 3 9 6 0 + TICK + CX 14 15 1 0 10 9 7 8 5 6 2 3 + TICK + CX 3 1 6 7 10 14 + TICK + CX 6 3 10 11 15 14 + TICK + CX 6 10 14 13 + TICK + MX 6 + DETECTOR(3, 3, 2) rec[-1] rec[-2] rec[-7] rec[-8] rec[-10] rec[-11] rec[-13] rec[-15] rec[-16] rec[-18] + TICK + #!pragma POLYGON(0,0,1,0.25) 9 13 11 6 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + #!pragma POLYGON(1,1,0,0.25) 11 15 9 6 + TICK + RX 6 + TICK + CX 6 10 15 14 + TICK + CX 6 3 10 11 14 13 + TICK + CX 3 1 6 7 10 14 + TICK + CX 1 0 10 9 7 8 14 13 5 6 2 3 + TICK + MX 14 10 5 2 7 1 15 + S 6 11 13 9 8 3 0 + DETECTOR(3, 1, 3) rec[-6] + DETECTOR(1, 0, 3) rec[-4] + DETECTOR(2, 2, 3) rec[-3] + DETECTOR(0, 1, 3) rec[-2] + DETECTOR(2, 1, 3) rec[-1] rec[-2] rec[-3] rec[-4] rec[-5] rec[-6] rec[-7] rec[-8] + DETECTOR(4, 2, 3) rec[-1] rec[-7] + TICK + #!pragma POLYGON(0,0,1,0.25) 13 9 6 11 + #!pragma POLYGON(0,1,0,0.25) 8 11 6 3 + #!pragma POLYGON(1,0,0,0.25) 3 6 9 0 + TICK + MPP X8*X11*X6*X3 + DETECTOR(1, 2, 4) rec[-1] rec[-6] rec[-14] + TICK + MPP X13*X9*X6*X11 + DETECTOR(3, 1, 5) rec[-1] rec[-3] rec[-7] rec[-13] + TICK + MPP X9*X0*X3*X6 + DETECTOR(1, 0, 6) rec[-1] rec[-8] rec[-15] + TICK + MPP Z8*Z11*Z6*Z3 + DETECTOR(2, 0, 7) rec[-1] rec[-20] rec[-21] rec[-28] rec[-29] + TICK + MPP Z9*Z0*Z3*Z6 + DETECTOR(2, 2, 8) rec[-1] rec[-22] rec[-30] + TICK + MPP Z13*Z9*Z6*Z11 + DETECTOR(4, 1, 9) rec[-1] rec[-20] rec[-21] rec[-28] rec[-29] + TICK + MPP Y13*Y9*Y0*Y6*Y3*Y8*Y11 + OBSERVABLE_INCLUDE(0) rec[-1] rec[-9] rec[-10] rec[-13] rec[-14] + """)) + chunk.verify() + assert len(chunk.flows) == 7 + assert all(not e.start for e in chunk.flows) + assert chunk.circuit.num_detectors == 25 - 6 + assert chunk.circuit == stim.Circuit(""" + R 0 9 15 11 6 3 17 8 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + DETECTOR(2, 2, 0) rec[-8] + DETECTOR(2, 0, 0) rec[-7] + DETECTOR(4, 3, 0) rec[-6] + DETECTOR(4, 1, 0) rec[-5] + TICK + R 7 5 16 14 + RX 4 2 10 12 + TICK + CX 4 7 2 5 12 16 10 14 + TICK + CX 2 3 5 6 10 11 14 15 7 8 + TICK + CX 2 0 10 6 12 8 7 11 5 9 16 17 + TICK + CX 4 3 10 9 12 11 7 6 16 15 + TICK + CX 4 7 2 5 10 14 12 16 + TICK + M 7 5 16 14 + MX 4 2 10 12 + MY 17 + DETECTOR(2, 2, 1) rec[-9] + DETECTOR(2, 0, 1) rec[-8] + DETECTOR(4, 3, 1) rec[-7] + DETECTOR(4, 1, 1) rec[-6] + DETECTOR(1, 2, 1) rec[-5] rec[-13] + DETECTOR(1, 0, 1) rec[-4] rec[-12] + DETECTOR(3, 1, 1) rec[-3] rec[-11] + DETECTOR(3, 3, 1) rec[-2] rec[-10] + TICK + TICK + R 13 + RX 14 10 5 2 7 1 + S 15 11 8 3 9 6 0 + TICK + CX 14 15 1 0 10 9 7 8 5 6 2 3 + TICK + CX 3 1 6 7 10 14 + TICK + CX 6 3 10 11 15 14 + TICK + CX 6 10 14 13 + TICK + MX 6 + DETECTOR(3, 3, 2) rec[-1] rec[-2] rec[-7] rec[-8] rec[-10] rec[-11] rec[-13] rec[-15] rec[-16] rec[-18] + TICK + TICK + RX 6 + TICK + CX 6 10 15 14 + TICK + CX 6 3 10 11 14 13 + TICK + CX 3 1 6 7 10 14 + TICK + CX 1 0 10 9 7 8 14 13 5 6 2 3 + TICK + MX 14 10 5 2 7 1 15 + S 6 11 13 9 8 3 0 + DETECTOR(3, 1, 3) rec[-6] + DETECTOR(1, 0, 3) rec[-4] + DETECTOR(2, 2, 3) rec[-3] + DETECTOR(0, 1, 3) rec[-2] + DETECTOR(2, 1, 3) rec[-1] rec[-2] rec[-3] rec[-4] rec[-5] rec[-6] rec[-7] rec[-8] + DETECTOR(4, 2, 3) rec[-1] rec[-7] + """) diff --git a/src/gen/_flows/_circuit_util.py b/src/gen/_flows/_circuit_util.py new file mode 100644 index 0000000..b2e816b --- /dev/null +++ b/src/gen/_flows/_circuit_util.py @@ -0,0 +1,103 @@ +import stim + +XZ_FLIPPED = { + "I": "I", + "X": "Z", + "Y": "Y", + "Z": "X", + "C_XYZ": "C_ZYX", + "C_ZYX": "C_XYZ", + "H": "H", + "H_XY": "H_YZ", + "H_XZ": "H_XZ", + "H_YZ": "H_XY", + "S": "SQRT_X", + "SQRT_X": "S", + "SQRT_X_DAG": "S_DAG", + "SQRT_Y": "SQRT_Y", + "SQRT_Y_DAG": "SQRT_Y_DAG", + "S_DAG": "SQRT_X_DAG", + "CX": "XCZ", + "CY": "XCY", + "CZ": "XCX", + "ISWAP": None, + "ISWAP_DAG": None, + "SQRT_XX": "SQRT_ZZ", + "SQRT_XX_DAG": "SQRT_ZZ_DAG", + "SQRT_YY": "SQRT_YY", + "SQRT_YY_DAG": "SQRT_YY_DAG", + "SQRT_ZZ": "SQRT_XX", + "SQRT_ZZ_DAG": "SQRT_XX_DAG", + "SWAP": "SWAP", + "XCX": "CZ", + "XCY": "CY", + "XCZ": "CX", + "YCX": "YCZ", + "YCY": "YCY", + "YCZ": "YCX", + "DEPOLARIZE1": "DEPOLARIZE1", + "DEPOLARIZE2": "DEPOLARIZE2", + "E": None, + "ELSE_CORRELATED_ERROR": None, + "PAULI_CHANNEL_1": None, + "PAULI_CHANNEL_2": None, + "X_ERROR": "Z_ERROR", + "Y_ERROR": "Y_ERROR", + "Z_ERROR": "X_ERROR", + "M": "MX", + "MPP": None, + "MR": "MRX", + "MRX": "MRZ", + "MRY": "MRY", + "MX": "M", + "MY": "MY", + "R": "RX", + "RX": "R", + "RY": "RY", + "DETECTOR": "DETECTOR", + "OBSERVABLE_INCLUDE": "OBSERVABLE_INCLUDE", + "QUBIT_COORDS": "QUBIT_COORDS", + "SHIFT_COORDS": "SHIFT_COORDS", + "TICK": "TICK", +} + + +def circuit_with_xz_flipped(circuit: stim.Circuit) -> stim.Circuit: + result = stim.Circuit() + for inst in circuit: + if isinstance(inst, stim.CircuitRepeatBlock): + result.append( + stim.CircuitRepeatBlock( + body=circuit_with_xz_flipped(inst.body_copy()), + repeat_count=inst.repeat_count, + ) + ) + else: + other = XZ_FLIPPED.get(inst.name) + if other is None: + raise NotImplementedError(f"{inst=}") + result.append( + stim.CircuitInstruction( + other, inst.targets_copy(), inst.gate_args_copy() + ) + ) + return result + + +def circuit_to_dem_target_measurement_records_map(circuit: stim.Circuit) -> dict[stim.DemTarget, list[int]]: + result = {} + for k in range(circuit.num_observables): + result[stim.target_logical_observable_id(k)] = [] + num_d = 0 + num_m = 0 + for inst in circuit.flattened(): + if inst.name == 'DETECTOR': + result[stim.target_relative_detector_id(num_d)] = [num_m + t.value for t in inst.targets_copy()] + num_d += 1 + elif inst.name == 'OBSERVABLE_INCLUDE': + result[stim.target_logical_observable_id(int(inst.gate_args_copy()[0]))].extend(num_m + t.value for t in inst.targets_copy()) + else: + c = stim.Circuit() + c.append(inst) + num_m += c.num_measurements + return result diff --git a/src/gen/_flows/_flow.py b/src/gen/_flows/_flow.py index 29b63c7..ef69622 100644 --- a/src/gen/_flows/_flow.py +++ b/src/gen/_flows/_flow.py @@ -1,20 +1,7 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +from typing import Iterable, Any, Optional, Callable, Literal -from typing import Iterable, Any, Callable - -from gen._core import PauliString +from gen._util import xor_sorted +from gen._core import PauliString, KeyedPauliString class Flow: @@ -25,22 +12,50 @@ def __init__( *, start: PauliString | None = None, end: PauliString | None = None, - measurement_indices: Iterable[int] = (), + measurement_indices: Iterable[int] | Literal['auto'] = (), obs_index: Any = None, - additional_coords: Iterable[float] = (), center: complex, - postselect: bool = False, - allow_vacuous: bool = False, + flags: Iterable[str] = frozenset(), ): - if not allow_vacuous: - assert start or end or measurement_indices, "vacuous flow" self.start = PauliString({}) if start is None else start self.end = PauliString({}) if end is None else end - self.measurement_indices: tuple[int, ...] = tuple(measurement_indices) - self.additional_coords = tuple(additional_coords) + self.measurement_indices: tuple[int, ...] | Literal['auto'] = measurement_indices if measurement_indices == 'auto' else tuple(xor_sorted(measurement_indices)) + self.flags = frozenset(flags) self.obs_index = obs_index self.center = center - self.postselect = postselect + if measurement_indices == 'auto' and not start and not end: + raise ValueError("measurement_indices == 'auto' and not start and not end") + + @property + def key_start(self) -> KeyedPauliString | PauliString: + if self.obs_index is None: + return self.start + return self.start.keyed(self.obs_index) + + @property + def key_end(self) -> KeyedPauliString | PauliString: + if self.obs_index is None: + return self.end + return self.end.keyed(self.obs_index) + + def with_edits( + self, + *, + start: PauliString | None = None, + end: PauliString | None = None, + measurement_indices: Iterable[int] | None = None, + obs_index: Any = 'not_specified', + center: complex | None = None, + flags: Iterable[str] | None = None, + ) -> 'Flow': + return Flow( + start=self.start if start is None else start, + end=self.end if end is None else end, + measurement_indices=self.measurement_indices if measurement_indices is None else measurement_indices, + obs_index=self.obs_index if obs_index == 'not_specified' else obs_index, + center=self.center if center is None else center, + flags=self.flags if flags is None else flags, + ) def __eq__(self, other): if not isinstance(other, Flow): @@ -50,19 +65,41 @@ def __eq__(self, other): and self.end == other.end and self.measurement_indices == other.measurement_indices and self.obs_index == other.obs_index - and self.additional_coords == other.additional_coords + and self.flags == other.flags and self.center == other.center - and self.postselect == other.postselect ) + def __str__(self): + start_terms = [] + for q, p in self.start.qubits.items(): + start_terms.append(f'{p}[{q}]') + end_terms = [] + for q, p in self.end.qubits.items(): + q = complex(q) + if q.real == 0: + q = '0+' + str(q) + q = str(q).replace('(', '').replace(')', '') + end_terms.append(f'{p}[{q}]') + if self.measurement_indices == 'auto': + end_terms.append('rec[auto]') + else: + for m in self.measurement_indices: + end_terms.append(f'rec[{m}]') + if not start_terms: + start_terms.append('1') + if not end_terms: + end_terms.append('1') + key = '' if self.obs_index is None else f' (obs={self.obs_index})' + return f'{"*".join(start_terms)} -> {"*".join(end_terms)}{key}' + def __repr__(self): return ( f"Flow(start={self.start!r}, " f"end={self.end!r}, " f"measurement_indices={self.measurement_indices!r}, " - f"additional_coords={self.additional_coords!r}, " + f"flags={sorted(self.flags)}, " f"obs_index={self.obs_index!r}, " - f"postselect={self.postselect!r})" + f"center={self.center!r}" ) def postselected(self) -> "Flow": @@ -71,9 +108,8 @@ def postselected(self) -> "Flow": end=self.end, measurement_indices=self.measurement_indices, obs_index=self.obs_index, - additional_coords=self.additional_coords, + flags=self.flags | {"postselect"}, center=self.center, - postselect=True, ) def with_xz_flipped(self) -> "Flow": @@ -82,9 +118,8 @@ def with_xz_flipped(self) -> "Flow": end=self.end.with_xz_flipped(), measurement_indices=self.measurement_indices, obs_index=self.obs_index, - additional_coords=self.additional_coords, + flags=self.flags, center=self.center, - postselect=self.postselect, ) def with_transformed_coords( @@ -95,9 +130,8 @@ def with_transformed_coords( end=self.end.with_transformed_coords(transform), measurement_indices=self.measurement_indices, obs_index=self.obs_index, - additional_coords=self.additional_coords, + flags=self.flags, center=transform(self.center), - postselect=self.postselect, ) def concat(self, other: "Flow", other_measure_offset: int) -> "Flow": @@ -105,15 +139,23 @@ def concat(self, other: "Flow", other_measure_offset: int) -> "Flow": raise ValueError("other.start != self.end") if other.obs_index != self.obs_index: raise ValueError("other.obs_index != self.obs_index") - if other.additional_coords != self.additional_coords: - raise ValueError("other.additional_coords != self.additional_coords") return Flow( start=self.start, end=other.end, center=(self.center + other.center) / 2, - measurement_indices=self.measurement_indices - + tuple(m + other_measure_offset for m in other.measurement_indices), + measurement_indices=self.measurement_indices + tuple(m + other_measure_offset for m in other.measurement_indices), obs_index=self.obs_index, - additional_coords=self.additional_coords, - postselect=self.postselect or other.postselect, + flags=self.flags | other.flags, + ) + + def __mul__(self, other: 'Flow') -> 'Flow': + if other.obs_index != self.obs_index: + raise ValueError("other.obs_index != self.obs_index") + return Flow( + start=self.start * other.start, + end=self.end * other.end, + measurement_indices=sorted(set(self.measurement_indices) ^ set(other.measurement_indices)), + obs_index=self.obs_index, + flags=self.flags | other.flags, + center=(self.center + other.center) / 2, ) diff --git a/src/gen/_flows/_flow_test.py b/src/gen/_flows/_flow_test.py index 4ee3ed2..a230243 100644 --- a/src/gen/_flows/_flow_test.py +++ b/src/gen/_flows/_flow_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from gen._flows._flow import Flow from gen._core._pauli_string import PauliString diff --git a/src/gen/_flows/_flow_util.py b/src/gen/_flows/_flow_util.py index 0c46b08..e7a87e7 100644 --- a/src/gen/_flows/_flow_util.py +++ b/src/gen/_flows/_flow_util.py @@ -1,29 +1,21 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Union, Any, Literal, Iterable +import collections +import dataclasses +from typing import Union, Any, Literal, Iterable, Callable import stim -from gen._core._builder import MeasurementTracker, Builder, AtLayer -from gen._core._util import sorted_complex -from gen._flows._chunk import Chunk, ChunkLoop +import gen +from gen._core import KeyedPauliString, MeasurementTracker, Builder, AtLayer, sorted_complex +from gen._flows._chunk import Chunk +from gen._flows._chunk_loop import ChunkLoop +from gen._flows._chunk_reflow import ChunkReflow from gen._flows._flow import PauliString, Flow def magic_init_for_chunk( chunk: Chunk, + *, + single_kept_obs_basis: str | None = None, ) -> Chunk: builder = Builder( q2i=chunk.q2i, @@ -32,36 +24,44 @@ def magic_init_for_chunk( ) index = 0 flows = [] + discards = [] for flow in chunk.flows: if flow.start: - builder.measure_pauli_string(flow.start, key=AtLayer(index, "solo")) - flows.append( - Flow( - center=flow.center, - end=flow.start, - measurement_indices=[index], - obs_index=flow.obs_index, - additional_coords=flow.additional_coords, + if flow.obs_index is not None and single_kept_obs_basis is not None and set(flow.start.qubits.values()) != set(single_kept_obs_basis): + discards.append((flow.start, flow.obs_index)) + else: + builder.measure_pauli_string(flow.start, key=AtLayer(index, "solo")) + flows.append( + Flow( + center=flow.center, + end=flow.start, + measurement_indices=[index], + obs_index=flow.obs_index, + flags=flow.flags, + ) ) - ) - index += 1 + index += 1 return Chunk( circuit=builder.circuit, q2i=builder.q2i, flows=flows, - magic=True, + discarded_outputs=discards, ) def magic_measure_for_chunk( chunk: Chunk, + *, + single_kept_obs_basis: str | None = None, ) -> Chunk: - return magic_measure_for_flows(chunk.flows) + return magic_measure_for_flows(chunk.flows, single_kept_obs_basis=single_kept_obs_basis) def magic_measure_for_flows( flows: list[Flow], + *, + single_kept_obs_basis: str | None = None, ) -> Chunk: all_qubits = sorted_complex({q for flow in flows for q in (flow.end.qubits or [])}) q2i = {q: i for i, q in enumerate(all_qubits)} @@ -72,26 +72,30 @@ def magic_measure_for_flows( ) index = 0 out_flows = [] + discards = [] for flow in flows: if flow.end: - key = AtLayer(index, "solo") - builder.measure_pauli_string(flow.end, key=key) - out_flows.append( - Flow( - center=flow.center, - start=flow.end, - measurement_indices=[index], - obs_index=flow.obs_index, - additional_coords=flow.additional_coords, + if flow.obs_index is not None and single_kept_obs_basis is not None and set(flow.end.qubits.values()) != set(single_kept_obs_basis): + discards.append((flow.start, flow.obs_index)) + else: + key = AtLayer(index, "solo") + builder.measure_pauli_string(flow.end, key=key) + out_flows.append( + Flow( + center=flow.center, + start=flow.end, + measurement_indices=[index], + obs_index=flow.obs_index, + flags=flow.flags, + ) ) - ) - index += 1 + index += 1 return Chunk( circuit=builder.circuit, q2i=builder.q2i, flows=out_flows, - magic=True, + discarded_inputs=discards, ) @@ -144,12 +148,96 @@ class _ChunkCompileState: def __init__( self, *, - open_flows: dict[tuple[PauliString, Any], Union[Flow, Literal["discard"]]], + open_flows: dict[PauliString | KeyedPauliString, Union[Flow, Literal["discard"]]], measure_offset: int, ): self.open_flows = open_flows self.measure_offset = measure_offset + def verify(self): + for (k1, k2), v in self.open_flows.items(): + assert isinstance(k1, (PauliString, KeyedPauliString)) + assert v == "discard" or isinstance(v, Flow) + + def __str__(self) -> str: + lines = [] + lines.append("_ChunkCompileState {") + + lines.append(" discard_flows {") + for key, flow in self.open_flows.items(): + if flow == "discard": + lines.append(f" {key}") + lines.append(" }") + + lines.append(" det_flows {") + for key, flow in self.open_flows.items(): + if flow != "discard" and flow.obs_index is None: + lines.append(f" {flow.end}, ms={flow.measurement_indices}") + lines.append(" }") + + lines.append(" obs_flows {") + for key, flow in self.open_flows.items(): + if flow != "discard" and flow.obs_index is not None: + lines.append(f" {flow.end}: ms={flow.measurement_indices}") + lines.append(" }") + + lines.append(f" measure_offset = {self.measure_offset}") + lines.append("}") + return '\n'.join(lines) + + +def _compile_chunk_reflow_into_circuit( + *, + chunk_reflow: ChunkReflow, + state: _ChunkCompileState, +) -> _ChunkCompileState: + next_flows: dict[PauliString | KeyedPauliString, Union[Flow, Literal["discard"]]] = {} + for output, inputs in chunk_reflow.out2in.items(): + measurements = set() + centers = [] + flags = set() + discarded = False + for inp_key in inputs: + if inp_key not in state.open_flows: + msg = [] + msg.append(f"Missing reflow input: {inp_key=}") + msg.append("Needed inputs {") + for ps in inputs: + msg.append(f" {ps}") + msg.append("}") + msg.append("Actual inputs {") + for ps in state.open_flows.keys(): + msg.append(f" {ps}") + msg.append("}") + raise ValueError('\n'.join(msg)) + inp = state.open_flows[inp_key] + if inp == 'discard': + discarded = True + else: + assert isinstance(inp, Flow) + assert not inp.start + measurements ^= frozenset(inp.measurement_indices) + centers.append(inp.center) + flags |= inp.flags + next_flows[output] = 'discard' if discarded else gen.Flow( + start=None, + end=output.pauli_string if isinstance(output, KeyedPauliString) else output, + measurement_indices=tuple(sorted(measurements)), + obs_index=output.key if isinstance(output, KeyedPauliString) else None, + flags=flags, + center=sum(centers) / len(centers), + ) + for k, v in state.open_flows.items(): + if k in chunk_reflow.removed_inputs: + continue + assert k not in next_flows + next_flows[k] = v + + return _ChunkCompileState( + measure_offset=state.measure_offset, + open_flows=next_flows, + ) + def _compile_chunk_into_circuit_many_repetitions( *, @@ -159,6 +247,7 @@ def _compile_chunk_into_circuit_many_repetitions( ignore_errors: bool, out_circuit: stim.Circuit, q2i: dict[complex, int], + flow_to_extra_coords_func: Callable[[Flow], Iterable[float]], ) -> _ChunkCompileState: if chunk_loop.repetitions == 0: return state @@ -170,6 +259,7 @@ def _compile_chunk_into_circuit_many_repetitions( ignore_errors=ignore_errors, out_circuit=out_circuit, q2i=q2i, + flow_to_extra_coords_func=flow_to_extra_coords_func, ) assert chunk_loop.repetitions > 1 @@ -199,6 +289,7 @@ def _compile_chunk_into_circuit_many_repetitions( ignore_errors=ignore_errors, out_circuit=circuits[-1], q2i=q2i, + flow_to_extra_coords_func=flow_to_extra_coords_func, ) if fully_in_loop: @@ -230,6 +321,7 @@ def _compile_chunk_into_circuit_sequence( ignore_errors: bool, out_circuit: stim.Circuit, q2i: dict[complex, int], + flow_to_extra_coords_func: Callable[[Flow], Iterable[float]], ) -> _ChunkCompileState: for sub_chunk in chunks: state = _compile_chunk_into_circuit( @@ -239,6 +331,7 @@ def _compile_chunk_into_circuit_sequence( ignore_errors=ignore_errors, out_circuit=out_circuit, q2i=q2i, + flow_to_extra_coords_func=flow_to_extra_coords_func, ) return state @@ -251,9 +344,10 @@ def _compile_chunk_into_circuit_atomic( ignore_errors: bool, out_circuit: stim.Circuit, q2i: dict[complex, int], + flow_to_extra_coords_func: Callable[[Flow], Iterable[float]], ) -> _ChunkCompileState: prev_flows = dict(state.open_flows) - next_flows: dict[tuple[PauliString, Any], Union[Flow, Literal["discard"]]] = {} + next_flows: dict[PauliString | KeyedPauliString, Union[Flow, Literal["discard"]]] = {} dumped_flows: list[Flow] = [] if include_detectors: for flow in chunk.flows: @@ -265,19 +359,25 @@ def _compile_chunk_into_circuit_atomic( measurement_indices=[ m + state.measure_offset for m in flow.measurement_indices ], - postselect=flow.postselect, - additional_coords=flow.additional_coords, + flags=flow.flags, ) if flow.start: - prev = prev_flows.pop((flow.start, flow.obs_index), None) + prev = prev_flows.pop(flow.key_start, None) if prev is None: if ignore_errors: continue else: - raise ValueError(f"Missing prev {flow!r} have {prev_flows!r}") + lines = [ + "A flow input wasn't satisfied.", + f" Expected input: {flow.key_start}", + f" Available inputs:", + ] + for prev_avail in prev_flows.keys(): + lines.append(f" {prev_avail}") + raise ValueError('\n'.join(lines)) elif prev == "discard": if flow.end: - next_flows[(flow.end, flow.obs_index)] = "discard" + next_flows[flow.key_end] = "discard" continue flow = prev.concat(flow, 0) if flow.end: @@ -289,42 +389,48 @@ def _compile_chunk_into_circuit_atomic( obs_index=flow.obs_index, center=flow.center, ) - next_flows[(flow.end, flow.obs_index)] = flow + next_flows[flow.key_end] = flow else: dumped_flows.append(flow) for discarded in chunk.discarded_inputs: - prev_flows.pop((discarded, None), None) + prev_flows.pop(discarded, None) for discarded in chunk.discarded_outputs: - assert (discarded, None) not in next_flows - next_flows[(discarded, None)] = "discard" + assert discarded not in next_flows + next_flows[discarded] = "discard" + unused_output = False for flow, val in prev_flows.items(): if val != "discard" and not ignore_errors: - raise ValueError( - f"Some flows were left over (not matched) when moving into chunk: {list(prev_flows.values())!r}" - ) + unused_output = True + if unused_output: + lines = ["Some flow outputs were unused:"] + for flow, val in prev_flows.items(): + if val != "discard" and not ignore_errors: + lines.append(f" {flow}") + raise ValueError('\n'.join(lines)) new_measure_offset = state.measure_offset + chunk.circuit.num_measurements _append_circuit_with_reindexed_qubits_to_circuit( circuit=chunk.circuit, out=out_circuit, old_q2i=chunk.q2i, new_q2i=q2i ) if include_detectors: - any_detectors = False + det_offset_within_circuit = max([e[2] + 1 for e in chunk.circuit.get_detector_coordinates().values()], default=0) + if det_offset_within_circuit > 0: + out_circuit.append("SHIFT_COORDS", [], (0, 0, det_offset_within_circuit)) + coord_use_counts = collections.Counter() for flow in dumped_flows: targets = [] for m in flow.measurement_indices: targets.append(stim.target_rec(m - new_measure_offset)) if flow.obs_index is None: - coords = (flow.center.real, flow.center.imag, 0) - if flow.additional_coords: - coords += flow.additional_coords - if flow.postselect: - coords += (999,) + dt = coord_use_counts[flow.center] + coord_use_counts[flow.center] += 1 + coords = (flow.center.real, flow.center.imag, dt) + tuple(flow_to_extra_coords_func(flow)) out_circuit.append("DETECTOR", targets, coords) - any_detectors = True else: out_circuit.append("OBSERVABLE_INCLUDE", targets, flow.obs_index) - if any_detectors: - out_circuit.append("SHIFT_COORDS", [], (0, 0, 1)) + det_offset = max(coord_use_counts.values(), default=0) + if det_offset > 0: + out_circuit.append("SHIFT_COORDS", [], (0, 0, det_offset)) if len(out_circuit) > 0 and out_circuit[-1].name != "TICK": out_circuit.append("TICK") @@ -334,16 +440,99 @@ def _compile_chunk_into_circuit_atomic( ) +@dataclasses.dataclass +class _EditFlow: + inp: stim.PauliString + meas: set[int] + out: stim.PauliString + key: int | None + + +def solve_flow_auto_measurements(*, flows: Iterable[Flow], circuit: stim.Circuit, q2i: dict[complex, int]) -> tuple[Flow, ...]: + flows = tuple(flows) + if all(flow.measurement_indices != 'auto' for flow in flows): + return flows + + table: list[_EditFlow] = [] + num_qubits = circuit.num_qubits + for flow in circuit.flow_generators(): + inp = flow.input_copy() + out = flow.output_copy() + if len(inp) == 0: + inp = stim.PauliString(num_qubits) + if len(out) == 0: + out = stim.PauliString(num_qubits) + table.append(_EditFlow( + inp=inp, + meas=set(flow.measurements_copy()), + out=out, + key=None, + )) + + for k in range(len(flows)): + if flows[k].measurement_indices == 'auto': + inp = stim.PauliString(num_qubits) + for q, p in flows[k].start.qubits.items(): + inp[q2i[q]] = p + out = stim.PauliString(num_qubits) + for q, p in flows[k].end.qubits.items(): + out[q2i[q]] = p + table.append(_EditFlow(inp=inp, meas=set(), out=out, key=k)) + + num_solved = 0 + + def partial_elim(predicate: Callable[[_EditFlow], bool]): + nonlocal num_solved + for k in range(num_solved, len(table)): + if predicate(table[k]): + pivot = k + break + else: + return + + for k in range(len(table)): + if k != pivot and predicate(table[k]): + table[k].inp *= table[pivot].inp + table[k].meas ^= table[pivot].meas + table[k].out *= table[pivot].out + t0 = table[pivot] + t1 = table[num_solved] + table[pivot] = t1 + table[num_solved] = t0 + num_solved += 1 + + for q in range(num_qubits): + partial_elim(lambda f: f.inp[q] & 1) + partial_elim(lambda f: f.inp[q] & 2) + for q in range(num_qubits): + partial_elim(lambda f: f.out[q] & 1) + partial_elim(lambda f: f.out[q] & 2) + + flows = list(flows) + for t in table: + if t.key is not None: + if t.inp.weight > 0 or t.out.weight > 0: + raise ValueError(f"Failed to solve {flows[t.key]}") + flows[t.key] = flows[t.key].with_edits(measurement_indices=t.meas) + return tuple(flows) + + def _compile_chunk_into_circuit( *, - chunk: Union[Chunk, ChunkLoop], + chunk: Union[Chunk, ChunkLoop, ChunkReflow], state: _ChunkCompileState, include_detectors: bool, ignore_errors: bool, out_circuit: stim.Circuit, q2i: dict[complex, int], + flow_to_extra_coords_func: Callable[[Flow], Iterable[float]], ) -> _ChunkCompileState: - if isinstance(chunk, ChunkLoop): + if isinstance(chunk, ChunkReflow): + return _compile_chunk_reflow_into_circuit( + chunk_reflow=chunk, + state=state, + ) + elif isinstance(chunk, ChunkLoop): return _compile_chunk_into_circuit_many_repetitions( chunk_loop=chunk, state=state, @@ -351,16 +540,29 @@ def _compile_chunk_into_circuit( ignore_errors=ignore_errors, out_circuit=out_circuit, q2i=q2i, + flow_to_extra_coords_func=flow_to_extra_coords_func, ) + elif isinstance(chunk, Chunk): + return _compile_chunk_into_circuit_atomic( + chunk=chunk, + state=state, + include_detectors=include_detectors, + ignore_errors=ignore_errors, + out_circuit=out_circuit, + q2i=q2i, + flow_to_extra_coords_func=flow_to_extra_coords_func, + ) + else: + raise NotImplementedError(f'{chunk=}') - return _compile_chunk_into_circuit_atomic( - chunk=chunk, - state=state, - include_detectors=include_detectors, - ignore_errors=ignore_errors, - out_circuit=out_circuit, - q2i=q2i, - ) + +def _fail_if_flags(flow: Flow): + flags = flow.flags + if len(flags) == 1 and 'postselect' in flags: + return 999, + if flags: + raise ValueError(f"Flow has {flags=}, but `flow_to_extra_coords_func` wasn't specified.") + return () def compile_chunks_into_circuit( @@ -368,21 +570,27 @@ def compile_chunks_into_circuit( *, include_detectors: bool = True, ignore_errors: bool = False, + add_magic_boundaries: bool = False, + flow_to_extra_coords_func: Callable[[Flow], Iterable[float]] = _fail_if_flags, ) -> stim.Circuit: all_qubits = set() + if add_magic_boundaries: + chunks = [chunks[0].mpp_init_chunk(), *chunks, chunks[-1].mpp_end_chunk()] - def _process(sub_chunk: Union[Chunk, ChunkLoop]): + def _compute_all_qubits(sub_chunk: Union[Chunk, ChunkLoop, ChunkReflow]): nonlocal all_qubits if isinstance(sub_chunk, ChunkLoop): for sub_sub_chunk in sub_chunk.chunks: - _process(sub_sub_chunk) + _compute_all_qubits(sub_sub_chunk) elif isinstance(sub_chunk, Chunk): all_qubits |= sub_chunk.q2i.keys() + elif isinstance(sub_chunk, ChunkReflow): + pass else: raise NotImplementedError(f"{sub_chunk=}") for c in chunks: - _process(c) + _compute_all_qubits(c) q2i = {q: i for i, q in enumerate(sorted_complex(set(all_qubits)))} full_circuit = stim.Circuit() @@ -398,6 +606,7 @@ def _process(sub_chunk: Union[Chunk, ChunkLoop]): ignore_errors=ignore_errors, out_circuit=full_circuit, q2i=q2i, + flow_to_extra_coords_func=flow_to_extra_coords_func, ) if include_detectors: if state.open_flows: diff --git a/src/gen/_flows/_flow_util_test.py b/src/gen/_flows/_flow_util_test.py index f0f8463..8bee100 100644 --- a/src/gen/_flows/_flow_util_test.py +++ b/src/gen/_flows/_flow_util_test.py @@ -1,20 +1,7 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim import gen +from gen._flows._flow_util import solve_flow_auto_measurements def test_magic_init_for_chunk(): @@ -33,7 +20,7 @@ def test_magic_init_for_chunk(): ), ], ) - c2 = chunk.magic_init_chunk() + c2 = chunk.mpp_init_chunk() c2.verify() @@ -187,3 +174,17 @@ def test_compile_postselected_chunks(): TICK """ ) + + +def test_solve_flow_auto_measurements(): + assert solve_flow_auto_measurements( + flows=[gen.Flow(start=gen.PauliString({'Z': [0 + 1j, 2 + 1j]}), measurement_indices='auto', center=-1, flags={'X'})], + circuit=stim.Circuit(""" + R 1 + CX 0 1 2 1 + M 1 + """), + q2i={0 + 1j: 0, 1 + 1j: 1, 2 + 1j: 2}, + ) == ( + gen.Flow(start=gen.PauliString({'Z': [0 + 1j, 2 + 1j]}), measurement_indices=[0], center=-1, flags={'X'}), + ) diff --git a/src/gen/_flows/_flow_verifier.py b/src/gen/_flows/_flow_verifier.py deleted file mode 100644 index bcf438c..0000000 --- a/src/gen/_flows/_flow_verifier.py +++ /dev/null @@ -1,593 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import collections -from typing import Iterable - -import numpy as np -import stim - -from gen._flows._flow import Flow -from gen._flows._chunk import Chunk - - -FLIP_REV_SET = { - "CX", - "CY", - "CZ", - "XCY", -} -REV_DICT = { - "I": "I", - "X": "X", - "Y": "Y", - "Z": "Z", - "C_XYZ": "C_ZYX", - "C_ZYX": "C_XYZ", - "H": "H", - "H_XY": "H_XY", - "H_XZ": "H_XZ", - "H_YZ": "H_YZ", - "S": "S", - "SQRT_X": "SQRT_X", - "SQRT_X_DAG": "SQRT_X", - "SQRT_Y": "SQRT_Y", - "SQRT_Y_DAG": "SQRT_Y", - "S_DAG": "S", - "CX": "XCZ", - "CY": "YCZ", - "CZ": "CZ", - "ISWAP": "ISWAP", - "ISWAP_DAG": "ISWAP", - "SQRT_XX": "SQRT_XX", - "SQRT_XX_DAG": "SQRT_XX", - "SQRT_YY": "SQRT_YY", - "SQRT_YY_DAG": "SQRT_YY", - "SQRT_ZZ": "SQRT_ZZ", - "SQRT_ZZ_DAG": "SQRT_ZZ", - "SWAP": "SWAP", - "XCX": "XCX", - "CXSWAP": "CXSWAP", - "SWAPCX": "SWAPCX", - "XCY": "YCX", - "XCZ": "CX", - "YCX": "XCY", - "YCY": "YCY", - "YCZ": "CY", - "SHIFT_COORDS": "SHIFT_COORDS", - "TICK": "TICK", -} - - -class FlowStabilizerVerifier: - def __init__( - self, next_measurement: int, q2i: dict[complex, int], flows: Iterable[Flow] - ): - self.flows: tuple[Flow, ...] = tuple(flows) - self.q2i = q2i - self.i2m = collections.defaultdict(list) - self.measurement_to_can_be_destructive: set[int] = set() - self.reset_index = 0 - self.next_measurement = next_measurement - self.reset_to_flow_indices: collections.defaultdict[ - int, list[int] - ] = collections.defaultdict(list) - num_qubits = max(q2i.values()) + 1 - self.xs = np.zeros(shape=(num_qubits, len(self.flows)), dtype=np.bool_) - self.zs = np.zeros(shape=(num_qubits, len(self.flows)), dtype=np.bool_) - for k in range(len(self.flows)): - flow: Flow = self.flows[k] - for m in flow.measurement_indices: - self.i2m[m].append(k) - for q, p in flow.end.qubits.items(): - assert p == "X" or p == "Y" or p == "Z" - self.xs[q2i[q], k] = p == "X" or p == "Y" - self.zs[q2i[q], k] = p == "Z" or p == "Y" - - def fail_if(self, mask: np.ndarray, msg: str): - if np.any(mask): - for k in range(len(mask)): - if mask[k]: - self.fail(k, msg) - - def pauli_terms(self, k: int) -> str: - i2q = {i: q for q, i in self.q2i.items()} - terms = [] - for q in range(self.xs.shape[0]): - x = self.xs[q, k] - z = self.zs[q, k] - if x or z: - terms.append("_XZY"[x + z * 2] + repr(i2q[q])) - return "*".join(terms) - - def fail(self, k: int, msg: str): - raise ValueError( - f"{msg} for flow {self.flows[k]} with current value {self.pauli_terms(k)}" - ) - - def finish(self): - for k in range(len(self.flows)): - for q, p in self.flows[k].start.qubits.items(): - assert p == "X" or p == "Y" or p == "Z" - self.xs[self.q2i[q], k] ^= p == "X" or p == "Y" - self.zs[self.q2i[q], k] ^= p == "Z" or p == "Y" - if np.any(self.xs) or np.any(self.zs): - for k in range(len(self.flows)): - if np.any(self.xs[:, k]) or np.any(self.zs[:, k]): - self.fail(k, "Mismatch at start") - - @staticmethod - def verify(chunk: "Chunk") -> "FlowStabilizerVerifier": - verifier = FlowStabilizerVerifier( - q2i=chunk.q2i, - flows=chunk.flows, - next_measurement=chunk.circuit.num_measurements - 1, - ) - for inst in chunk.circuit.flattened()[::-1]: - verifier.rev_apply(inst) - verifier.finish() - return verifier - - @staticmethod - def invert(chunk: "Chunk") -> Chunk: - v = FlowStabilizerVerifier.verify(chunk) - measurement_to_flow_indices: collections.defaultdict[ - int, list[int] - ] = collections.defaultdict(list) - for k, flow in enumerate(chunk.flows): - for m in flow.measurement_indices: - measurement_to_flow_indices[m].append(k) - - header = stim.Circuit() - rev_circuit = stim.Circuit() - new_flow_measurements: list[list[int]] = [[] for _ in range(len(v.flows))] - reset_index = 0 - new_measure_index = 0 - old_measure_index = chunk.circuit.num_measurements - for inst in chunk.circuit.flattened()[::-1]: - if inst.name in FLIP_REV_SET: - old_targets = inst.targets_copy() - new_targets = [ - old_targets[k + i] - for k in range(0, len(old_targets), 2)[::-1] - for i in range(2) - ] - rev_circuit.append(inst.name, new_targets, inst.gate_args_copy()) - elif inst.name in REV_DICT: - rev_circuit.append( - REV_DICT[inst.name], - inst.targets_copy()[::-1], - inst.gate_args_copy(), - ) - elif inst.name in ["R", "RX", "RY"]: - ts = inst.targets_copy()[::-1] - rev_circuit.append( - inst.name.replace("R", "M"), ts, inst.gate_args_copy() - ) - for k in range(len(ts)): - for f in v.reset_to_flow_indices[reset_index]: - new_flow_measurements[f].append(new_measure_index) - new_measure_index += 1 - reset_index += 1 - elif inst.name in ["M", "MX", "MY"]: - ts = inst.targets_copy()[::-1] - if all( - old_measure_index - k - 1 in v.measurement_to_can_be_destructive - for k in range(len(ts)) - ): - rev_circuit.append( - inst.name.replace("M", "R"), ts, inst.gate_args_copy() - ) - old_measure_index -= len(ts) - else: - rev_circuit.append(inst.name, ts, inst.gate_args_copy()) - for k in range(len(ts)): - for f in measurement_to_flow_indices[old_measure_index]: - new_flow_measurements[f].append(new_measure_index) - old_measure_index -= 1 - new_measure_index += 1 - elif inst.name == "QUBIT_COORDS": - header.append(inst) - else: - raise NotImplementedError(f"{inst=}") - - return Chunk( - circuit=header + rev_circuit, - q2i=chunk.q2i, - flows=[ - Flow( - center=flow.center, - start=flow.end, - end=flow.start, - measurement_indices=new_flow_measurements[k], - additional_coords=flow.additional_coords, - obs_index=flow.obs_index, - allow_vacuous=True, - ) - for k, flow in enumerate(chunk.flows) - ], - discarded_inputs=chunk.discarded_outputs, - discarded_outputs=chunk.discarded_inputs, - ) - - def rev_apply(self, inst: stim.CircuitInstruction): - if inst.name == "H" or inst.name == "SQRT_Y" or inst.name == "SQRT_Y_DAG": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - tmp = self.xs[q].copy() - self.xs[q, :] = self.zs[q] - self.zs[q, :] = tmp - elif ( - inst.name == "I" or inst.name == "Z" or inst.name == "X" or inst.name == "Y" - ): - pass - elif inst.name == "S" or inst.name == "S_DAG" or inst.name == "H_XY": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - self.zs[q, :] ^= self.xs[q] - elif inst.name == "SQRT_X" or inst.name == "SQRT_X_DAG" or inst.name == "H_YZ": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - self.xs[q, :] ^= self.zs[q] - elif inst.name == "C_XYZ": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - self.zs[q, :] ^= self.xs[q] - self.xs[q, :] ^= self.zs[q] - elif inst.name == "C_ZYX": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - self.xs[q, :] ^= self.zs[q] - self.zs[q, :] ^= self.xs[q] - elif inst.name == "RY": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - self.fail_if(self.xs[q] ^ self.zs[q], "Anticommuted with RY") - for k in range(len(self.flows)): - if self.xs[q, k] and self.zs[q, k]: - self.reset_to_flow_indices[self.reset_index].append(k) - self.reset_index += 1 - self.xs[q, :] = 0 - self.zs[q, :] = 0 - elif inst.name == "RX": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - self.fail_if(self.zs[q], "Anticommuted with RX") - for k in range(len(self.flows)): - if self.xs[q, k]: - self.reset_to_flow_indices[self.reset_index].append(k) - self.reset_index += 1 - self.xs[q, :] = 0 - elif inst.name == "R": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - self.fail_if(self.xs[q], "Anticommuted with R") - for k in range(len(self.flows)): - if self.zs[q, k]: - self.reset_to_flow_indices[self.reset_index].append(k) - self.reset_index += 1 - self.zs[q, :] = 0 - elif inst.name == "M": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - m = self.next_measurement - self.next_measurement -= 1 - self.fail_if(self.xs[q], "Anticommuted with M") - if not np.any(self.xs[q, :]) and not np.any(self.zs[q, :]): - self.measurement_to_can_be_destructive.add(m) - for s in self.i2m[m]: - self.zs[q, s] ^= True - elif inst.name == "MR": - for gate in "RM": - self.rev_apply( - stim.CircuitInstruction( - name=gate, - targets=inst.targets_copy(), - gate_args=inst.gate_args_copy(), - ) - ) - elif inst.name == "CXSWAP": - for gate in ["SWAP", "CX"]: - self.rev_apply( - stim.CircuitInstruction( - name=gate, - targets=inst.targets_copy(), - gate_args=inst.gate_args_copy(), - ) - ) - elif inst.name == "SWAPCX": - for gate in ["CX", "SWAP"]: - self.rev_apply( - stim.CircuitInstruction( - name=gate, - targets=inst.targets_copy(), - gate_args=inst.gate_args_copy(), - ) - ) - elif inst.name == "MRX": - for gate in ["RX", "MX"]: - self.rev_apply( - stim.CircuitInstruction( - name=gate, - targets=inst.targets_copy(), - gate_args=inst.gate_args_copy(), - ) - ) - elif inst.name == "MRY": - for gate in ["RY", "MY"]: - self.rev_apply( - stim.CircuitInstruction( - name=gate, - targets=inst.targets_copy(), - gate_args=inst.gate_args_copy(), - ) - ) - elif inst.name == "MY": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - m = self.next_measurement - self.next_measurement -= 1 - self.fail_if(self.xs[q] ^ self.zs[q], "Anticommuted with M") - if not np.any(self.xs[q, :]) and not np.any(self.zs[q, :]): - self.measurement_to_can_be_destructive.add(m) - for s in self.i2m[m]: - self.xs[q, s] ^= True - self.zs[q, s] ^= True - elif inst.name == "MX": - for t in inst.targets_copy()[::-1]: - assert t.is_qubit_target - q = t.value - m = self.next_measurement - self.next_measurement -= 1 - self.fail_if(self.zs[q], "Anticommuted with MX") - if not np.any(self.xs[q, :]) and not np.any(self.zs[q, :]): - self.measurement_to_can_be_destructive.add(m) - for s in self.i2m[m]: - self.xs[q, s] ^= True - elif inst.name == "XCZ": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - self.xs[q1] ^= self.xs[q2] - self.zs[q2] ^= self.zs[q1] - elif inst.name == "CX": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t2.is_qubit_target - q2 = t2.value - if t1.is_measurement_record_target: - m = self.next_measurement + t1.value + 1 - for s in np.flatnonzero(self.zs[q2, :]): - self.i2m[m].append(s) - else: - assert t1.is_qubit_target - q1 = t1.value - self.xs[q2] ^= self.xs[q1] - self.zs[q1] ^= self.zs[q2] - elif inst.name == "CZ": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t2.is_qubit_target - q2 = t2.value - if t1.is_measurement_record_target: - m = self.next_measurement + t1.value + 1 - for s in np.flatnonzero(self.xs[q2, :]): - self.i2m[m].append(s) - else: - assert t1.is_qubit_target - q1 = t1.value - self.zs[q2] ^= self.xs[q1] - self.zs[q1] ^= self.xs[q2] - elif inst.name == "CY" or inst.name == "YCZ": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - if inst.name == "YCZ": - t1, t2 = t2, t1 - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - yt = self.xs[q2] ^ self.zs[q2] - self.zs[q1] ^= yt - self.zs[q2] ^= self.xs[q1] - self.xs[q2] ^= self.xs[q1] - elif inst.name == "ISWAP" or inst.name == "ISWAP_DAG": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - # swap - for bs in [self.xs, self.zs]: - bs[q1] ^= bs[q2] - bs[q2] ^= bs[q1] - bs[q1] ^= bs[q2] - # cz - self.zs[q2] ^= self.xs[q1] - self.zs[q1] ^= self.xs[q2] - # s s - self.zs[q1, :] ^= self.xs[q1] - self.zs[q2, :] ^= self.xs[q2] - elif inst.name == "SQRT_ZZ" or inst.name == "SQRT_ZZ_DAG": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - # cz - self.zs[q2] ^= self.xs[q1] - self.zs[q1] ^= self.xs[q2] - # s s - self.zs[q1, :] ^= self.xs[q1] - self.zs[q2, :] ^= self.xs[q2] - elif inst.name == "XCX": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - self.xs[q2] ^= self.zs[q1] - self.xs[q1] ^= self.zs[q2] - elif inst.name == "YCY": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - # s s - self.zs[q1, :] ^= self.xs[q1] - self.zs[q2, :] ^= self.xs[q2] - # xcx - self.xs[q2] ^= self.zs[q1] - self.xs[q1] ^= self.zs[q2] - # s s - self.zs[q1, :] ^= self.xs[q1] - self.zs[q2, :] ^= self.xs[q2] - elif inst.name == "SQRT_YY" or inst.name == "SQRT_YY_DAG": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - # s s - self.zs[q1, :] ^= self.xs[q1] - self.zs[q2, :] ^= self.xs[q2] - # xcx - self.xs[q2] ^= self.zs[q1] - self.xs[q1] ^= self.zs[q2] - # sqrt_x sqrt_x - self.xs[q1, :] ^= self.zs[q1] - self.xs[q2, :] ^= self.zs[q2] - # s s - self.zs[q1, :] ^= self.xs[q1] - self.zs[q2, :] ^= self.xs[q2] - elif inst.name == "SQRT_XX" or inst.name == "SQRT_XX_DAG": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - # xcx - self.xs[q2] ^= self.zs[q1] - self.xs[q1] ^= self.zs[q2] - # sqrt_x sqrt_x - self.xs[q1, :] ^= self.zs[q1] - self.xs[q2, :] ^= self.zs[q2] - elif inst.name == "SWAP": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - for bs in [self.xs, self.zs]: - bs[q1] ^= bs[q2] - bs[q2] ^= bs[q1] - bs[q1] ^= bs[q2] - elif inst.name == "XCY" or inst.name == "YCX": - ts = inst.targets_copy() - for k in range(0, len(ts), 2)[::-1]: - t1 = ts[k] - t2 = ts[k + 1] - if inst.name == "YCX": - t1, t2 = t2, t1 - assert t1.is_qubit_target - assert t2.is_qubit_target - q1 = t1.value - q2 = t2.value - yt = self.xs[q2] ^ self.zs[q2] - self.xs[q1] ^= yt - self.zs[q2] ^= self.zs[q1] - self.xs[q2] ^= self.zs[q1] - elif inst.name == "MPP": - targets = inst.targets_copy()[::-1] - start = 0 - while start < len(targets): - end = start + 1 - while end < len(targets) and targets[end].is_combiner: - end += 2 - - x_mask = np.zeros(shape=self.xs.shape[0], dtype=np.bool_) - z_mask = np.zeros(shape=self.xs.shape[0], dtype=np.bool_) - for t in targets[start:end:2]: - if t.is_x_target: - x_mask[t.value] ^= True - elif t.is_y_target: - x_mask[t.value] ^= True - z_mask[t.value] ^= True - elif t.is_z_target: - z_mask[t.value] ^= True - else: - raise NotImplementedError(f"{inst=}") - - for k in range(self.xs.shape[1]): - x_combos = np.sum(np.bitwise_and(self.xs[:, k], z_mask), axis=0) - z_combos = np.sum(np.bitwise_and(self.zs[:, k], x_mask), axis=0) - if np.any((x_combos + z_combos) & 1): - raise ValueError("Anticommuted with MPP") - m = self.next_measurement - self.next_measurement -= 1 - for s in self.i2m[m]: - self.zs[:, s] ^= z_mask - self.xs[:, s] ^= x_mask - - start = end - - elif inst.name == "TICK": - pass - elif inst.name == "QUBIT_COORDS": - pass - else: - raise NotImplementedError(f"{inst=}") diff --git a/src/gen/_flows/_flow_verifier_test.py b/src/gen/_flows/_flow_verifier_test.py deleted file mode 100644 index 19bc2b1..0000000 --- a/src/gen/_flows/_flow_verifier_test.py +++ /dev/null @@ -1,833 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest -import stim - -import gen - - -def test_verify_h(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - H 0 - """ - ), - q2i={1j: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({1j: "X"}), - end=gen.PauliString({1j: "Z"}), - ) - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - H 0 - """ - ), - q2i={1j: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({1j: "X"}), - end=gen.PauliString({1j: "Y"}), - ) - ], - ) - with pytest.raises(ValueError): - chunk.verify() - - -def test_verify_r(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - R 0 - """ - ), - q2i={1j: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({1j: "Z"}), - end=gen.PauliString({}), - ) - ], - ) - with pytest.raises(ValueError): - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - R 0 - """ - ), - q2i={1j: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({}), - end=gen.PauliString({1j: "Z"}), - ) - ], - ) - chunk.verify() - - -def test_verify_measurement(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - R 1 - CX 0 1 2 1 - M 1 3 - H 0 - """ - ), - q2i={0: 0, 1j: 1, 2j: 2, 3j: 3}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "Z", 2j: "Z"}), - measurement_indices=[0], - ), - gen.Flow( - center=0, - end=gen.PauliString({0: "X", 2j: "Z"}), - measurement_indices=[0], - ), - ], - ) - chunk.verify() - - -def test_verify_mpp(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - MPP X0*Y1*Z2 - """ - ), - q2i={0: 0, 1j: 1, 2j: 2, 3j: 3}, - flows=[ - gen.Flow( - center=0, - end=gen.PauliString({0: "X", 1j: "Y", 2j: "Z"}), - measurement_indices=[0], - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - MPP X0*Y1*Z2 - MPP X0*X1*Z2 - """ - ), - q2i={0: 0, 1j: 1, 2j: 2, 3j: 3}, - flows=[ - gen.Flow( - center=0, - end=gen.PauliString({0: "X", 1j: "Y", 2j: "Z"}), - measurement_indices=[0], - ), - ], - ) - with pytest.raises(ValueError, match="Anticommuted with MPP"): - chunk.verify() - - -def test_verify_c_xyz(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - C_XYZ 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "Y", 1: "Z", 2: "X"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - C_ZYX 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "Z", 1: "X", 2: "Y"}), - ), - ], - ) - chunk.verify() - - -def test_verify_s(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - S 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "Y", 1: "X", 2: "Z"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - S_DAG 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "Y", 1: "X", 2: "Z"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - H_XY 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "Y", 1: "X", 2: "Z"}), - ), - ], - ) - chunk.verify() - - -def test_verify_sqrt_x(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - SQRT_X 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "X", 1: "Z", 2: "Y"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - SQRT_X_DAG 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "X", 1: "Z", 2: "Y"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - H_YZ 0 1 2 - """ - ), - q2i={0: 0, 1: 1, 2: 2}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Z"}), - end=gen.PauliString({0: "X", 1: "Z", 2: "Y"}), - ), - ], - ) - chunk.verify() - - -def test_verify_cy(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - CY 0 1 2 3 4 5 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "Y", 2: "Y", 3: "Y", 4: "Z"}), - end=gen.PauliString({0: "X", 2: "Y", 4: "Z"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - CY 0 1 2 3 4 5 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 2: "Y", 4: "Z"}), - end=gen.PauliString({0: "X", 1: "Y", 2: "Y", 3: "Y", 4: "Z"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - CY 0 1 2 3 4 5 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "X", 2: "Y", 3: "X", 4: "Z", 5: "X"}), - end=gen.PauliString({0: "Y", 1: "Z", 2: "X", 3: "Z", 5: "X"}), - ), - ], - ) - chunk.verify() - - -@pytest.mark.parametrize( - "gate", - [ - "I", - "X", - "Y", - "Z", - "C_XYZ", - "C_ZYX", - "H", - "H_XY", - "H_XZ", - "H_YZ", - "S", - "SQRT_X", - "SQRT_X_DAG", - "SQRT_Y", - "SQRT_Y_DAG", - "SQRT_Z", - "SQRT_Z_DAG", - "S_DAG", - "CXSWAP", - "SWAPCX", - "CNOT", - "CX", - "CY", - "CZ", - "ISWAP", - "ISWAP_DAG", - "SQRT_XX", - "SQRT_XX_DAG", - "SQRT_YY", - "SQRT_YY_DAG", - "SQRT_ZZ", - "SQRT_ZZ_DAG", - "SWAP", - "XCX", - "XCY", - "XCZ", - "YCX", - "YCY", - "YCZ", - "ZCX", - "ZCY", - "ZCZ", - ], -) -def test_verify_gate_flows_vs_stim_tableau(gate: str): - circuit = stim.Circuit(f"{gate} 0 1") - tableau = stim.Tableau.from_circuit(circuit) - flows = [] - if len(tableau) == 2: - for p0 in "IXYZ": - for p1 in "IXYZ": - i = stim.PauliString(p0 + p1) - flows.append((i, tableau(i))) - elif len(tableau) == 1: - for p0 in "IXYZ": - i = stim.PauliString(p0) - flows.append((i, tableau(i))) - else: - raise NotImplementedError() - for flow in flows: - chunk = gen.Chunk( - circuit=circuit, - q2i={0: 0, 1: 1}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString.from_stim_pauli_string(flow[0]), - end=gen.PauliString.from_stim_pauli_string(flow[1]), - allow_vacuous=True, - ), - ], - ) - chunk.verify() - inverse = chunk.inverted() - inverse.verify() - - -def test_verify_swap(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - SWAP 0 1 2 3 4 5 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 2: "Y", 3: "Y", 4: "Z", 5: "Y"}), - end=gen.PauliString({1: "X", 3: "Y", 2: "Y", 5: "Z", 4: "Y"}), - ), - ], - ) - chunk.verify() - - -def test_verify_xcy(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - XCY 0 1 2 3 4 5 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 2: "Y", 3: "Y", 4: "Z", 5: "Y"}), - end=gen.PauliString({0: "X", 2: "Y", 4: "Z"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - YCX 1 0 3 2 5 4 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 2: "Y", 3: "Y", 4: "Z", 5: "Y"}), - end=gen.PauliString({0: "X", 2: "Y", 4: "Z"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - XCY 0 1 2 3 4 5 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 2: "Y", 4: "Z"}), - end=gen.PauliString({0: "X", 2: "Y", 3: "Y", 4: "Z", 5: "Y"}), - ), - ], - ) - chunk.verify() - - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - XCY 0 1 2 3 4 5 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X", 1: "X", 2: "Y", 3: "X", 4: "Z", 5: "X"}), - end=gen.PauliString({1: "X", 2: "Z", 3: "Z", 4: "Y", 5: "Z"}), - ), - ], - ) - chunk.verify() - - -def test_reset_analysis(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - R 0 1 2 3 4 - CX 2 0 - M 0 - """ - ), - q2i={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({}), - measurement_indices=[0], - end=gen.PauliString({1: "Z"}), - ), - ], - ) - f = gen.FlowStabilizerVerifier.verify(chunk) - assert f.reset_to_flow_indices == {2: [0], 3: [0], 4: [0]} - - inverted = gen.FlowStabilizerVerifier.invert(chunk) - inverted.verify() - assert len(inverted.flows) == len(chunk.flows) - assert inverted.circuit == stim.Circuit( - """ - R 0 - CX 2 0 - M 4 3 2 1 0 - """ - ) - - -def test_verify_mr(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - MR 0 - """ - ), - q2i={0: 0, 1: 1}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "Z"}), - measurement_indices=[0], - ), - gen.Flow( - center=0, - end=gen.PauliString({0: "Z"}), - ), - ], - ) - chunk.verify() - - -def test_verify_feedback_cx(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - MR 0 - CX rec[-1] 1 - """ - ), - q2i={0: 0, 1: 1}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "Z", 1: "Y"}), - end=gen.PauliString({1: "Y"}), - ), - gen.Flow( - center=0, - start=gen.PauliString({0: "Z"}), - end=gen.PauliString({}), - measurement_indices=[0], - ), - gen.Flow( - center=0, - start=gen.PauliString({}), - end=gen.PauliString({0: "Z"}), - measurement_indices=[], - ), - gen.Flow( - center=0, - start=gen.PauliString({1: "Z"}), - end=gen.PauliString({1: "Z"}), - measurement_indices=[0], - ), - gen.Flow( - center=0, - start=gen.PauliString({1: "X"}), - end=gen.PauliString({1: "X"}), - measurement_indices=[], - ), - ], - ) - chunk2 = gen.Chunk( - circuit=stim.Circuit( - """ - MR 0 - CX rec[-1] 1 - """ - ), - q2i={0: 0, 1: 1}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({1: "Y"}), - end=gen.PauliString({1: "Y"}), - measurement_indices=[0], - ), - ], - ) - chunk.verify() - chunk2.verify() - - -def test_verify_feedback_cz(): - chunk = gen.Chunk( - circuit=stim.Circuit( - """ - MR 0 - CZ rec[-1] 1 - """ - ), - q2i={0: 0, 1: 1}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "Z", 1: "Y"}), - end=gen.PauliString({1: "Y"}), - ), - gen.Flow( - center=0, - start=gen.PauliString({0: "Z"}), - end=gen.PauliString({}), - measurement_indices=[0], - ), - gen.Flow( - center=0, - start=gen.PauliString({}), - end=gen.PauliString({0: "Z"}), - measurement_indices=[], - ), - gen.Flow( - center=0, - start=gen.PauliString({1: "X"}), - end=gen.PauliString({1: "X"}), - measurement_indices=[0], - ), - gen.Flow( - center=0, - start=gen.PauliString({1: "Z"}), - end=gen.PauliString({1: "Z"}), - measurement_indices=[], - ), - ], - ) - chunk2 = gen.Chunk( - circuit=stim.Circuit( - """ - MR 0 - CX rec[-1] 1 - """ - ), - q2i={0: 0, 1: 1}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({1: "Y"}), - end=gen.PauliString({1: "Y"}), - measurement_indices=[0], - ), - ], - ) - chunk.verify() - chunk2.verify() - - -def test_verify_feedback_passthrough(): - gen.Chunk( - circuit=stim.Circuit( - """ - M 0 - R 0 - CX rec[-1] 0 - """ - ), - q2i={0: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "Z"}), - end=gen.PauliString({0: "Z"}), - ), - ], - ).verify() - - gen.Chunk( - circuit=stim.Circuit( - """ - M 0 - R 0 - CX rec[-1] 0 - """ - ), - q2i={0: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "Z"}), - measurement_indices=[0], - ), - ], - ).verify() - - gen.Chunk( - circuit=stim.Circuit( - """ - M 0 - R 0 - CX rec[-1] 0 - """ - ), - q2i={0: 0}, - flows=[ - gen.Flow( - center=0, - end=gen.PauliString({0: "Z"}), - measurement_indices=[0], - ), - ], - ).verify() - - gen.Chunk( - circuit=stim.Circuit( - """ - MX 0 - RX 0 - CZ rec[-1] 0 - """ - ), - q2i={0: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X"}), - end=gen.PauliString({0: "X"}), - ), - ], - ).verify() - gen.Chunk( - circuit=stim.Circuit( - """ - MX 0 - RX 0 - CZ rec[-1] 0 - """ - ), - q2i={0: 0}, - flows=[ - gen.Flow( - center=0, - start=gen.PauliString({0: "X"}), - measurement_indices=[0], - ), - ], - ).verify() - gen.Chunk( - circuit=stim.Circuit( - """ - MX 0 - RX 0 - CZ rec[-1] 0 - """ - ), - q2i={0: 0}, - flows=[ - gen.Flow( - center=0, - end=gen.PauliString({0: "X"}), - measurement_indices=[0], - ), - ], - ).verify() - - -def test_reproduce_feedback_verification_failure(): - gen.Chunk( - circuit=stim.Circuit( - """ - MRX 1 - CZ rec[-1] 1 - """ - ), - q2i={ - 1j: 0, - 2j: 1, - 3j: 2, - }, # Note: extra coordinates were key to triggering the bug. - flows=[ - gen.Flow( - end=gen.PauliString({2j: "X"}), - measurement_indices=[0], - center=0, - ), - ], - ).verify() diff --git a/src/gen/_flows/_test_util.py b/src/gen/_flows/_test_util.py new file mode 100644 index 0000000..8cc991f --- /dev/null +++ b/src/gen/_flows/_test_util.py @@ -0,0 +1,25 @@ +from typing import Iterable + + +def assert_has_same_set_of_items_as( + actual: Iterable, + expected: Iterable, + actual_name: str = 'actual', + expected_name: str = 'expected') -> None: + __tracebackhide__ = True + + actual = frozenset(actual) + expected = frozenset(expected) + if actual == expected: + return + + lines = [f"set({actual_name}) != set({expected_name})", ""] + if actual - expected: + lines.append("Extra items in actual (left):") + for d in sorted(actual - expected): + lines.append(f' {d}') + if expected - actual: + lines.append("Missing items in actual (left):") + for d in sorted(expected - actual): + lines.append(f' {d}') + raise AssertionError("\n".join(lines)) diff --git a/src/gen/_layers/__init__.py b/src/gen/_layers/__init__.py index c04fb48..63bac79 100644 --- a/src/gen/_layers/__init__.py +++ b/src/gen/_layers/__init__.py @@ -1,19 +1,14 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - """Works with circuits in a layered representation that's easy to operate on. """ from gen._layers._transpile import ( transpile_to_z_basis_interaction_circuit, ) +from gen._layers._layer_circuit import ( + LayerCircuit, +) +from gen._layers._interact_layer import ( + InteractLayer, +) +from gen._layers._reset_layer import ( + ResetLayer, +) diff --git a/src/gen/_layers/_data.py b/src/gen/_layers/_data.py index c70b1fb..e4739b6 100644 --- a/src/gen/_layers/_data.py +++ b/src/gen/_layers/_data.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from typing import Literal import numpy as np diff --git a/src/gen/_layers/_det_obs_annotation_layer.py b/src/gen/_layers/_det_obs_annotation_layer.py index 40302c7..bce1439 100644 --- a/src/gen/_layers/_det_obs_annotation_layer.py +++ b/src/gen/_layers/_det_obs_annotation_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import stim @@ -23,6 +9,15 @@ class DetObsAnnotationLayer(Layer): circuit: stim.Circuit = dataclasses.field(default_factory=stim.Circuit) + def with_rec_targets_shifted_by(self, shift: int) -> 'DetObsAnnotationLayer': + result = DetObsAnnotationLayer() + for inst in self.circuit: + result.circuit.append(inst.name, [ + stim.target_rec(t.value + shift) + for t in inst.targets_copy() + ], inst.gate_args_copy()) + return result + def copy(self) -> "DetObsAnnotationLayer": return DetObsAnnotationLayer(circuit=self.circuit.copy()) diff --git a/src/gen/_layers/_empty_layer.py b/src/gen/_layers/_empty_layer.py index 462fd89..0a84994 100644 --- a/src/gen/_layers/_empty_layer.py +++ b/src/gen/_layers/_empty_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import stim diff --git a/src/gen/_layers/_feedback_layer.py b/src/gen/_layers/_feedback_layer.py index 987de46..567f5de 100644 --- a/src/gen/_layers/_feedback_layer.py +++ b/src/gen/_layers/_feedback_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses from typing import TYPE_CHECKING, Literal @@ -30,6 +16,11 @@ class FeedbackLayer(Layer): targets: list[int] = dataclasses.field(default_factory=list) bases: list[Literal["X", "Y", "Z"]] = dataclasses.field(default_factory=list) + def with_rec_targets_shifted_by(self, shift: int) -> 'FeedbackLayer': + result = self.copy() + result.controls = [stim.target_rec(t.value + shift) for t in result.controls] + return result + def copy(self) -> "FeedbackLayer": return FeedbackLayer( targets=list(self.targets), diff --git a/src/gen/_layers/_feedback_layer_test.py b/src/gen/_layers/_feedback_layer_test.py index d99e0f0..19617c4 100644 --- a/src/gen/_layers/_feedback_layer_test.py +++ b/src/gen/_layers/_feedback_layer_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim from gen._layers._data import R_ZXY diff --git a/src/gen/_layers/_interact_layer.py b/src/gen/_layers/_interact_layer.py index 4e4c89d..fa1f5d1 100644 --- a/src/gen/_layers/_interact_layer.py +++ b/src/gen/_layers/_interact_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import collections import dataclasses @@ -90,4 +76,13 @@ def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: return [ InteractSwapLayer(i_layer=self.copy(), swap_layer=next_layer.copy()) ] + elif isinstance(next_layer, InteractLayer) and self.touched().isdisjoint(next_layer.touched()): + return [ + InteractLayer( + targets1=self.targets1 + next_layer.targets1, + targets2=self.targets2 + next_layer.targets2, + bases1=self.bases1 + next_layer.bases1, + bases2=self.bases2 + next_layer.bases2, + ) + ] return [self, next_layer] diff --git a/src/gen/_layers/_interact_swap_layer.py b/src/gen/_layers/_interact_swap_layer.py index 63d2fb3..ce890c7 100644 --- a/src/gen/_layers/_interact_swap_layer.py +++ b/src/gen/_layers/_interact_swap_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import stim diff --git a/src/gen/_layers/_iswap_layer.py b/src/gen/_layers/_iswap_layer.py index e98f039..4b81bcc 100644 --- a/src/gen/_layers/_iswap_layer.py +++ b/src/gen/_layers/_iswap_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import stim diff --git a/src/gen/_layers/_layer.py b/src/gen/_layers/_layer.py index 7898fd8..4ffe89e 100644 --- a/src/gen/_layers/_layer.py +++ b/src/gen/_layers/_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - from typing import Optional import stim diff --git a/src/gen/_layers/_layer_circuit.py b/src/gen/_layers/_layer_circuit.py index 1b98fff..6f56dfe 100644 --- a/src/gen/_layers/_layer_circuit.py +++ b/src/gen/_layers/_layer_circuit.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses from typing import TypeVar, Type, cast, Literal @@ -566,6 +552,138 @@ def scan(qubit: int, start_layer: int) -> int | None: cur_layer_index += 1 return LayerCircuit([layer for layer in new_layers if not layer.is_vacuous()]) + def with_whole_rotation_layers_slid_earlier(self) -> "LayerCircuit": + rev_layers = [] + cur_rot_layer: RotationLayer | None = None + cur_rot_touched: set[int] | None = None + for layer in self.layers[::-1]: + if cur_rot_layer is not None and not layer.touched().isdisjoint(cur_rot_touched): + rev_layers.append(cur_rot_layer) + cur_rot_layer = None + cur_rot_touched = None + if isinstance(layer, RotationLayer): + layer = layer.copy() + if cur_rot_layer is not None: + layer.rotations.update(cur_rot_layer.rotations) + cur_rot_layer = layer + cur_rot_touched = cur_rot_layer.touched() + else: + rev_layers.append(layer) + if cur_rot_layer is not None: + rev_layers.append(cur_rot_layer) + return LayerCircuit(rev_layers[::-1]) + + def with_ejected_loop_iterations(self) -> "LayerCircuit": + new_layers = [] + for layer in self.layers: + if isinstance(layer, LoopLayer): + if layer.repetitions == 0: + pass + elif layer.repetitions == 1: + new_layers.extend(layer.body.layers) + elif layer.repetitions == 2: + new_layers.extend(layer.body.layers) + new_layers.extend(layer.body.layers) + else: + new_layers.extend(layer.body.layers) + new_layers.append(LoopLayer(body=layer.body.copy(), repetitions=layer.repetitions - 2)) + new_layers.extend(layer.body.layers) + assert layer.repetitions > 2 + else: + new_layers.append(layer) + return LayerCircuit(new_layers) + + def without_empty_layers(self) -> "LayerCircuit": + new_layers = [] + for layer in self.layers: + if isinstance(layer, EmptyLayer): + pass + elif isinstance(layer, LoopLayer): + new_layers.append(LoopLayer(layer.body.without_empty_layers(), layer.repetitions)) + else: + new_layers.append(layer) + return LayerCircuit(new_layers) + + def with_cleaned_up_loop_iterations(self) -> "LayerCircuit": + new_layers = list(self.without_empty_layers().layers) + k = 0 + while k < len(new_layers): + if isinstance(new_layers[k], LoopLayer): + body_layers = new_layers[k].body.layers + reps = new_layers[k].repetitions + while k >= len(body_layers) and new_layers[k - len(body_layers):k] == body_layers: + new_layers[k - len(body_layers):k] = [] + k -= len(body_layers) + reps += 1 + while k + len(body_layers) < len(new_layers) and new_layers[k + 1:k + 1 + len(body_layers)] == body_layers: + new_layers[k + 1:k + 1 + len(body_layers)] = [] + reps += 1 + new_layers[k] = LoopLayer(LayerCircuit(body_layers), reps) + k += 1 + return LayerCircuit(new_layers) + + def with_whole_measurement_layers_slid_earlier(self) -> "LayerCircuit": + rev_layers = [] + cur_meas_layer: MeasureLayer | None = None + cur_meas_touched: set[int] | None = None + for layer in self.layers[::-1]: + if cur_meas_layer is not None and not layer.touched().isdisjoint(cur_meas_touched): + rev_layers.append(cur_meas_layer) + cur_meas_layer = None + cur_meas_touched = None + + if cur_meas_layer is not None and isinstance(layer, (FeedbackLayer, DetObsAnnotationLayer)): + layer = layer.with_rec_targets_shifted_by(-len(cur_meas_layer.targets)) + + if isinstance(layer, MeasureLayer): + layer = layer.copy() + if cur_meas_layer is not None: + layer.bases.extend(cur_meas_layer.bases) + layer.targets.extend(cur_meas_layer.targets) + cur_meas_layer = layer + cur_meas_touched = cur_meas_layer.touched() + else: + rev_layers.append(layer) + if cur_meas_layer is not None: + rev_layers.append(cur_meas_layer) + return LayerCircuit(rev_layers[::-1]) + + def with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type(self, layer_types: type | tuple[type, ...]) -> "LayerCircuit": + new_layers = list(self.layers) + k = 0 + while k < len(new_layers): + if isinstance(new_layers[k], layer_types): + touched = new_layers[k].touched() + k_prev = k + while k_prev > 0 and new_layers[k_prev - 1].touched().isdisjoint(touched): + k_prev -= 1 + if k_prev != k and type(new_layers[k_prev]) == type(new_layers[k]): + new_layer, = [e for e in new_layers[k_prev].locally_optimized(new_layers[k]) if e is not None] + del new_layers[k] + new_layers[k_prev] = new_layer + break + k += 1 + return LayerCircuit(new_layers) + + def with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(self, layer_types: type | tuple[type, ...]) -> "LayerCircuit": + new_layers = list(self.layers) + k = 0 + while k < len(new_layers): + if isinstance(new_layers[k], layer_types): + touched = new_layers[k].touched() + k_prev = k + while k_prev > 0 and new_layers[k_prev - 1].touched().isdisjoint(touched): + k_prev -= 1 + while k_prev < k and type(new_layers[k_prev]) != type(new_layers[k]): + k_prev += 1 + if k_prev != k: + new_layer, = [e for e in new_layers[k_prev].locally_optimized(new_layers[k]) if e is not None] + del new_layers[k] + new_layers[k_prev] = new_layer + continue + k += 1 + return LayerCircuit(new_layers) + def with_irrelevant_tail_layers_removed(self) -> "LayerCircuit": irrelevant_layer_types_at_end = ( ResetLayer, diff --git a/src/gen/_layers/_layer_circuit_test.py b/src/gen/_layers/_layer_circuit_test.py index 71be4e4..5c69467 100644 --- a/src/gen/_layers/_layer_circuit_test.py +++ b/src/gen/_layers/_layer_circuit_test.py @@ -1,20 +1,8 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim +from gen._layers._interact_layer import InteractLayer from gen._layers._layer_circuit import LayerCircuit +from gen._layers._reset_layer import ResetLayer def test_with_squashed_rotations(): @@ -352,3 +340,318 @@ def test_merge_resets_and_measurements(): """ ) ) + + +def test_swap_cancellation(): + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + SWAP 0 1 2 3 4 5 + TICK + SWAP 2 3 4 6 7 8 + """)).with_locally_optimized_layers() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + SWAP 0 1 4 5 7 8 + TICK + SWAP 4 6 + """)) + + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + SWAP 0 1 2 3 4 5 + TICK + SWAP 2 3 + """)).with_locally_optimized_layers() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + SWAP 0 1 4 5 + """)) + + +def test_with_rotation_layers_moved_earlier(): + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + CX 0 1 + TICK + SWAP 2 3 + TICK + H 1 4 5 6 + """)).with_whole_rotation_layers_slid_earlier() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + CX 0 1 + TICK + H 1 4 5 6 + TICK + SWAP 2 3 + """)) + + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + CX 0 1 + TICK + S 7 + TICK + SWAP 2 3 + TICK + H 1 4 5 6 + """)).with_whole_rotation_layers_slid_earlier() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + CX 0 1 + TICK + S 7 + H 1 4 5 6 + TICK + SWAP 2 3 + """)) + + +def test_with_whole_measurement_layers_slid_earlier(): + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + M 0 1 + CX rec[-2] 5 + DETECTOR rec[-1] + TICK + M 2 3 + """)).with_whole_measurement_layers_slid_earlier() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + M 0 1 2 3 + CX rec[-4] 5 + DETECTOR rec[-3] + """)) + + +def test_with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(): + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + + R 0 1 + TICK + + CX 0 1 + TICK + + CX 1 0 + TICK + + CX 0 1 + TICK + + M 5 + DETECTOR rec[-1] + TICK + + R 2 3 + TICK + + CX 2 3 + TICK + + CX 3 4 + """)).with_whole_layers_slid_as_to_merge_with_previous_layer_of_same_type(ResetLayer).with_whole_layers_slid_as_early_as_possible_for_merge_with_same_layer(InteractLayer) == LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + + R 0 1 2 3 + TICK + + CX 0 1 2 3 + TICK + + CX 1 0 3 4 + TICK + + CX 0 1 + TICK + + M 5 + DETECTOR rec[-1] + """)) + + +def test_with_cleaned_up_loop_iterations(): + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + """)).with_cleaned_up_loop_iterations() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + REPEAT 6 { + R 0 1 + TICK + S 2 + TICK + } + """)).without_empty_layers() + + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + H 1 + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + """)).with_cleaned_up_loop_iterations() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + H 1 + + REPEAT 6 { + R 0 1 + TICK + S 2 + TICK + } + """)).without_empty_layers() + + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + S 2 + TICK + + H 1 + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + """)).with_cleaned_up_loop_iterations() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + S 2 + TICK + + H 1 + + REPEAT 7 { + R 0 1 + TICK + S 2 + TICK + } + """)).without_empty_layers() + + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + S 2 + TICK + + H 1 + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + + R 0 1 + TICK + S 2 + TICK + """)).with_cleaned_up_loop_iterations() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + S 2 + TICK + + H 1 + + REPEAT 8 { + R 0 1 + TICK + S 2 + TICK + } + """)).without_empty_layers() + + assert LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + S 2 + TICK + + H 1 + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + REPEAT 5 { + R 0 1 + TICK + S 2 + TICK + } + + R 0 1 + TICK + S 2 + TICK + + R 0 1 + TICK + S 2 + TICK + + H 0 + TICK + + R 0 1 + TICK + S 2 + TICK + """)).with_cleaned_up_loop_iterations() == LayerCircuit.from_stim_circuit(stim.Circuit(""" + R 0 1 + TICK + S 2 + TICK + + H 1 + + REPEAT 9 { + R 0 1 + TICK + S 2 + TICK + } + + H 0 + TICK + + R 0 1 + TICK + S 2 + TICK + """)).without_empty_layers() diff --git a/src/gen/_layers/_loop_layer.py b/src/gen/_layers/_loop_layer.py index b632b19..152fd5f 100644 --- a/src/gen/_layers/_loop_layer.py +++ b/src/gen/_layers/_loop_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses from typing import TYPE_CHECKING diff --git a/src/gen/_layers/_measure_layer.py b/src/gen/_layers/_measure_layer.py index b4e3863..09e9d15 100644 --- a/src/gen/_layers/_measure_layer.py +++ b/src/gen/_layers/_measure_layer.py @@ -1,18 +1,5 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses +from typing import Optional, Set import stim @@ -59,4 +46,8 @@ def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: bases=self.bases + next_layer.bases, ) ] + if isinstance(next_layer, RotationLayer) and set(self.targets).isdisjoint( + next_layer.rotations + ): + return [next_layer, self] return [self, next_layer] diff --git a/src/gen/_layers/_mpp_layer.py b/src/gen/_layers/_mpp_layer.py index 7eb4b26..2f7d129 100644 --- a/src/gen/_layers/_mpp_layer.py +++ b/src/gen/_layers/_mpp_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import stim @@ -31,27 +17,8 @@ def copy(self) -> "MppLayer": def touched(self) -> set[int]: return set(t.value for mpp in self.targets for t in mpp) - def to_z_basis(self) -> list["Layer"]: - assert len(self.touched()) == sum(len(e) for e in self.targets) - rot = RotationLayer() - new_targets: list[list[stim.GateTarget]] = [] - for groups in self.targets: - new_group: list[stim.GateTarget] = [] - for t in groups: - new_group.append(stim.target_z(t.value)) - if t.is_x_target: - rot.append_rotation(R_ZYX, t.value) - elif t.is_y_target: - rot.append_rotation(R_XZY, t.value) - elif not t.is_z_target: - raise NotImplementedError(f"{t=}") - new_targets.append(new_group) - - return [ - rot, - MppLayer(targets=new_targets), - rot.copy(), - ] + def to_z_basis(self) -> list[Layer]: + return [self] def append_into_stim_circuit(self, out: stim.Circuit) -> None: flat_targets = [] diff --git a/src/gen/_layers/_noise_layer.py b/src/gen/_layers/_noise_layer.py index 3ff2798..1a0b5a2 100644 --- a/src/gen/_layers/_noise_layer.py +++ b/src/gen/_layers/_noise_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import stim diff --git a/src/gen/_layers/_qubit_coord_annotation_layer.py b/src/gen/_layers/_qubit_coord_annotation_layer.py index 0c2ff71..0300060 100644 --- a/src/gen/_layers/_qubit_coord_annotation_layer.py +++ b/src/gen/_layers/_qubit_coord_annotation_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses from typing import Iterable diff --git a/src/gen/_layers/_reset_layer.py b/src/gen/_layers/_reset_layer.py index 83a3442..b2be8d0 100644 --- a/src/gen/_layers/_reset_layer.py +++ b/src/gen/_layers/_reset_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses from typing import Literal diff --git a/src/gen/_layers/_rotation_layer.py b/src/gen/_layers/_rotation_layer.py index 42e094c..e5f5a18 100644 --- a/src/gen/_layers/_rotation_layer.py +++ b/src/gen/_layers/_rotation_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import sinter diff --git a/src/gen/_layers/_shift_coord_annotation_layer.py b/src/gen/_layers/_shift_coord_annotation_layer.py index f7b6504..afeae62 100644 --- a/src/gen/_layers/_shift_coord_annotation_layer.py +++ b/src/gen/_layers/_shift_coord_annotation_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses from typing import Iterable diff --git a/src/gen/_layers/_sqrt_pp_layer.py b/src/gen/_layers/_sqrt_pp_layer.py index fbb2885..4cae748 100644 --- a/src/gen/_layers/_sqrt_pp_layer.py +++ b/src/gen/_layers/_sqrt_pp_layer.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import collections import dataclasses diff --git a/src/gen/_layers/_swap_layer.py b/src/gen/_layers/_swap_layer.py index 7dc1e9c..2bc9d8e 100644 --- a/src/gen/_layers/_swap_layer.py +++ b/src/gen/_layers/_swap_layer.py @@ -1,22 +1,10 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import dataclasses import stim from gen._layers._interact_layer import InteractLayer +from gen._layers._det_obs_annotation_layer import DetObsAnnotationLayer +from gen._layers._shift_coord_annotation_layer import ShiftCoordAnnotationLayer from gen._layers._layer import Layer @@ -28,6 +16,13 @@ class SwapLayer(Layer): def touched(self) -> set[int]: return set(self.targets1 + self.targets2) + def to_swap_dict(self) -> dict[int, int]: + d = {} + for a, b in zip(self.targets1, self.targets2): + d[a] = b + d[b] = a + return d + def copy(self) -> "SwapLayer": return SwapLayer(targets1=list(self.targets1), targets2=list(self.targets2)) @@ -48,4 +43,32 @@ def locally_optimized(self, next_layer: Layer | None) -> list[Layer | None]: i = next_layer.copy() i.targets1, i.targets2 = i.targets2, i.targets1 return [InteractSwapLayer(i_layer=i, swap_layer=self.copy())] + if isinstance(next_layer, (ShiftCoordAnnotationLayer, DetObsAnnotationLayer)): + return [next_layer, self] + if isinstance(next_layer, SwapLayer): + total_swaps = self.to_swap_dict() + leftover_swaps = SwapLayer() + for a, b in zip(next_layer.targets1, next_layer.targets2): + a2 = total_swaps.get(a) + b2 = total_swaps.get(b) + if a2 is None and b2 is None: + total_swaps[a] = b + total_swaps[b] = a + elif a2 == b and b2 == a: + del total_swaps[a] + del total_swaps[b] + else: + leftover_swaps.targets1.append(a) + leftover_swaps.targets2.append(b) + result = [] + if total_swaps: + new_layer = SwapLayer() + for k, v in total_swaps.items(): + if k < v: + new_layer.targets1.append(k) + new_layer.targets2.append(v) + result.append(new_layer) + if leftover_swaps.targets1: + result.append(leftover_swaps) + return result return [self, next_layer] diff --git a/src/gen/_layers/_transpile.py b/src/gen/_layers/_transpile.py index 1b9fe1c..2ed7291 100644 --- a/src/gen/_layers/_transpile.py +++ b/src/gen/_layers/_transpile.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim from gen._layers._layer_circuit import LayerCircuit diff --git a/src/gen/_layers/_transpile_test.py b/src/gen/_layers/_transpile_test.py index fd3511b..0794d8f 100644 --- a/src/gen/_layers/_transpile_test.py +++ b/src/gen/_layers/_transpile_test.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim from gen import transpile_to_z_basis_interaction_circuit diff --git a/src/gen/_surf/__init__.py b/src/gen/_surf/__init__.py deleted file mode 100644 index 1b5c7c0..0000000 --- a/src/gen/_surf/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from gen._surf._closed_curve import ( - ClosedCurve, -) -from gen._surf._css_observable_boundary_pair import ( - CssObservableBoundaryPair, -) -from gen._surf._geo import ( - int_points_on_line, - int_points_inside_polygon, -) -from gen._surf._order import ( - checkerboard_basis, - Order_Z, - Order_ᴎ, - Order_N, - Order_S, -) -from gen._surf._patch_outline import ( - PatchOutline, -) -from gen._surf._step_sequence_outline import ( - StepSequenceOutline, - StepOutline, -) -from gen._surf._patch_transition_outline import ( - PatchTransitionOutline, -) -from gen._surf._path_outline import ( - PathOutline, -) -from gen._surf._surface_code import ( - layer_begin, - layer_loop, - layer_transition, - layer_end, - layer_single_shot, - surface_code_patch, -) -from gen._surf._trans import ( - build_patch_to_patch_surface_code_transition_rounds, -) diff --git a/src/gen/_surf/_closed_curve.py b/src/gen/_surf/_closed_curve.py deleted file mode 100644 index b8b23f7..0000000 --- a/src/gen/_surf/_closed_curve.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pathlib -from typing import Iterable, Literal, Callable, TYPE_CHECKING - -from gen._core._patch import Patch -from gen._surf._geo import ( - int_travel_points_on_polygon_boundary, - int_points_on_line, - int_points_inside_polygon_set, - half_int_points_inside_int_polygon_set, -) -from gen._core._util import min_max_complex - -if TYPE_CHECKING: - import gen - - -class ClosedCurve: - """A closed series of line segments between integer coordinates aligned at 45 degree angles. - - Each line segment has an associated basis (X or Z). - """ - - def __init__( - self, *, points: Iterable[complex] = (), bases: Iterable[Literal["X", "Z"]] = () - ): - self.points = tuple(points) - self.bases = tuple(bases) - - def to_patch_outline(self) -> "gen.PatchOutline": - from gen._surf._patch_outline import PatchOutline - - return PatchOutline([self]) - - def to_patch( - self, - *, - rel_order_func: Callable[[complex], Iterable[complex]], - ) -> Patch: - return self.to_patch_outline().to_patch(rel_order_func=rel_order_func) - - def write_svg( - self, - path: str | pathlib.Path, - ) -> None: - return self.to_patch_outline().write_svg(path) - - def with_basis(self, basis: Literal["X", "Z"]) -> "ClosedCurve": - return ClosedCurve(points=self.points, bases=[basis] * len(self.bases)) - - @staticmethod - def from_cycle(point_or_basis: Iterable[Literal["X", "Z"] | complex | int | float]): - point_or_basis = list(point_or_basis) - cur_basis: Literal["X", "Z", "U"] = "U" - cur_basis_used = True - points = [] - bases: list[Literal["X", "Z", "U"]] = [] - for e in point_or_basis: - if e == "X" or e == "Z": - if not cur_basis_used: - assert cur_basis == e, "Basis disagreement" - cur_basis = e - cur_basis_used = False - elif isinstance(e, (int, float, complex)): - cur_basis_used = True - if points and points[-1] == e: - continue - points.append(e) - bases.append(cur_basis) - else: - raise NotImplementedError(f"{e=}") - assert cur_basis != "U" - if not cur_basis_used: - assert bases[0] == "U" or bases[0] == cur_basis - - for k in range(len(bases)): - if bases[k] == "U": - bases[k] = cur_basis - while points[-1] == points[0]: - points.pop() - bases.pop() - - return ClosedCurve(points=points, bases=bases) - - def to_cycle(self) -> list[Literal["X", "Z"] | complex]: - out = [] - for k in range(len(self.points)): - out.append(self.bases[k]) - out.append(self.points[k]) - out.append(self.bases[0]) - return out - - def med(self) -> complex: - """Returns the center of the axis-aligned bounding box of the curve.""" - a, b = min_max_complex(self.points) - return (a.real + b.real) // 2 + (a.imag + b.imag) // 2 * 1j - - def __len__(self): - """Returns the number of line segments making up the curve.""" - return len(self.points) - - def __getitem__(self, item: int) -> tuple[str, complex, complex]: - assert isinstance(item, int) - return self.bases[item], self.points[item - 1], self.points[item] - - def segment_indices_intersecting(self, points: set[complex]) -> list[int]: - """Returns the indices of line segments intersecting the given point set.""" - hits = [] - for k in range(len(self.points)): - a = self.points[k - 1] - b = self.points[k] - if any(e in points for e in int_points_on_line(a, b)): - hits.append(k) - return hits - - def offset_by(self, offset: complex) -> "ClosedCurve": - """Translates the curve's location by translating all of its line segments.""" - return ClosedCurve(points=[p + offset for p in self.points], bases=self.bases) - - def interior_set( - self, *, include_boundary: bool, half_ints: bool = False - ) -> frozenset[complex]: - if half_ints: - return frozenset( - half_int_points_inside_int_polygon_set( - curves=[self.points], include_boundary=include_boundary - ) - ) - return frozenset( - int_points_inside_polygon_set( - [self.points], include_boundary=include_boundary - ) - ) - - def boundary_set(self) -> set[complex]: - """Returns the set of integer coordinates along the line segments of the curve.""" - return set(int_travel_points_on_polygon_boundary(self.points)) - - def boundary_travel(self) -> list[complex]: - """Lists the integer coordinates along the line segments of the curve, in order.""" - return int_travel_points_on_polygon_boundary(self.points) - - def with_transformed_coords( - self, coord_transform: Callable[[complex], complex] - ) -> "ClosedCurve": - return ClosedCurve( - points=[coord_transform(e) for e in self.points], - bases=self.bases, - ) - - @property - def basis(self) -> Literal["X", "Z"] | None: - b = set(self.bases) - if len(b) == 1: - return next(iter(b)) - return None - - def __eq__(self, other): - if not isinstance(other, ClosedCurve): - return NotImplemented - return self.points == other.points and self.bases == other.bases - - def __ne__(self, other): - return not (self == other) - - def __repr__(self): - return f"ClosedCurve.from_cycle({self.to_cycle()!r})" diff --git a/src/gen/_surf/_closed_curve_test.py b/src/gen/_surf/_closed_curve_test.py deleted file mode 100644 index c02a1c8..0000000 --- a/src/gen/_surf/_closed_curve_test.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from gen._surf._closed_curve import ClosedCurve - - -def test_to_from_cycle(): - c1 = ClosedCurve(points=[(5 + 0j), (5 + 3j), 3j, 0j], bases=["Z", "X", "Z", "X"]) - assert c1.to_cycle() == ["Z", (5 + 0j), "X", (5 + 3j), "Z", 3j, "X", 0j, "Z"] - assert ( - ClosedCurve.from_cycle(["Z", (5 + 0j), "X", (5 + 3j), "Z", 3j, "X", 0j, "Z"]) - == c1 - ) - assert ( - ClosedCurve.from_cycle(["Z", (5 + 0j), "X", (5 + 3j), "Z", 3j, "X", 0j]) == c1 - ) - assert ( - ClosedCurve.from_cycle([(5 + 0j), "X", (5 + 3j), "Z", 3j, "X", 0j, "Z"]) == c1 - ) - assert ( - ClosedCurve.from_cycle([(5 + 0j), "X", (5 + 3j), "Z", 3j, "X", 0j, "Z", 5]) - == c1 - ) - - c2 = ClosedCurve(points=[(5 + 0j), (5 + 3j), 3j, 0j], bases=["Z", "Z", "Z", "X"]) - assert ( - ClosedCurve.from_cycle(["Z", (5 + 0j), "Z", (5 + 3j), "Z", 3j, "X", 0j, "Z"]) - == c2 - ) - assert ( - ClosedCurve.from_cycle( - ["Z", (5 + 0j), 5, 5, "X", 5, "Z", 5, "Z", (5 + 3j), "Z", 3j, "X", 0j, "Z"] - ) - == c2 - ) - assert ( - ClosedCurve.from_cycle(["Z", (5 + 0j), (5 + 3j), "Z", 3j, "X", 0j, "Z"]) == c2 - ) - assert ( - ClosedCurve.from_cycle(["Z", (5 + 0j), "Z", (5 + 3j), 3j, "X", 0j, "Z"]) == c2 - ) - assert ( - ClosedCurve.from_cycle(["Z", (5 + 0j), (5 + 3j), "Z", 3j, "X", 0j, "Z"]) == c2 - ) - assert ClosedCurve.from_cycle([(5 + 0j), (5 + 3j), "Z", 3j, "X", 0j, "Z"]) == c2 diff --git a/src/gen/_surf/_css_observable_boundary_pair.py b/src/gen/_surf/_css_observable_boundary_pair.py deleted file mode 100644 index f07f5af..0000000 --- a/src/gen/_surf/_css_observable_boundary_pair.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Iterator - -from gen._surf._path_outline import PathOutline - - -class CssObservableBoundaryPair: - def __init__(self, *, x_obs: PathOutline, z_obs: PathOutline): - self.x_obs = x_obs - self.z_obs = z_obs - - def __iter__(self) -> Iterator[tuple[str, PathOutline]]: - yield "X", self.x_obs - yield "Z", self.z_obs diff --git a/src/gen/_surf/_geo.py b/src/gen/_surf/_geo.py deleted file mode 100644 index d7daf4f..0000000 --- a/src/gen/_surf/_geo.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import collections -from typing import Iterable, Sequence - -from gen._core._util import min_max_complex - - -def int_points_on_line(a: complex, b: complex) -> list[complex]: - """Lists the integer points along a given line segment. - - The delta `a-b` must be horizontal (imaginary part is 0), vertical - (real part is 0), or diagonal (real and imaginary parts have same - absolute magnitude). - - Args: - a: A complex number with integer real part and integer imaginary part. - b: A complex number with integer real part and integer imaginary part. - - Returns: - The list of integer points. - """ - dr = b.real - a.real - di = b.imag - a.imag - if a == b: - return [a] - if len({0, abs(di), abs(dr)}) != 2: - raise NotImplementedError( - f"{a=} to {b=} isn't horizontal, vertical, or 1:1 diagonal." - ) - steps = int(max(abs(dr), abs(di))) - result = [] - dr /= steps - di /= steps - assert int(dr) == dr - assert int(di) == di - dr = int(dr) - di = int(di) - for k in range(steps + 1): - result.append(a + dr * k + di * k * 1j) - return result - - -def int_travel_points_on_polygon_boundary(corners: Sequence[complex]) -> list[complex]: - boundary = [] - for k in range(len(corners)): - a = corners[k - 1] - b = corners[k] - for p in int_points_on_line(a, b): - if len(boundary) == 0 or boundary[-1] != p: - boundary.append(p) - assert boundary[-1] == boundary[0] - boundary.pop() - return boundary - - -def half_int_points_inside_int_polygon_set( - *, - curves: Sequence[Sequence[complex]], - include_boundary: bool, - match_mask: int | None = None, -) -> set[complex]: - curves2 = [[pt * 2 for pt in curve] for curve in curves] - interior2 = int_points_inside_polygon_set( - curves2, include_boundary=include_boundary, match_mask=match_mask - ) - return {p / 2 for p in interior2 if p.real % 2 == 1 and p.imag % 2 == 1} - - -def half_int_points_inside_int_polygon( - corners: Sequence[complex], *, include_boundary: bool -) -> set[complex]: - return half_int_points_inside_int_polygon_set( - curves=[corners], include_boundary=include_boundary - ) - - -def int_points_inside_polygon( - corners: Sequence[complex], *, include_boundary: bool -) -> set[complex]: - return int_points_inside_polygon_set([corners], include_boundary=include_boundary) - - -def int_points_inside_polygon_set( - curves: Iterable[Sequence[complex]], - *, - include_boundary: bool, - match_mask: int | None = None, -) -> set[complex]: - curves = tuple(curves) - min_c, max_c = min_max_complex((pt for curve in curves for pt in curve), default=0) - - boundary = set() - half_boundary = collections.defaultdict(int) - for k, curve in enumerate(curves): - boundary |= set(int_travel_points_on_polygon_boundary(curve)) - for p in set(int_travel_points_on_polygon_boundary([p * 2 for p in curve])): - half_boundary[p / 2] ^= 1 << k - - result = set() - for r in range(int(min_c.real), int(max_c.real) + 1): - r0 = r - 0.5 - r1 = r + 0.5 - mask0 = 0 - inside0 = 0 - mask1 = 0 - inside1 = 0 - i = int(min_c.imag) - while i <= max_c.imag: - c0 = r0 + i * 1j - c1 = r1 + i * 1j - inside0 ^= half_boundary[c0] != 0 - inside1 ^= half_boundary[c1] != 0 - mask0 ^= half_boundary[c0] - mask1 ^= half_boundary[c1] - m0 = mask0 if inside0 else 0 - m1 = mask1 if inside1 else 0 - if ( - i == int(i) - and m0 == m1 - and (m0 != 0 if match_mask is None else m0 == match_mask) - ): - result.add(r + i * 1j) - i += 0.5 - - if include_boundary: - result |= boundary - else: - result -= boundary - - return result - - -def int_point_disjoint_regions_inside_polygon_set( - curves: Iterable[Sequence[complex]], *, include_boundary: bool -) -> dict[int, set[complex]]: - curves = tuple(curves) - min_real = int(min(pt.real for curve in curves for pt in curve)) - min_imag = int(min(pt.imag for curve in curves for pt in curve)) - max_real = int(max(pt.real for curve in curves for pt in curve)) - max_imag = int(max(pt.imag for curve in curves for pt in curve)) - - half_boundary = collections.defaultdict(int) - for k, curve in enumerate(curves): - for p in set(int_travel_points_on_polygon_boundary([p * 2 for p in curve])): - half_boundary[p / 2] ^= 1 << k - - result = collections.defaultdict(set) - for r in range(min_real, max_real + 1): - r0 = r - 0.5 - r1 = r + 0.5 - - mask0 = 0 - mask1 = 0 - inside0 = 0 - inside1 = 0 - i = min_imag - while i <= max_imag: - c0 = r0 + i * 1j - c1 = r1 + i * 1j - c = r + i * 1j - b0 = half_boundary[c0] - b1 = half_boundary[c1] - new_inside0 = inside0 ^ (b0 != 0) - new_inside1 = inside1 ^ (b1 != 0) - new_mask0 = mask0 ^ b0 - new_mask1 = mask1 ^ b1 - if i == int(i): - if include_boundary: - # On horizontal segment? - if inside0 and not new_inside0: - result[mask0].add(c) - if not inside0 and new_inside0: - result[new_mask0].add(c) - if inside1 and not new_inside1: - result[mask1].add(c) - if not inside1 and new_inside1: - result[new_mask1].add(c) - - # On vertical segment? - if inside0 and not inside1: - result[mask0].add(c) - if new_inside0 and not new_inside1: - result[new_mask0].add(c) - if inside1 and not inside0: - result[mask1].add(c) - if new_inside1 and not new_inside0: - result[new_mask1].add(c) - if ( - inside0 - and inside1 - and new_inside0 - and new_inside1 - and mask0 == mask1 == new_mask0 == new_mask1 - ): - # Interior. - result[mask0].add(c) - mask0 = new_mask0 - mask1 = new_mask1 - inside0 = new_inside0 - inside1 = new_inside1 - i += 0.5 - - if 0 in result: - del result[0] - - return dict(result) diff --git a/src/gen/_surf/_geo_test.py b/src/gen/_surf/_geo_test.py deleted file mode 100644 index 93f5363..0000000 --- a/src/gen/_surf/_geo_test.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from gen._surf._geo import ( - int_points_on_line, - int_points_inside_polygon, - half_int_points_inside_int_polygon, - int_point_disjoint_regions_inside_polygon_set, -) -from gen._core._util import sorted_complex - - -def test_int_points_on_line(): - with pytest.raises(NotImplementedError): - int_points_on_line(0, 1 + 2j) - - assert int_points_on_line(0, 0) == [0] - assert int_points_on_line(0, 5) == [0, 1, 2, 3, 4, 5] - assert int_points_on_line(1, -3) == [1, 0, -1, -2, -3] - assert int_points_on_line(1j, 3j) == [1j, 2j, 3j] - assert int_points_on_line(0, -2j) == [0, -1j, -2j] - assert int_points_on_line(5, 8 + 3j) == [5, 6 + 1j, 7 + 2j, 8 + 3j] - assert int_points_on_line(5, 8 - 3j) == [5, 6 - 1j, 7 - 2j, 8 - 3j] - assert int_points_on_line(5, 2 + 3j) == [5, 4 + 1j, 3 + 2j, 2 + 3j] - assert int_points_on_line(5, 2 - 3j) == [5, 4 - 1j, 3 - 2j, 2 - 3j] - - -def test_int_points_inside_polygon(): - assert sorted_complex( - int_points_inside_polygon([0, 3, 3 + 2j, 5j], include_boundary=True) - ) == [ - 0 + 0j, - 0 + 1j, - 0 + 2j, - 0 + 3j, - 0 + 4j, - 0 + 5j, - 1 + 0j, - 1 + 1j, - 1 + 2j, - 1 + 3j, - 1 + 4j, - 2 + 0j, - 2 + 1j, - 2 + 2j, - 2 + 3j, - 3 + 0j, - 3 + 1j, - 3 + 2j, - ] - assert sorted_complex( - int_points_inside_polygon([0, 3, 3 + 2j, 5j], include_boundary=False) - ) == [ - 1 + 1j, - 1 + 2j, - 1 + 3j, - 2 + 1j, - 2 + 2j, - ] - - -def test_half_int_points_inside_int_polygon(): - assert sorted_complex( - half_int_points_inside_int_polygon([0, 3, 3 + 2j, 5j], include_boundary=True) - ) == [ - 0.5 + 0.5j, - 0.5 + 1.5j, - 0.5 + 2.5j, - 0.5 + 3.5j, - 0.5 + 4.5j, - 1.5 + 0.5j, - 1.5 + 1.5j, - 1.5 + 2.5j, - 1.5 + 3.5j, - 2.5 + 0.5j, - 2.5 + 1.5j, - 2.5 + 2.5j, - ] - assert sorted_complex( - half_int_points_inside_int_polygon([0, 3, 3 + 2j, 5j], include_boundary=False) - ) == [ - 0.5 + 0.5j, - 0.5 + 1.5j, - 0.5 + 2.5j, - 0.5 + 3.5j, - 1.5 + 0.5j, - 1.5 + 1.5j, - 1.5 + 2.5j, - 2.5 + 0.5j, - 2.5 + 1.5j, - ] - - -def test_int_point_disjoint_regions_inside_polygon_set(): - a = int_point_disjoint_regions_inside_polygon_set( - [ - [0, 3, 3 + 2j, 2j], - [3j, 3 + 3j, 3 + 5j, 5j], - ], - include_boundary=False, - ) - assert len(a) == 2 - assert a[1] == {1 + 1j, 2 + 1j} - assert a[2] == {1 + 4j, 2 + 4j} - - a = int_point_disjoint_regions_inside_polygon_set( - [ - [0, 3, 3 + 2j, 2j], - [3j, 3 + 3j, 3 + 5j, 5j], - ], - include_boundary=True, - ) - assert len(a) == 2 - assert a[1] == { - 0, - 1, - 2, - 3, - 0 + 1j, - 1 + 1j, - 2 + 1j, - 3 + 1j, - 0 + 2j, - 1 + 2j, - 2 + 2j, - 3 + 2j, - } - assert a[2] == { - 0 + 3j, - 1 + 3j, - 2 + 3j, - 3 + 3j, - 0 + 4j, - 1 + 4j, - 2 + 4j, - 3 + 4j, - 0 + 5j, - 1 + 5j, - 2 + 5j, - 3 + 5j, - } diff --git a/src/gen/_surf/_open_curve.py b/src/gen/_surf/_open_curve.py deleted file mode 100644 index 841d8a9..0000000 --- a/src/gen/_surf/_open_curve.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Iterable, Literal - -from gen._surf._geo import int_points_on_line - - -class OpenCurve: - """A contiguous series of line segments between integer coordinates aligned at 45 degree angles. - - Each line segment has an associated basis (X or Z). - """ - - def __init__( - self, *, points: Iterable[complex], bases: Iterable[Literal["X", "Z"]] - ): - self.points = list(points) - self.bases = list(bases) - if len(self.points) < 2: - raise ValueError(f"len({self.points=}) < 2") - if len(self.points) != len(self.bases) + 1: - raise ValueError(f"len({self.points=}) != len({self.bases=}) + 1") - - @staticmethod - def from_sequence( - points_and_bases: Iterable[Literal["X", "Z"] | complex | int | float] - ): - points_and_bases = list(points_and_bases) - - points = [] - bases: list[Literal["X", "Z"]] = [] - - next_basis = None - for e in points_and_bases: - if e == "X" or e == "Z": - next_basis = e - elif isinstance(e, (int, float, complex)): - if points: - if points[-1] == e: - continue - if next_basis is None: - raise ValueError( - f"Specify basis before second point in {points_and_bases=}" - ) - bases.append(next_basis) - points.append(e) - else: - raise NotImplementedError(f"{e=}") - return OpenCurve(points=points, bases=bases) - - def to_sequence(self) -> list[Literal["X", "Z"] | complex]: - out = [] - for k in range(len(self.bases)): - out.append(self.points[k]) - out.append(self.bases[k]) - out.append(self.points[-1]) - return out - - def __len__(self): - """Returns the number of line segments making up the curve.""" - return len(self.points) - - def __getitem__(self, item: int) -> tuple[str, complex, complex]: - assert isinstance(item, int) - return self.bases[item], self.points[item], self.points[item + 1] - - def offset_by(self, offset: complex) -> "OpenCurve": - """Translates the curve's location by translating all of its line segments.""" - return OpenCurve(points=[p + offset for p in self.points], bases=self.bases) - - @property - def basis(self) -> Literal["X", "Z"] | None: - b = set(self.bases) - if len(b) == 1: - return next(iter(b)) - return None - - def int_points_set(self) -> set[complex]: - """Returns the set of integer coordinates along the line segments of the curve.""" - return set(self.int_points_in_order()) - - def int_points_in_order(self) -> list[complex]: - """Lists the integer coordinates along the line segments of the curve, in order.""" - points = [] - for k in range(len(self.bases)): - a = self.points[k] - b = self.points[k + 1] - for p in int_points_on_line(a, b): - if len(points) == 0 or points[-1] != p: - points.append(p) - return points - - def __eq__(self, other): - if not isinstance(other, OpenCurve): - return NotImplemented - return self.points == other.points and self.bases == other.bases - - def __ne__(self, other): - return not (self == other) - - def __repr__(self): - return f"OpenCurve.from_sequence({self.to_sequence()!r})" diff --git a/src/gen/_surf/_open_curve_test.py b/src/gen/_surf/_open_curve_test.py deleted file mode 100644 index bc70e51..0000000 --- a/src/gen/_surf/_open_curve_test.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from gen._surf._open_curve import OpenCurve - - -def test_to_from_sequence(): - c1 = OpenCurve(points=[1, 5, 9 + 4j, 9], bases=["Z", "X", "X"]) - assert c1.to_sequence() == [1, "Z", 5, "X", (9 + 4j), "X", 9] - assert OpenCurve.from_sequence([1, "Z", 5, "X", (9 + 4j), "X", 9]) == c1 - assert OpenCurve.from_sequence(["Z", 1, 5, "X", (9 + 4j), "X", 9]) == c1 - assert OpenCurve.from_sequence([1, "Z", 5, "X", (9 + 4j), 9]) == c1 - assert OpenCurve.from_sequence([1, "Z", 5, "X", "Z", "X", (9 + 4j), 9]) == c1 - assert OpenCurve.from_sequence([1, "Z", 5, "X", 5, "Z", "X", (9 + 4j), 9]) == c1 - assert ( - OpenCurve.from_sequence( - ["Z", "Z", "Z", 1, "Z", 5, "X", 5, "Z", "X", (9 + 4j), 9, "X", "Z"] - ) - == c1 - ) - - with pytest.raises(ValueError, match="len.+points.+< 2"): - OpenCurve.from_sequence([]) - with pytest.raises(ValueError, match="len.+points.+< 2"): - OpenCurve.from_sequence([1]) - with pytest.raises(ValueError, match="len.+points.+< 2"): - OpenCurve.from_sequence([1, "X"]) - with pytest.raises(NotImplementedError): - OpenCurve.from_sequence([1, "Y", 2]) - with pytest.raises(ValueError, match="len.+points.+< 2"): - OpenCurve.from_sequence([1, "X", "X"]) - - -def test_iter(): - c1 = OpenCurve(points=[1, 5, 9 + 4j, 9], bases=["Z", "X", "X"]) - assert list(c1) == [ - ("Z", 1, 5), - ("X", 5, (9 + 4j)), - ("X", (9 + 4j), 9), - ] - - -def test_int_points_in_order(): - c1 = OpenCurve(points=[1, 5, 9 + 4j, 9], bases=["Z", "X", "X"]) - assert c1.int_points_in_order() == [ - 1 + 0j, - 2 + 0j, - 3 + 0j, - 4 + 0j, - 5 + 0j, - 6 + 1j, - 7 + 2j, - 8 + 3j, - 9 + 4j, - 9 + 3j, - 9 + 2j, - 9 + 1j, - 9 + 0j, - ] diff --git a/src/gen/_surf/_order.py b/src/gen/_surf/_order.py deleted file mode 100644 index f9133e6..0000000 --- a/src/gen/_surf/_order.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -UL, UR, DL, DR = [e * 0.5 for e in [-1 - 1j, +1 - 1j, -1 + 1j, +1 + 1j]] -Order_Z = [UL, UR, DL, DR] -Order_ᴎ = [UL, DL, UR, DR] -Order_N = [DL, UL, DR, UR] -Order_S = [DL, DR, UL, UR] - - -def checkerboard_basis(q: complex) -> str: - """Classifies a coordinate as X type or Z type according to a checkerboard.""" - is_x = int(q.real + q.imag) & 1 == 0 - return "X" if is_x else "Z" diff --git a/src/gen/_surf/_patch_outline.py b/src/gen/_surf/_patch_outline.py deleted file mode 100644 index 8862d09..0000000 --- a/src/gen/_surf/_patch_outline.py +++ /dev/null @@ -1,353 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import collections -import functools -import pathlib -from typing import Iterable, Callable, Union, Literal, TYPE_CHECKING - -from gen._core import Patch, Tile, sorted_complex -from gen._surf._closed_curve import ClosedCurve -from gen._surf._css_observable_boundary_pair import CssObservableBoundaryPair -from gen._surf._geo import ( - int_points_on_line, - int_points_inside_polygon_set, - half_int_points_inside_int_polygon_set, - int_point_disjoint_regions_inside_polygon_set, -) -from gen._surf._order import Order_Z, checkerboard_basis -from gen._util import write_file - -if TYPE_CHECKING: - from gen._surf._patch_transition_outline import PatchTransitionOutline - - -class PatchOutline: - """Defines a surface code stabilizer configuration in terms of its boundaries.""" - - def __init__( - self, - region_curves: Iterable[ClosedCurve], - *, - observables: Iterable[CssObservableBoundaryPair] = (), - ): - """ - Args: - region_curves: The curves defining the boundaries of the patch. - These curves should not intersect each other, but may be inside - each other, creating interior voids or nested islands. - """ - self.region_curves = tuple(region_curves) - self.observables = tuple(observables) - - def iter_control_points(self) -> Iterable[complex]: - for curve in self.region_curves: - yield from curve.points - - def validate(self): - if not self.observables: - return - boundary_xs = self.x_boundary_set - boundary_zs = self.z_boundary_set - - for i in range(len(self.observables)): - for j in range(len(self.observables)): - pts_x = self.observables[i].x_obs.int_point_set - pts_z = self.observables[j].z_obs.int_point_set - does_commute = len(pts_x & pts_z) % 2 == 0 - should_commute = i != j - if does_commute != should_commute: - raise ValueError( - f"Bad commutation relationship between X{i} and Z{j}: {pts_x=} vs {pts_z=}" - ) - - for obs_pair in self.observables: - for basis, obs in obs_pair: - same_boundary_points = boundary_xs if basis == "X" else boundary_zs - if obs.end_point_set - same_boundary_points: - raise ValueError( - f"Observable doesn't terminate on same-basis boundaries: {basis=} {obs=}." - ) - if not (obs.int_point_set - same_boundary_points): - raise ValueError( - f"Observable is a stabilizer (only runs along same-basis boundaries): {basis=} {obs=}." - ) - - @functools.cached_property - def used_set(self) -> frozenset[complex]: - return frozenset(self.interior_set(include_boundary=True)) - - @functools.cached_property - def x_boundary_set(self) -> frozenset[complex]: - result = set() - for curve in self.region_curves: - for basis, a, b in curve: - if basis == "X": - result |= set(int_points_on_line(a, b)) - return frozenset(result) - - @functools.cached_property - def z_boundary_set(self) -> frozenset[complex]: - result = set() - for curve in self.region_curves: - for basis, a, b in curve: - if basis == "Z": - result |= set(int_points_on_line(a, b)) - return frozenset(result) - - @functools.cached_property - def boundary_set(self) -> frozenset[complex]: - """Returns the set of integer coordinates that are on any of the boundary curves.""" - result = set() - for c in self.region_curves: - result |= c.boundary_set() - return frozenset(result) - - def interior_set(self, *, include_boundary: bool) -> set[complex]: - """Returns the set of integer coordinates inside the bounded area. - - Args: - include_boundary: Whether or not boundary points are considered part - of the interior or not. - """ - return int_points_inside_polygon_set( - [c.points for c in self.region_curves], include_boundary=include_boundary - ) - - def disjoint_interiors(self, *, include_boundary: bool) -> dict[int, set[complex]]: - """Groups the interior by connected component. - - Args: - include_boundary: Whether or not boundary points are considered part - of the interior or not. - - Returns: - A (mask -> interior) dictionary where the mask is the set of curves - the interior is within and the interior is a set of integer - coordinates. - """ - return int_point_disjoint_regions_inside_polygon_set( - [c.points for c in self.region_curves], include_boundary=include_boundary - ) - - def fused(self, a: complex, b: complex) -> "PatchOutline": - """Performs lattice surgery between the two segments intersected by the line from a to b. - - It is assumed that there are exactly two segments along the line from a to b. - It is assumed that these two segments' endpoints are equal when perp-projecting - onto the a-to-b vector. - - Returns: - A boundary list containing the stitched result. - """ - hits = [] - pts = set(int_points_on_line(a, b)) - for k in range(len(self.region_curves)): - for e in self.region_curves[k].segment_indices_intersecting(pts): - hits.append((k, e)) - if len(hits) != 2: - raise NotImplementedError(f"len({hits=}) != 2") - - (c0, s0), (c1, s1) = sorted(hits) - if c0 == c1: - # creating an interior space - c = c0 - v = self.region_curves[c] - new_points = [] - new_bases = [] - fb = v.bases[s0 - 1] - - new_bases.extend(v.bases[:s0]) - new_points.extend(v.points[:s0]) - new_points.extend(v.points[s1:]) - new_bases.append(fb) - new_bases.extend(v.bases[s1 + 1 :]) - - interior_points = v.points[s0:s1] - interior_bases = v.bases[s0:s1] - - return PatchOutline( - [ - *self.region_curves[:c], - ClosedCurve(points=new_points, bases=new_bases), - *self.region_curves[c + 1 :], - ClosedCurve(points=interior_points, bases=interior_bases), - ] - ) - else: - # stitching two regions - v0 = self.region_curves[c0] - v1 = self.region_curves[c1] - new_points = [] - new_bases = [] - fb = v0.bases[s0 - 1] - - new_bases.extend(v0.bases[:s0]) - new_points.extend(v0.points[:s0]) - - new_points.extend(v1.points[s1:]) - new_bases.append(fb) - new_bases.extend(v1.bases[s1 + 1 :]) - - new_points.extend(v1.points[:s1]) - new_bases.extend(v1.bases[:s1]) - - new_points.extend(v0.points[s0:]) - new_bases.append(fb) - new_bases.extend(v0.bases[s0 + 1 :]) - - return PatchOutline( - [ - *self.region_curves[:c0], - ClosedCurve(points=new_points, bases=new_bases), - *self.region_curves[c0 + 1 : c1], - *self.region_curves[c1 + 1 :], - ] - ) - - def __eq__(self, other): - if not isinstance(other, PatchOutline): - return NotImplemented - return self.region_curves == other.region_curves - - def __ne__(self, other): - return not (self == other) - - def __repr__(self): - return f"PatchOutline(curves={self.region_curves!r})" - - def write_svg( - self, - path: str | pathlib.Path, - *, - other_segments: Iterable[tuple[complex, Literal["X", "Y", "Z"], complex]] = (), - other: Union[ - "PatchOutline", - "PatchTransitionOutline", - Iterable[Union["PatchOutline", "PatchTransitionOutline"]], - ] = (), - ) -> None: - from gen._surf._viz_patch_outline_svg import patch_outline_svg_viewer - from gen._surf._patch_transition_outline import PatchTransitionOutline - - if isinstance(other, (PatchOutline, PatchTransitionOutline)): - other = [other] - viewer = patch_outline_svg_viewer( - values=[self, *other], - other_segments=other_segments, - ) - write_file(path, viewer) - - def to_patch( - self, - *, - rel_order_func: Callable[[complex], Iterable[complex]] | None = None, - ) -> Patch: - """Converts the boundary list into an explicit surface code stabilizer configuration.""" - - if rel_order_func is None: - rel_order_func = lambda _: Order_Z - data_qubits = self.interior_set(include_boundary=True) - contiguous_x_data_qubit_sets = [] - contiguous_z_data_qubit_sets = [] - for c in self.region_curves: - cur_set = None - for k in range(len(c.points)): - if k == 0 or c.bases[k] != c.bases[k - 1]: - cur_set = set() - if c.bases[k] == "X": - contiguous_x_data_qubit_sets.append(cur_set) - else: - contiguous_z_data_qubit_sets.append(cur_set) - a = c.points[k - 1] - b = c.points[k] - for p in int_points_on_line(a, b): - cur_set.add(p) - - internal_measure_qubits = half_int_points_inside_int_polygon_set( - curves=[curve.points for curve in self.region_curves], - include_boundary=True, - ) - - plaqs = [] - external_measure_qubits = set() - for c in self.region_curves: - b = c.boundary_travel() - b3 = b * 3 - n = len(b) - for k in range(n): - nearby_measurement_qubits = {b[k] + d for d in Order_Z} - for m in nearby_measurement_qubits: - if m in internal_measure_qubits: - continue - if m in external_measure_qubits: - continue - relevant_contiguous_sets = ( - contiguous_z_data_qubit_sets - if checkerboard_basis(m) == "Z" - else contiguous_x_data_qubit_sets - ) - nearby_boundary_data = b3[n + k - 2 : n + k + 3] - ds = set() - for contiguous_candidate in relevant_contiguous_sets: - kept_ds = { - m + d - for d in Order_Z - if m + d in nearby_boundary_data - and m + d in contiguous_candidate - } - if len(kept_ds) > len(ds): - ds = kept_ds - if len(ds) < 2: - continue - plaqs.append( - Tile( - bases=checkerboard_basis(m), - measurement_qubit=m, - ordered_data_qubits=[ - m + d if d is not None and m + d in ds else None - for d in rel_order_func(m) - ], - ) - ) - external_measure_qubits.add(m) - - d_count = collections.Counter() - for m in internal_measure_qubits: - for d in Order_Z: - d_count[m + d] += 1 - for p in plaqs: - # Don't trim data qubits used by boundary measurements. - for d in p.data_set: - d_count[d] += 100 - - used_data_qubits = set() - for d in sorted_complex(data_qubits): - if d_count[d] > 1: - used_data_qubits.add(d) - - for m in internal_measure_qubits: - b = checkerboard_basis(m) - plaqs.append( - Tile( - bases=b, - measurement_qubit=m, - ordered_data_qubits=[ - m + d if d is not None and m + d in used_data_qubits else None - for d in rel_order_func(m) - ], - ) - ) - - return Patch(plaqs) diff --git a/src/gen/_surf/_patch_outline_test.py b/src/gen/_surf/_patch_outline_test.py deleted file mode 100644 index bd96de7..0000000 --- a/src/gen/_surf/_patch_outline_test.py +++ /dev/null @@ -1,536 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from gen._surf._geo import int_points_inside_polygon -from gen._surf._order import Order_Z, Order_ᴎ, checkerboard_basis -from gen._surf._patch_outline import PatchOutline -from gen._surf._closed_curve import ClosedCurve -from gen._surf._css_observable_boundary_pair import CssObservableBoundaryPair -from gen._surf._path_outline import PathOutline -from gen._surf._viz_patch_outline_svg import patch_outline_svg_viewer - - -def test_tight_boundary(): - c1 = ClosedCurve.from_cycle(["Z", (5 + 0j), "X", (5 + 3j), "Z", 3j, "X", 0j, "Z"]) - - c2 = ClosedCurve.from_cycle( - [ - "X", - 5 + 4j, - "X", - 5 + 6j, - "X", - 0 + 6j, - "X", - 0 + 4j, - ] - ) - - b = PatchOutline([c1, c2]) - fused = b.fused(c1.med(), c2.med()) - - c3 = ClosedCurve.from_cycle( - [ - "Z", - 5 + 0j, - "X", - 5 + 3j, - "X", - 5 + 4j, - "X", - 5 + 6j, - "X", - 0 + 6j, - "X", - 0 + 4j, - "X", - 0 + 3j, - "X", - 0 + 0j, - ] - ) - assert fused == PatchOutline([c3]) - - p = b.to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - assert sum(e.basis == "X" and len(e.data_set) == 2 for e in p.tiles) == 11 - assert sum(e.basis == "Z" and len(e.data_set) == 2 for e in p.tiles) == 4 - - -def test_pitchfork_boundary(): - b = PatchOutline( - [ - ClosedCurve( - points=[0, 3, 3 + 10j, 2 + 10j, 2 + 6j, 1 + 6j, 1 + 10j, 10j], - bases="XXXXZXXX", - ), - ] - ) - p = b.to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - - assert sum(e.basis == "X" and len(e.data_set) == 2 for e in p.tiles) == 15 - assert sum(e.basis == "X" and len(e.data_set) == 3 for e in p.tiles) == 1 - assert sum(e.basis == "Z" and len(e.data_set) == 2 for e in p.tiles) == 2 - assert sum(e.basis == "Z" and len(e.data_set) == 3 for e in p.tiles) == 0 - - -def test_line(): - c = ClosedCurve.from_cycle( - [ - "Z", - 6, - "X", - 6, - "Z", - 0, - "X", - 0, - ] - ) - b = PatchOutline([c]) - p = b.to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - assert p.data_set == {0, 1, 2, 3, 4, 5, 6} - assert p.measure_set == { - 0.5 + 0.5j, - 1.5 - 0.5j, - 2.5 + 0.5j, - 3.5 - 0.5j, - 4.5 + 0.5j, - 5.5 - 0.5j, - } - assert all(len(e.data_set) == 2 for e in p.tiles) - assert len(p.tiles) == 6 - - -def test_hole(): - c = ClosedCurve.from_cycle( - [ - "Z", - 0, - "X", - 10, - "Z", - 10 + 10j, - "X", - 10j, - ] - ) - - c2 = ClosedCurve.from_cycle( - [ - "X", - 2 + 2j, - "X", - 8 + 2j, - "X", - 8 + 8j, - "X", - 2 + 8j, - ] - ) - - b = PatchOutline([c, c2]) - p = b.to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - assert sum(e.basis == "X" and len(e.data_set) == 2 for e in p.tiles) == 18 - assert sum(e.basis == "Z" and len(e.data_set) == 2 for e in p.tiles) == 10 - assert sum(e.basis == "X" and len(e.data_set) == 3 for e in p.tiles) == 2 - assert sum(e.basis == "Z" and len(e.data_set) == 3 for e in p.tiles) == 0 - - -def test_fused_simple(): - a = ClosedCurve.from_cycle( - [ - "X", - 4 + 0j, - "Z", - 4 + 4j, - "X", - 0 + 4j, - "Z", - 0 + 0j, - ] - ) - - b = PatchOutline([a, a.offset_by(6)]) - b = b.fused(2 + 2j, 8 + 2j) - assert b == PatchOutline( - [ - ClosedCurve( - points=[ - 4 + 0j, - 6 + 0j, - 10 + 0j, - 10 + 4j, - 6 + 4j, - 4 + 4j, - 0 + 4j, - 0 + 0j, - ], - bases=["X", "X", "X", "Z", "X", "X", "X", "Z"], - ), - ] - ) - - -def test_fused_stability(): - a = ClosedCurve.from_cycle( - [ - "Z", - 4 + 0j, - "Z", - 4 + 4j, - "Z", - 0 + 4j, - "Z", - 0 + 0j, - ] - ) - - b = PatchOutline([a, a.offset_by(6j), a.offset_by(12j)]) - b = b.fused(2 + 2j, 2 + 8j) - assert b == PatchOutline( - [ - ClosedCurve( - points=[ - 4 + 0j, - 4 + 4j, - 4 + 6j, - 4 + 10j, - 0 + 10j, - 0 + 6j, - 0 + 4j, - 0 + 0j, - ], - bases=["Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z"], - ), - a.offset_by(12j), - ] - ) - - b = b.fused(2 + 8j, 2 + 14j) - assert b == PatchOutline( - [ - ClosedCurve( - points=[ - 4 + 0j, - 4 + 4j, - 4 + 6j, - 4 + 10j, - 4 + 12j, - 4 + 16j, - 0 + 16j, - 0 + 12j, - 0 + 10j, - 0 + 6j, - 0 + 4j, - 0 + 0j, - ], - bases=["Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z"], - ), - ] - ) - - -def test_fused_t(): - a = ClosedCurve.from_cycle( - [ - "Z", - 4 + 0j, - "Z", - 4 + 4j, - "Z", - 0 + 4j, - "Z", - 0 + 0j, - ] - ) - - b = PatchOutline([a, a.offset_by(6j), a.offset_by(6j - 6)]) - b = b.fused(2 + 2j, 2 + 8j) - assert b == PatchOutline( - [ - ClosedCurve( - points=[ - 4 + 0j, - 4 + 4j, - 4 + 6j, - 4 + 10j, - 0 + 10j, - 0 + 6j, - 0 + 4j, - 0 + 0j, - ], - bases=["Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z"], - ), - a.offset_by(6j - 6), - ] - ) - - b = b.fused(-4 + 8j, 2 + 8j) - assert b == PatchOutline( - [ - ClosedCurve( - points=[ - 4 + 0j, - 4 + 4j, - 4 + 6j, - 4 + 10j, - 0 + 10j, - -2 + 10j, - -6 + 10j, - -6 + 6j, - -2 + 6j, - 0 + 6j, - 0 + 4j, - 0 + 0j, - ], - bases=["Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z"], - ), - ] - ) - assert ( - len( - [ - p - for p in int_points_inside_polygon( - b.region_curves[0].points, include_boundary=False - ) - if p.real == 0 - ] - ) - == 3 - ) - - -def test_fused_interior(): - a = ClosedCurve.from_cycle( - [ - "Z", - 12 + 0j, - "Z", - 12 + 4j, - "Z", - 8 + 4j, - "Z", - 4 + 4j, - "Z", - 4 + 8j, - "Z", - 8 + 8j, - "Z", - 12 + 8j, - "Z", - 12 + 12j, - "Z", - 0 + 12j, - "Z", - 0, - ] - ) - - actual = PatchOutline([a]).fused(10 + 2j, 10 + 10j) - assert actual == PatchOutline( - [ - ClosedCurve( - points=[(12 + 0j), (12 + 4j), (12 + 8j), (12 + 12j), 12j, 0], - bases=["Z", "Z", "Z", "Z", "Z", "Z"], - ), - ClosedCurve( - points=[(8 + 4j), (4 + 4j), (4 + 8j), (8 + 8j)], - bases=["Z", "Z", "Z", "Z"], - ), - ] - ) - - -def test_fused_inner(): - b1 = ClosedCurve.from_cycle( - [ - "X", - 0 + 16j, - 6 + 16j, - 8 + 16j, - 14 + 16j, - 16 + 16j, - 22 + 16j, - 22 + 18j, - 22 + 20j, - 22 + 22j, - 22 + 24j, - 22 + 26j, - 22 + 28j, - 22 + 30j, - 16 + 30j, - 16 + 28j, - 16 + 26j, - 14 + 26j, - 8 + 26j, - "Z", - 8 + 24j, - "X", - 14 + 24j, - 16 + 24j, - 16 + 22j, - 16 + 20j, - 16 + 18j, - 14 + 18j, - 8 + 18j, - 6 + 18j, - 6 + 20j, - 8 + 20j, - 14 + 20j, - "Z", - 14 + 22j, - "X", - 8 + 22j, - 6 + 22j, - 6 + 24j, - 6 + 26j, - 6 + 28j, - 8 + 28j, - 14 + 28j, - "Z", - 14 + 30j, - "X", - 8 + 30j, - 6 + 30j, - 30j, - 28j, - 26j, - 24j, - 22j, - 20j, - 18j, - ] - ).to_patch_outline() - b2 = b1.fused(19 + 29j, 11 + 29j) - - assert len(b2.region_curves) == 2 - assert all(e == "X" for e in b2.region_curves[0].bases) - assert b2.region_curves[1] == ClosedCurve.from_cycle( - [ - "X", - (16 + 28j), - (16 + 26j), - (14 + 26j), - (8 + 26j), - "Z", - (8 + 24j), - "X", - (14 + 24j), - (16 + 24j), - (16 + 22j), - (16 + 20j), - (16 + 18j), - (14 + 18j), - (8 + 18j), - (6 + 18j), - (6 + 20j), - (8 + 20j), - (14 + 20j), - "Z", - (14 + 22j), - "X", - (8 + 22j), - (6 + 22j), - (6 + 24j), - (6 + 26j), - (6 + 28j), - (8 + 28j), - (14 + 28j), - ] - ) - - -def test_validate(): - patch = ClosedCurve.from_cycle([0, "X", 5, "Z", 5 + 5j, "X", 5j, "Z", 0]) - obs = CssObservableBoundaryPair( - x_obs=PathOutline([(0, 5j, "X")]), - z_obs=PathOutline([(0, 5, "Z")]), - ) - PatchOutline( - [patch], - observables=[obs], - ).validate() - with pytest.raises(ValueError, match="X0 and Z1"): - PatchOutline( - [patch], - observables=[obs, obs], - ).validate() - with pytest.raises(ValueError, match="terminate on same-basis boundaries"): - PatchOutline( - [patch], - observables=[ - CssObservableBoundaryPair( - x_obs=PathOutline([(0, 5j, "X")]), - z_obs=PathOutline([(0, 4, "Z")]), - ) - ], - ).validate() - with pytest.raises(ValueError, match="only runs along same-basis boundaries"): - PatchOutline( - [patch], - observables=[ - CssObservableBoundaryPair( - x_obs=PathOutline([(0, 5, "X")]), - z_obs=PathOutline([(0, 5j, "Z")]), - ) - ], - ).validate() - - -def test_diagram(): - boundary = PatchOutline( - [ClosedCurve.from_cycle([0, "X", 5, "Z", 5 + 5j, "X", 5j, "Z", 0])], - observables=[ - CssObservableBoundaryPair( - x_obs=PathOutline([(0, 5j, "X")]), - z_obs=PathOutline([(1j, 1j + 5, "Z")]), - ), - ], - ) - assert ( - patch_outline_svg_viewer([boundary]).strip() - == """ - - -X - -Y - -Z - - - - - - - - - - - - - -""".strip() - ) diff --git a/src/gen/_surf/_patch_transition_outline.py b/src/gen/_surf/_patch_transition_outline.py deleted file mode 100644 index fc928d1..0000000 --- a/src/gen/_surf/_patch_transition_outline.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import functools -from typing import Iterable - -import gen -from gen._surf._closed_curve import ClosedCurve -from gen._surf._geo import int_points_inside_polygon_set -from gen._surf._path_outline import PathOutline - - -class PatchTransitionOutline: - """An outline of what's happening during a transition between two patches. - - This class stores data encoding a floor or ceiling in a 3d topological - spacetime diagram. It encodes the details of time boundaries. - """ - - def __init__( - self, - *, - observable_deltas: dict[str, PathOutline], - data_boundary_planes: Iterable[ClosedCurve], - ): - self.observable_deltas = observable_deltas - self.data_boundary_planes = tuple(data_boundary_planes) - for data_boundary_plane in self.data_boundary_planes: - if data_boundary_plane.basis is None: - raise ValueError(f"Not a uniform basis: {data_boundary_plane=}") - - @staticmethod - def empty() -> "PatchTransitionOutline": - return PatchTransitionOutline(observable_deltas={}, data_boundary_planes=[]) - - def __bool__(self) -> bool: - return bool(self.observable_deltas) or bool(self.data_boundary_planes) - - @functools.cached_property - def patch(self) -> gen.Patch: - tiles = [] - for b, qs in [ - ("X", self.data_x_set), - ("Y", self.data_y_set), - ("Z", self.data_z_set), - ]: - for q in qs: - tiles.append( - gen.Tile(bases=b, measurement_qubit=q, ordered_data_qubits=[q]) - ) - return gen.Patch(tiles) - - def iter_control_points(self) -> Iterable[complex]: - for curve in self.data_boundary_planes: - yield from curve.points - for segment_sets in self.observable_deltas.values(): - for a, b, _ in segment_sets.segments: - yield a - yield b - - @functools.cached_property - def data_basis_map(self) -> dict[complex, str]: - result = {} - for q in self.data_x_set: - result[q] = "X" - for q in self.data_y_set: - result[q] = "Y" - for q in self.data_z_set: - result[q] = "Z" - return result - - @functools.cached_property - def data_set(self) -> frozenset[complex]: - curves = [] - for plane in self.data_boundary_planes: - curves.append(plane.points) - return frozenset(int_points_inside_polygon_set(curves, include_boundary=True)) - - @functools.cached_property - def data_x_set(self) -> frozenset[complex]: - curves = [] - for plane in self.data_boundary_planes: - if plane.basis == "X": - curves.append(plane.points) - return frozenset(int_points_inside_polygon_set(curves, include_boundary=True)) - - @functools.cached_property - def data_y_set(self) -> frozenset[complex]: - curves = [] - for plane in self.data_boundary_planes: - if plane.basis == "Y": - curves.append(plane.points) - return frozenset(int_points_inside_polygon_set(curves, include_boundary=True)) - - @functools.cached_property - def data_z_set(self) -> frozenset[complex]: - curves = [] - for plane in self.data_boundary_planes: - if plane.basis == "Z": - curves.append(plane.points) - return frozenset(int_points_inside_polygon_set(curves, include_boundary=True)) diff --git a/src/gen/_surf/_patch_transition_outline_test.py b/src/gen/_surf/_patch_transition_outline_test.py deleted file mode 100644 index 4c15084..0000000 --- a/src/gen/_surf/_patch_transition_outline_test.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from gen._surf._closed_curve import ClosedCurve -from gen._surf._patch_transition_outline import PatchTransitionOutline -from gen._surf._path_outline import PathOutline - - -def test_data_set(): - trans = PatchTransitionOutline( - observable_deltas={1: PathOutline([(0 + 1j, 2 + 1j, "X")])}, - data_boundary_planes=[ClosedCurve.from_cycle(["X", 0, 2, 2 + 2j, 2j])], - ) - assert trans.data_set == {0, 1, 2, 1j, 1j + 1, 1j + 2, 2j, 2j + 1, 2j + 2} diff --git a/src/gen/_surf/_path_outline.py b/src/gen/_surf/_path_outline.py deleted file mode 100644 index b152b39..0000000 --- a/src/gen/_surf/_path_outline.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import functools -from typing import Iterable, Sequence, Literal - -import gen -from gen._surf._geo import int_points_on_line -from gen._core._util import complex_key - - -class PathOutline: - """A series of line segments between integer coordinates.""" - - def __init__(self, segments: Iterable[tuple[complex, complex, Literal["X", "Z"]]]): - canon_segments = [] - for a, c, b in segments: - a, c = sorted([a, c], key=complex_key) - canon_segments.append((a, c, b)) - self.segments = frozenset(canon_segments) - - @staticmethod - def from_stops( - basis: Literal["X", "Z"], - stops: Sequence[complex], - *, - extra: Iterable[complex] = (), - ) -> "PathOutline": - assert len(stops) > 1 - segments = [] - for k in range(len(stops) - 1): - segments.append((stops[k], stops[k + 1], basis)) - for e in extra: - segments.append((e, e, basis)) - return PathOutline(segments) - - def offset_by(self, offset: complex) -> "PathOutline": - return PathOutline((a + offset, b + offset, c) for a, b, c in self.segments) - - def basis(self) -> str | None: - bases = {c for _, _, c in self.segments} - if len(bases) != 1: - return None - return next(iter(bases)) - - def to_pauli_string(self) -> gen.PauliString: - b = self.basis() - assert b is not None - return gen.PauliString({q: b for q in self.int_point_set}) - - @functools.cached_property - def int_point_set(self) -> set[complex]: - return {p for a, b, _ in self.segments for p in int_points_on_line(a, b)} - - @property - def end_point_set(self) -> frozenset[complex]: - end_points = set() - for a, b, _ in self.segments: - end_points ^= {a, b} - return frozenset(end_points) - - @property - def end_point_set_including_elbows(self) -> frozenset[complex]: - end_points = set() - for a, b, _ in self.segments: - end_points.add(a) - end_points.add(b) - return frozenset(end_points) - - def __eq__(self, other): - if not isinstance(other, PathOutline): - return NotImplemented - return self.segments == other.segments - - def __ne__(self, other): - return not (self == other) - - def __repr__(self): - return f"SegmentSet({self.segments!r})" diff --git a/src/gen/_surf/_step_outline.py b/src/gen/_surf/_step_outline.py deleted file mode 100644 index cce7eb2..0000000 --- a/src/gen/_surf/_step_outline.py +++ /dev/null @@ -1,447 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import functools -from typing import Callable, Any, Iterable - -from gen._core import Builder, AtLayer -from gen._surf._closed_curve import ClosedCurve -from gen._surf._patch_outline import PatchOutline -from gen._surf._patch_transition_outline import PatchTransitionOutline - - -class StepOutline: - def __init__( - self, - *, - start: PatchTransitionOutline | None = None, - body: PatchOutline, - end: PatchTransitionOutline | None = None, - obs_delta_body_start: dict[str, ClosedCurve] | None = None, - obs_delta_body_end: dict[str, ClosedCurve] | None = None, - rounds: int, - ): - self.start: PatchTransitionOutline = ( - start if start is not None else PatchTransitionOutline.empty() - ) - self.body: PatchOutline = body - self.obs_delta_body_start = ( - {} if obs_delta_body_start is None else obs_delta_body_start - ) - self.obs_delta_body_end = ( - {} if obs_delta_body_end is None else obs_delta_body_end - ) - self.end: PatchTransitionOutline = ( - end if end is not None else PatchTransitionOutline.empty() - ) - self.rounds = rounds - assert self.rounds > 0 - - def without_start_transition(self): - return StepOutline( - start=None, - body=self.body, - end=self.end, - obs_delta_body_start=self.obs_delta_body_end, - obs_delta_body_end=self.obs_delta_body_start, - rounds=self.rounds, - ) - - def without_end_transition(self): - return StepOutline( - start=self.start, - body=self.body, - end=None, - obs_delta_body_start=self.obs_delta_body_end, - obs_delta_body_end=self.obs_delta_body_start, - rounds=self.rounds, - ) - - @functools.cached_property - def used_set(self) -> frozenset[complex]: - return self.start.data_set | self.end.data_set | self.body.to_patch().used_set - - def to_magic_init_step(self) -> "MagicInitStep": - return MagicInitStep(self) - - def to_magic_end_step(self) -> "MagicEndStep": - return MagicEndStep(self) - - def build_rounds( - self, - *, - builder: Builder, - rel_order_func: Callable[[complex], Iterable[complex]], - alternate_ordering_with_round_parity: bool, - start_round_index: int, - cmp_layer: Any | None, - save_layer: Any | None, - edit_cur_obs: dict[str, set[complex]], - o2i: dict[str, int | None], - ) -> None: - assert self.rounds > 0 - empty = PatchTransitionOutline.empty() - patch = self.body.to_patch(rel_order_func=rel_order_func) - - def loop_round_key(r: int) -> Any: - return (save_layer, "round", r) - - def fast_forward_record( - *, rec_offset: int, old_round_key: Any, new_round_key: Any - ): - builder.tracker.next_measurement_index += rec_offset - for m in patch.measure_set: - builder.tracker.recorded[AtLayer(m, new_round_key)] = [ - e + rec_offset - for e in builder.tracker.recorded[AtLayer(m, old_round_key)] - ] - - round_builders = [] - round_index = start_round_index - end_round_index = start_round_index + self.rounds - while round_index < end_round_index: - round_save_layer = loop_round_key(round_index) - round_builders.append(builder.fork()) - bd: list[tuple[str, ClosedCurve]] = [] - if round_index == start_round_index: - bd.extend(self.obs_delta_body_start.items()) - if round_index == end_round_index - 1: - bd.extend(self.obs_delta_body_end.items()) - _css_surface_code_round( - builder=round_builders[-1], - edit_cur_obs=edit_cur_obs, - start_outline=self.start if round_index == start_round_index else empty, - end_outline=self.end if round_index == end_round_index - 1 else empty, - observable_stabilizer_deltas=bd, - patch_outline=self.body, - save_layer=round_save_layer, - cmp_layer=cmp_layer - if round_index == start_round_index - else loop_round_key(round_index - 1), - rel_order_func=rel_order_func, - reverse_order=alternate_ordering_with_round_parity - and round_index % 2 == 1, - o2i=o2i, - ) - - round_index += 1 - - # Use repeat blocks when the circuit starts repeating. - bulk_rounds_left = ( - end_round_index - - round_index - - bool(self.end or self.obs_delta_body_end) - ) - if bulk_rounds_left > 0: - if ( - alternate_ordering_with_round_parity - and bulk_rounds_left % 2 == 0 - and len(round_builders) >= 4 - and round_builders[-3].circuit == round_builders[-1].circuit - and round_builders[-4].circuit == round_builders[-2].circuit - ): - round_builders.pop() - round_builders.pop() - reversed_half = round_builders.pop() - round_builders[-1].circuit += reversed_half.circuit - m = round_builders[-1].circuit.num_measurements - double_rounds_to_run = bulk_rounds_left // 2 - round_builders[-1].circuit *= double_rounds_to_run + 2 - round_index += double_rounds_to_run * 2 - fast_forward_record( - rec_offset=m * (double_rounds_to_run - 1), - new_round_key=loop_round_key(round_index - 1), - old_round_key=round_save_layer, - ) - elif ( - not alternate_ordering_with_round_parity - and len(round_builders) >= 2 - and round_builders[-1].circuit == round_builders[-2].circuit - ): - round_builders.pop() - rounds_to_run = bulk_rounds_left - m = round_builders[-1].circuit.num_measurements - round_builders[-1].circuit *= rounds_to_run + 2 - round_index += rounds_to_run - fast_forward_record( - rec_offset=m * rounds_to_run, - new_round_key=loop_round_key(round_index - 1), - old_round_key=round_save_layer, - ) - - for sub_builder in round_builders: - builder.circuit += sub_builder.circuit - - # Expose last round tracker info as overall saved tracker info - round_save_layer = loop_round_key(end_round_index - 1) - for q in self.end.data_set | patch.measure_set: - builder.tracker.recorded[AtLayer(q, save_layer)] = builder.tracker.recorded[ - AtLayer(q, round_save_layer) - ] - - -def _css_surface_code_round( - *, - builder: Builder, - o2i: dict[str, int | None], - edit_cur_obs: dict[str, set[complex]], - start_outline: PatchTransitionOutline, - end_outline: PatchTransitionOutline, - observable_stabilizer_deltas: Iterable[tuple[str, ClosedCurve]], - patch_outline: PatchOutline, - rel_order_func: Callable[[complex], Iterable[complex]], - reverse_order: bool, - save_layer: Any, - cmp_layer: Any | None, -) -> None: - patch = patch_outline.to_patch(rel_order_func=rel_order_func) - x_tiles = [tile for tile in patch.tiles if tile.basis == "X"] - z_tiles = [tile for tile in patch.tiles if tile.basis == "Z"] - if len(x_tiles) + len(z_tiles) != len(patch.tiles): - raise NotImplementedError(f"Non-CSS {patch.tiles=}") - - # Reset moment. - reset_bases = { - "X": {tile.measurement_qubit for tile in x_tiles} | start_outline.data_x_set, - "Z": {tile.measurement_qubit for tile in z_tiles} | start_outline.data_z_set, - } - for basis, qs in reset_bases.items(): - if qs: - builder.gate(f"R{basis}", qs) - builder.tick() - - # CX moments. - (num_layers,) = {len(e.ordered_data_qubits) for e in patch.tiles} - for k in range(num_layers)[:: -1 if reverse_order else +1]: - pairs = [] - for tile in patch.tiles: - q = tile.ordered_data_qubits[k] - if q is not None: - pair = (tile.measurement_qubit, q) - if tile.basis == "Z": - pair = pair[::-1] - pairs.append(pair) - builder.gate2("CX", pairs) - builder.tick() - - # Measure moment. - measure_bases = { - "X": {tile.measurement_qubit for tile in x_tiles} | end_outline.data_x_set, - "Z": {tile.measurement_qubit for tile in z_tiles} | end_outline.data_z_set, - } - tmp_save_layer = ("raw", save_layer) - for basis, qs in measure_bases.items(): - if qs: - builder.measure( - qs, - basis=basis, - save_layer=tmp_save_layer, - ) - - # Compare to resets and/or previous round. - for tile in patch.tiles: - comm = start_outline.data_x_set - anti = start_outline.data_z_set - if tile.basis == "Z": - comm, anti = anti, comm - if not tile.data_set.isdisjoint(anti): - continue - keys = [AtLayer(tile.measurement_qubit, tmp_save_layer)] - if not (tile.data_set <= comm): - if cmp_layer is None: - # No comparison available. - continue - prev_key = AtLayer(tile.measurement_qubit, cmp_layer) - keys.append(prev_key) - if prev_key not in builder.tracker.recorded: - continue - builder.detector(keys, pos=tile.measurement_qubit) - - # Handle data measurements. - for tile in patch.tiles: - comm = end_outline.data_x_set - anti = end_outline.data_z_set - if tile.basis == "Z": - comm, anti = anti, comm - save_key = AtLayer(tile.measurement_qubit, save_layer) - - if not tile.data_set.isdisjoint(anti): - # Stabilizer lost due to measurements. - builder.tracker.record_obstacle(save_key) - elif tile.data_set <= comm: - # Stabilizer finalized by measurements. - builder.detector( - [AtLayer(q, tmp_save_layer) for q in tile.used_set], - pos=tile.measurement_qubit, - t=0.5, - ) - builder.tracker.record_obstacle(save_key) - else: - # All or part of stabilizer survived measurements. - builder.tracker.make_measurement_group( - [AtLayer(tile.measurement_qubit, tmp_save_layer)] - + [AtLayer(q, tmp_save_layer) for q in tile.data_set if q in comm], - key=save_key, - ) - for q in end_outline.data_set: - builder.tracker.recorded[AtLayer(q, save_layer)] = builder.tracker.recorded[ - AtLayer(q, tmp_save_layer) - ] - - # Apply observable deltas. - for obs_name, delta in start_outline.observable_deltas.items(): - obs_index = o2i[obs_name] - if obs_index is None: - continue - pts = delta.int_point_set - assert pts <= start_outline.data_set, (obs_name, pts, start_outline.data_set) - assert pts.isdisjoint(edit_cur_obs[obs_name]) - edit_cur_obs[obs_name] ^= pts - for obs_name, delta in observable_stabilizer_deltas: - obs_index = o2i[obs_name] - if obs_index is None: - continue - pts = delta.interior_set(include_boundary=True) - b = delta.basis - assert b is not None - ms = { - tile.measurement_qubit - for tile in patch.tiles - if tile.basis == b and tile.data_set <= pts - } - for pt in ms: - edit_cur_obs[obs_name] ^= patch.m2tile[pt].data_set - builder.obs_include( - [AtLayer(m, tmp_save_layer) for m in ms], obs_index=obs_index - ) - for obs_name, delta in end_outline.observable_deltas.items(): - obs_index = o2i[obs_name] - if obs_index is None: - continue - pts = delta.int_point_set - assert pts <= end_outline.data_x_set | end_outline.data_z_set - assert pts <= edit_cur_obs[obs_name] - edit_cur_obs[obs_name] ^= pts - builder.obs_include( - [AtLayer(q, tmp_save_layer) for q in pts], obs_index=obs_index - ) - - builder.shift_coords(dt=1) - builder.tick() - - -class MagicInitStep: - def __init__(self, step: StepOutline): - self.step = step - - @property - def rounds(self) -> int: - return self.step.rounds - - def build_rounds( - self, - *, - builder: Builder, - rel_order_func: Callable[[complex], Iterable[complex]], - alternate_ordering_with_round_parity: bool, - start_round_index: int, - cmp_layer: Any | None, - save_layer: Any | None, - edit_cur_obs: dict[str, set[complex]], - o2i: dict[str, int | None], - ) -> None: - builder.gate("DEPOLARIZE1", builder.q2i.keys(), 0.75) - builder.tick() - patch = self.step.body.to_patch() - for tile in patch.tiles: - builder.measure_pauli_product( - tile.to_data_pauli_string(), - key=AtLayer(tile.measurement_qubit, save_layer), - ) - for obs_name, delta in self.step.start.observable_deltas.items(): - obs_index = o2i[obs_name] - if obs_index is None: - continue - builder.measure_pauli_product( - delta.to_pauli_string(), - key=AtLayer(obs_name, save_layer), - ) - assert not edit_cur_obs[obs_name] - edit_cur_obs[obs_name] ^= delta.int_point_set - for obs_name, delta in self.step.start.observable_deltas.items(): - obs_index = o2i[obs_name] - if obs_index is None: - continue - builder.obs_include([AtLayer(obs_name, save_layer)], obs_index=obs_index) - builder.shift_coords(dt=1) - builder.tick() - - -class MagicEndStep: - def __init__(self, step: StepOutline): - self.step = step - - @property - def rounds(self) -> int: - return self.step.rounds - - def build_rounds( - self, - *, - builder: Builder, - rel_order_func: Callable[[complex], Iterable[complex]], - alternate_ordering_with_round_parity: bool, - start_round_index: int, - cmp_layer: Any | None, - save_layer: Any | None, - edit_cur_obs: dict[str, set[complex]], - o2i: dict[str, int | None], - ) -> None: - patch = self.step.body.to_patch() - for tile in patch.tiles: - builder.measure_pauli_product( - tile.to_data_pauli_string(), - key=AtLayer(tile.measurement_qubit, save_layer), - ) - for obs_name, delta in self.step.end.observable_deltas.items(): - obs_index = o2i[obs_name] - if obs_index is None: - continue - builder.measure_pauli_product( - delta.to_pauli_string(), - key=AtLayer(obs_name, save_layer), - ) - assert edit_cur_obs[obs_name] == delta.int_point_set, ( - obs_name, - edit_cur_obs[obs_name], - delta.int_point_set, - ) - edit_cur_obs[obs_name].clear() - - for tile in patch.tiles: - builder.detector( - [ - AtLayer(tile.measurement_qubit, cmp_layer), - AtLayer(tile.measurement_qubit, save_layer), - ], - pos=tile.measurement_qubit, - ) - for obs_name, delta in self.step.end.observable_deltas.items(): - obs_index = o2i[obs_name] - if obs_index is None: - continue - builder.obs_include([AtLayer(obs_name, save_layer)], obs_index=obs_index) - builder.shift_coords(dt=1) - builder.tick() - builder.gate("DEPOLARIZE1", builder.q2i.keys(), 0.75) diff --git a/src/gen/_surf/_step_outline_test.py b/src/gen/_surf/_step_outline_test.py deleted file mode 100644 index 5a9a016..0000000 --- a/src/gen/_surf/_step_outline_test.py +++ /dev/null @@ -1,379 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import stim - -import gen - - -def test_step_outline_simple(): - rel_order_func = ( - lambda m: gen.Order_Z if gen.checkerboard_basis(m) == "Z" else gen.Order_ᴎ - ) - step = gen.StepOutline( - start=gen.PatchTransitionOutline( - observable_deltas={}, - data_boundary_planes=[ - gen.ClosedCurve.from_cycle( - [ - "Z", - 0, - 2, - 2 + 2j, - 2j, - ] - ) - ], - ), - body=gen.PatchOutline( - [ - gen.ClosedCurve.from_cycle( - [ - 0, - "X", - 2, - "Z", - 2 + 2j, - "X", - 2j, - "Z", - ] - ) - ] - ), - end=gen.PatchTransitionOutline( - observable_deltas={}, - data_boundary_planes=[ - gen.ClosedCurve.from_cycle( - [ - "X", - 0, - 2, - 2 + 2j, - 2j, - ] - ) - ], - ), - rounds=25, - ) - builder = gen.Builder.for_qubits( - step.body.to_patch(rel_order_func=rel_order_func).used_set - ) - step.build_rounds( - builder=builder, - rel_order_func=rel_order_func, - alternate_ordering_with_round_parity=False, - start_round_index=5, - cmp_layer=None, - save_layer="test", - edit_cur_obs={}, - o2i={}, - ) - assert builder.circuit == stim.Circuit( - """ - QUBIT_COORDS(0, 0) 0 - QUBIT_COORDS(0, 1) 1 - QUBIT_COORDS(0, 2) 2 - QUBIT_COORDS(1, 0) 3 - QUBIT_COORDS(1, 1) 4 - QUBIT_COORDS(1, 2) 5 - QUBIT_COORDS(2, 0) 6 - QUBIT_COORDS(2, 1) 7 - QUBIT_COORDS(2, 2) 8 - QUBIT_COORDS(-0.5, 1.5) 9 - QUBIT_COORDS(0.5, -0.5) 10 - QUBIT_COORDS(0.5, 0.5) 11 - QUBIT_COORDS(0.5, 1.5) 12 - QUBIT_COORDS(1.5, 0.5) 13 - QUBIT_COORDS(1.5, 1.5) 14 - QUBIT_COORDS(1.5, 2.5) 15 - QUBIT_COORDS(2.5, 0.5) 16 - RX 10 12 13 15 - R 0 1 2 3 4 5 6 7 8 9 11 14 16 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - MX 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-4] - DETECTOR(0.5, 0.5, 0) rec[-3] - DETECTOR(1.5, 1.5, 0) rec[-2] - DETECTOR(2.5, 0.5, 0) rec[-1] - SHIFT_COORDS(0, 0, 1) - TICK - REPEAT 23 { - RX 10 12 13 15 - R 9 11 14 16 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - MX 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-12] rec[-4] - DETECTOR(0.5, -0.5, 0) rec[-16] rec[-8] - DETECTOR(0.5, 0.5, 0) rec[-11] rec[-3] - DETECTOR(0.5, 1.5, 0) rec[-15] rec[-7] - DETECTOR(1.5, 0.5, 0) rec[-14] rec[-6] - DETECTOR(1.5, 1.5, 0) rec[-10] rec[-2] - DETECTOR(1.5, 2.5, 0) rec[-13] rec[-5] - DETECTOR(2.5, 0.5, 0) rec[-9] rec[-1] - SHIFT_COORDS(0, 0, 1) - TICK - } - RX 10 12 13 15 - R 9 11 14 16 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - MX 0 1 2 3 4 5 6 7 8 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-21] rec[-4] - DETECTOR(0.5, -0.5, 0) rec[-25] rec[-8] - DETECTOR(0.5, 0.5, 0) rec[-20] rec[-3] - DETECTOR(0.5, 1.5, 0) rec[-24] rec[-7] - DETECTOR(1.5, 0.5, 0) rec[-23] rec[-6] - DETECTOR(1.5, 1.5, 0) rec[-19] rec[-2] - DETECTOR(1.5, 2.5, 0) rec[-22] rec[-5] - DETECTOR(2.5, 0.5, 0) rec[-18] rec[-1] - DETECTOR(0.5, -0.5, 0.5) rec[-17] rec[-14] rec[-8] - DETECTOR(0.5, 1.5, 0.5) rec[-16] rec[-15] rec[-13] rec[-12] rec[-7] - DETECTOR(1.5, 0.5, 0.5) rec[-14] rec[-13] rec[-11] rec[-10] rec[-6] - DETECTOR(1.5, 2.5, 0.5) rec[-12] rec[-9] rec[-5] - SHIFT_COORDS(0, 0, 1) - TICK - """ - ) - - -def test_step_outline_alternating(): - rel_order_func = ( - lambda m: gen.Order_Z if gen.checkerboard_basis(m) == "Z" else gen.Order_ᴎ - ) - step = gen.StepOutline( - start=gen.PatchTransitionOutline( - observable_deltas={}, - data_boundary_planes=[ - gen.ClosedCurve.from_cycle( - [ - "Z", - 0, - 2, - 2 + 2j, - 2j, - ] - ) - ], - ), - body=gen.PatchOutline( - [ - gen.ClosedCurve.from_cycle( - [ - 0, - "X", - 2, - "Z", - 2 + 2j, - "X", - 2j, - "Z", - ] - ) - ] - ), - end=gen.PatchTransitionOutline( - observable_deltas={}, - data_boundary_planes=[ - gen.ClosedCurve.from_cycle( - [ - "X", - 0, - 2, - 2 + 2j, - 2j, - ] - ) - ], - ), - rounds=25, - ) - builder = gen.Builder.for_qubits( - step.body.to_patch(rel_order_func=rel_order_func).used_set - ) - step.build_rounds( - builder=builder, - rel_order_func=rel_order_func, - alternate_ordering_with_round_parity=True, - start_round_index=5, - cmp_layer=None, - save_layer="test", - edit_cur_obs={}, - o2i={}, - ) - assert builder.circuit == stim.Circuit( - """ - QUBIT_COORDS(0, 0) 0 - QUBIT_COORDS(0, 1) 1 - QUBIT_COORDS(0, 2) 2 - QUBIT_COORDS(1, 0) 3 - QUBIT_COORDS(1, 1) 4 - QUBIT_COORDS(1, 2) 5 - QUBIT_COORDS(2, 0) 6 - QUBIT_COORDS(2, 1) 7 - QUBIT_COORDS(2, 2) 8 - QUBIT_COORDS(-0.5, 1.5) 9 - QUBIT_COORDS(0.5, -0.5) 10 - QUBIT_COORDS(0.5, 0.5) 11 - QUBIT_COORDS(0.5, 1.5) 12 - QUBIT_COORDS(1.5, 0.5) 13 - QUBIT_COORDS(1.5, 1.5) 14 - QUBIT_COORDS(1.5, 2.5) 15 - QUBIT_COORDS(2.5, 0.5) 16 - RX 10 12 13 15 - R 0 1 2 3 4 5 6 7 8 9 11 14 16 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - MX 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-4] - DETECTOR(0.5, 0.5, 0) rec[-3] - DETECTOR(1.5, 1.5, 0) rec[-2] - DETECTOR(2.5, 0.5, 0) rec[-1] - SHIFT_COORDS(0, 0, 1) - TICK - RX 10 12 13 15 - R 9 11 14 16 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - MX 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-12] rec[-4] - DETECTOR(0.5, -0.5, 0) rec[-16] rec[-8] - DETECTOR(0.5, 0.5, 0) rec[-11] rec[-3] - DETECTOR(0.5, 1.5, 0) rec[-15] rec[-7] - DETECTOR(1.5, 0.5, 0) rec[-14] rec[-6] - DETECTOR(1.5, 1.5, 0) rec[-10] rec[-2] - DETECTOR(1.5, 2.5, 0) rec[-13] rec[-5] - DETECTOR(2.5, 0.5, 0) rec[-9] rec[-1] - SHIFT_COORDS(0, 0, 1) - TICK - REPEAT 11 { - RX 10 12 13 15 - R 9 11 14 16 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - MX 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-12] rec[-4] - DETECTOR(0.5, -0.5, 0) rec[-16] rec[-8] - DETECTOR(0.5, 0.5, 0) rec[-11] rec[-3] - DETECTOR(0.5, 1.5, 0) rec[-15] rec[-7] - DETECTOR(1.5, 0.5, 0) rec[-14] rec[-6] - DETECTOR(1.5, 1.5, 0) rec[-10] rec[-2] - DETECTOR(1.5, 2.5, 0) rec[-13] rec[-5] - DETECTOR(2.5, 0.5, 0) rec[-9] rec[-1] - SHIFT_COORDS(0, 0, 1) - TICK - RX 10 12 13 15 - R 9 11 14 16 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - MX 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-12] rec[-4] - DETECTOR(0.5, -0.5, 0) rec[-16] rec[-8] - DETECTOR(0.5, 0.5, 0) rec[-11] rec[-3] - DETECTOR(0.5, 1.5, 0) rec[-15] rec[-7] - DETECTOR(1.5, 0.5, 0) rec[-14] rec[-6] - DETECTOR(1.5, 1.5, 0) rec[-10] rec[-2] - DETECTOR(1.5, 2.5, 0) rec[-13] rec[-5] - DETECTOR(2.5, 0.5, 0) rec[-9] rec[-1] - SHIFT_COORDS(0, 0, 1) - TICK - } - RX 10 12 13 15 - R 9 11 14 16 - TICK - CX 2 9 4 11 8 14 10 3 12 5 13 7 - TICK - CX 1 11 5 14 7 16 12 4 13 6 15 8 - TICK - CX 1 9 3 11 7 14 10 0 12 2 13 4 - TICK - CX 0 11 4 14 6 16 12 1 13 3 15 5 - TICK - MX 0 1 2 3 4 5 6 7 8 10 12 13 15 - M 9 11 14 16 - DETECTOR(-0.5, 1.5, 0) rec[-21] rec[-4] - DETECTOR(0.5, -0.5, 0) rec[-25] rec[-8] - DETECTOR(0.5, 0.5, 0) rec[-20] rec[-3] - DETECTOR(0.5, 1.5, 0) rec[-24] rec[-7] - DETECTOR(1.5, 0.5, 0) rec[-23] rec[-6] - DETECTOR(1.5, 1.5, 0) rec[-19] rec[-2] - DETECTOR(1.5, 2.5, 0) rec[-22] rec[-5] - DETECTOR(2.5, 0.5, 0) rec[-18] rec[-1] - DETECTOR(0.5, -0.5, 0.5) rec[-17] rec[-14] rec[-8] - DETECTOR(0.5, 1.5, 0.5) rec[-16] rec[-15] rec[-13] rec[-12] rec[-7] - DETECTOR(1.5, 0.5, 0.5) rec[-14] rec[-13] rec[-11] rec[-10] rec[-6] - DETECTOR(1.5, 2.5, 0.5) rec[-12] rec[-9] rec[-5] - SHIFT_COORDS(0, 0, 1) - TICK - """ - ) diff --git a/src/gen/_surf/_step_sequence_outline.py b/src/gen/_surf/_step_sequence_outline.py deleted file mode 100644 index 7276a83..0000000 --- a/src/gen/_surf/_step_sequence_outline.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pathlib -from typing import Union, Optional, Callable, Any, Iterable, Tuple - -import stim - -import gen -from gen._core import Builder, AtLayer -from gen._surf._closed_curve import ClosedCurve -from gen._surf._order import Order_Z -from gen._surf._patch_outline import PatchOutline -from gen._surf._patch_transition_outline import PatchTransitionOutline -from gen._surf._step_outline import StepOutline -from gen._util import write_file - - -class StepSequenceOutline: - def __init__(self, steps: Iterable[StepOutline]): - self.steps = list(steps) - - def to_circuit( - self, *, rel_order_func: Callable[[complex], Iterable[complex]] - ) -> stim.Circuit: - builder = Builder.for_qubits(...) - - round_index = 0 - cmp_layer = None - save_layer = 0 - for step in self.steps: - step.build_rounds( - builder=builder, - rel_order_func=rel_order_func, - alternate_ordering_with_round_parity=True, - start_round_index=round_index, - edit_cur_obs=None, - o2i=None, - cmp_layer=cmp_layer, - save_layer=("step_to_circuit", save_layer), - ) - cmp_layer = save_layer - round_index += step.rounds - - return builder.circuit - - def write_outlines_svg(self, path: str | pathlib.Path) -> None: - from gen._surf._viz_patch_outline_svg import patch_outline_svg_viewer - - viewer = patch_outline_svg_viewer( - [ - piece - for segment in self.steps - for piece in [segment.start, segment.body, segment.end] - if any(piece.iter_control_points()) - ] - ) - write_file(path, viewer) - - def write_patches_svg( - self, - path: str | pathlib.Path, - *, - rel_order_func: Callable[[complex], Iterable[complex]] = None, - ) -> None: - from gen._viz_patch_svg import patch_svg_viewer - - show_order = True - if rel_order_func is None: - rel_order_func = lambda _: Order_Z - show_order = False - patches = [] - for k in range(len(self.steps)): - segment = self.steps[k] - patch = segment.body.to_patch(rel_order_func=rel_order_func) - if segment.start.data_set: - b2 = segment.start.data_basis_map - tiles = list(segment.start.patch.tiles) - for t in patch.tiles: - if all( - b2.get(q, b) == b - for q, b in t.to_data_pauli_string().qubits.items() - ): - tiles.append(t) - patches.append(gen.Patch(tiles)) - patches.append(patch) - if segment.end.data_set: - b2 = segment.end.data_basis_map - tiles = list(segment.end.patch.tiles) - for t in patch.tiles: - if all( - b2.get(q, b) == b - for q, b in t.to_data_pauli_string().qubits.items() - ): - tiles.append(t) - patches.append(gen.Patch(tiles)) - viewer = patch_svg_viewer( - patches, show_order=show_order, show_measure_qubits=False - ) - write_file(path, viewer) - - def write_gltf(self, path: str | pathlib.Path) -> None: - from gen._surf._viz_sequence_3d import patch_sequence_to_model - - print(f"wrote file://{pathlib.Path(path).absolute()}") - patch_sequence_to_model(self).save_json(str(path)) - - def write_3d_viewer_html(self, path: str | pathlib.Path) -> None: - from gen._surf._viz_gltf_3d import viz_3d_gltf_model_html - from gen._surf._viz_sequence_3d import patch_sequence_to_model - - write_file(path, viz_3d_gltf_model_html(patch_sequence_to_model(self))) diff --git a/src/gen/_surf/_surface_code.py b/src/gen/_surf/_surface_code.py deleted file mode 100644 index 11b62bd..0000000 --- a/src/gen/_surf/_surface_code.py +++ /dev/null @@ -1,350 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import AbstractSet, Callable, Any, Literal - -from gen._core._builder import Builder, AtLayer -from gen._core._patch import Patch -from gen._core._tile import Tile -from gen._core._util import sorted_complex -from gen._surf._closed_curve import ClosedCurve -from gen._surf._patch_outline import PatchOutline -from gen._surf._surface_code_legacy import measure_patch_legacy - - -def surface_code_patch( - *, - width: int, - height: int, - top_basis: Literal["X", "Z"], - bot_basis: Literal["X", "Z"], - left_basis: Literal["X", "Z"], - right_basis: Literal["X", "Z"], - rel_order_func: Callable[[complex], tuple[complex, ...]], -) -> Patch: - """Generates the stabilizer configuration for a surface code patch.""" - - c = ClosedCurve.from_cycle( - [ - top_basis, - width - 1, - right_basis, - width + height * 1j - 1 - 1j, - bot_basis, - height * 1j - 1j, - left_basis, - 0, - ] - ) - return PatchOutline([c]).to_patch(rel_order_func=rel_order_func) - - -def layer_loop( - *, - builder: Builder, - patch: Patch, - style: Literal["css", "mpp", "cz"], - compare_layer: Any, - save_layer: Any, - repetitions: int, - mark_as_post_selected: Callable[[AtLayer], bool] = lambda _: False, -) -> None: - if repetitions == 0: - for tile in patch.tiles: - m = tile.measurement_qubit - builder.tracker.make_measurement_group( - [AtLayer(m, compare_layer)], key=AtLayer(m, save_layer) - ) - return - - measure_index_before_first_iteration = builder.tracker.next_measurement_index - loop_start_layer = ("loop_start", save_layer) - - # Perform first iteration of loop. - child = builder.fork() - measure_patch_legacy( - builder=child, - patch=patch, - save_layer=loop_start_layer, - style=style, - ) - for tile in patch.tiles: - m = tile.measurement_qubit - child.detector( - [AtLayer(m, compare_layer), AtLayer(m, loop_start_layer)], - pos=m, - mark_as_post_selected=mark_as_post_selected(AtLayer(m, save_layer)), - ) - child.circuit.append("SHIFT_COORDS", [], [0, 0, 1]) - child.tick() - - # Update tracker to contain last iteration information at desired save index. - num_measurements_in_loop = ( - builder.tracker.next_measurement_index - measure_index_before_first_iteration - ) - first_to_last_offset = num_measurements_in_loop * (repetitions - 1) - for tile in patch.tiles: - m = tile.measurement_qubit - (start,) = builder.tracker.recorded[AtLayer(m, loop_start_layer)] - builder.tracker.recorded[AtLayer(m, save_layer)] = [ - start + first_to_last_offset - ] - builder.tracker.next_measurement_index += first_to_last_offset - - # Append loop body to circuit. - builder.circuit += child.circuit * repetitions - - -def layer_single_shot( - *, - builder: Builder, - patch: Patch, - style: Literal["cz", "css", "mpp"], - data_basis: str | Callable[[complex], str], - data_obs_qubit_sets: dict[int, AbstractSet[complex]] | None = None, -) -> None: - if isinstance(data_basis, str): - fixed_data_basis = data_basis - data_basis = lambda _: fixed_data_basis - dq = {q: data_basis(q) for q in patch.data_set} - measure_patch_legacy( - patch=patch, - style=style, - builder=builder, - data_resets=dq, - data_measures=dq, - save_layer="single", - ) - - def matches_basis(t: Tile) -> bool: - for b, d in zip(t.bases, t.ordered_data_qubits): - if d is not None and b != data_basis(d): - return False - return True - - for tile in patch.tiles: - if matches_basis(tile): - builder.detector( - [ - AtLayer(tile.measurement_qubit, "single"), - ], - pos=tile.measurement_qubit, - ) - for tile in patch.tiles: - if matches_basis(tile): - builder.detector( - [AtLayer(q, "single") for q in tile.used_set], - pos=tile.measurement_qubit, - t=0.5, - ) - for index, obs_qubits in data_obs_qubit_sets.items(): - builder.obs_include([AtLayer(q, "single") for q in obs_qubits], obs_index=index) - - -def layer_begin( - *, - builder: Builder, - style: Literal["cz", "css", "mpp"], - patch: Patch, - save_layer: Any, - obs_key_sets: dict[int, AbstractSet[complex]] | None = None, - reset_basis: str | Callable[[complex], str], - mark_as_post_selected: Callable[[AtLayer], bool] = lambda _: False, -) -> None: - layer_transition( - builder=builder, - style=style, - kept_data_qubits=set(), - past_patch=None, - past_save_layer=None, - past_compare_layer=None, - past_layer_lost_data_measure_basis=None, - past_layer_lost_data_obs_qubit_sets=None, - future_patch=patch, - future_save_layer=save_layer, - future_layer_obs_key_sets=obs_key_sets, - future_layer_gain_data_reset_basis=reset_basis, - mark_as_post_selected=mark_as_post_selected, - ) - - -def layer_end( - *, - builder: Builder, - style: Literal["cz", "css", "mpp"], - patch: Patch, - save_layer: Any, - compare_layer: Any, - data_measure_basis: str | Callable[[complex], str], - data_obs_qubit_sets: dict[int, AbstractSet[complex]] | None = None, - mark_as_post_selected: Callable[[AtLayer], bool] = lambda _: False, -) -> None: - layer_transition( - builder=builder, - style=style, - kept_data_qubits=set(), - past_patch=patch, - past_save_layer=save_layer, - past_compare_layer=compare_layer, - past_layer_lost_data_measure_basis=data_measure_basis, - past_layer_lost_data_obs_qubit_sets=data_obs_qubit_sets, - future_patch=None, - future_save_layer=None, - future_layer_obs_key_sets=None, - future_layer_gain_data_reset_basis=None, - mark_as_post_selected=mark_as_post_selected, - ) - - -def layer_transition( - *, - builder: Builder, - style: Literal["css", "mpp"], - kept_data_qubits: set[complex] | frozenset[complex], - past_patch: Patch | None, - past_save_layer: Any | None, - past_compare_layer: Any | None, - past_layer_lost_data_measure_basis: None | str | Callable[[complex], str], - past_layer_lost_data_obs_qubit_sets: dict[int, AbstractSet[complex]] | None = None, - future_patch: Patch | None, - future_save_layer: Any | None, - future_layer_obs_key_sets: dict[int, AbstractSet[complex]] | None = None, - future_layer_gain_data_reset_basis: None | str | Callable[[complex], str], - mark_as_post_selected: Callable[[AtLayer], bool] = lambda _: False, -) -> None: - assert future_patch is not None or past_patch is not None - assert ( - (future_save_layer is None) - == (future_patch is None) - == (future_layer_gain_data_reset_basis is None) - ) - assert ( - (past_save_layer is None) - == (past_patch is None) - == (past_compare_layer is None) - == (past_layer_lost_data_measure_basis is None) - ) - if future_patch is None: - future_patch = Patch([]) - if past_patch is None: - past_patch = Patch([]) - if isinstance(past_layer_lost_data_measure_basis, str): - fixed_future_time_basis = past_layer_lost_data_measure_basis - past_layer_lost_data_measure_basis = lambda _: fixed_future_time_basis - if isinstance(future_layer_gain_data_reset_basis, str): - fixed_past_time_basis = future_layer_gain_data_reset_basis - future_layer_gain_data_reset_basis = lambda _: fixed_past_time_basis - - assert kept_data_qubits <= past_patch.data_set - assert kept_data_qubits <= future_patch.data_set, repr( - sorted_complex(kept_data_qubits - future_patch.data_set) - ) - lost_data = past_patch.data_set - kept_data_qubits - gained_data = future_patch.data_set - kept_data_qubits - - def matches_past_basis(tile: Tile) -> bool: - for b, d in zip(tile.bases, tile.ordered_data_qubits): - if ( - d is not None - and d in gained_data - and b != future_layer_gain_data_reset_basis(d) - ): - return False - return True - - def matches_future_basis(tile: Tile) -> bool: - for b, d in zip(tile.bases, tile.ordered_data_qubits): - if ( - d is not None - and d in lost_data - and b != past_layer_lost_data_measure_basis(d) - ): - return False - return True - - if past_patch.tiles: - measure_patch_legacy( - style=style, - patch=past_patch, - builder=builder, - data_measures={q: past_layer_lost_data_measure_basis(q) for q in lost_data}, - save_layer=past_save_layer, - ) - for prev_tile in past_patch.tiles: - m = prev_tile.measurement_qubit - builder.detector( - [AtLayer(m, past_compare_layer), AtLayer(m, past_save_layer)], - pos=m, - mark_as_post_selected=mark_as_post_selected( - AtLayer(m, past_save_layer) - ), - ) - for prev_tile in past_patch.tiles: - m = prev_tile.measurement_qubit - if matches_future_basis(prev_tile) and prev_tile.data_set.isdisjoint( - kept_data_qubits - ): - builder.detector( - [AtLayer(q, past_save_layer) for q in prev_tile.used_set], - pos=m, - t=0.5, - ) - if past_layer_lost_data_obs_qubit_sets: - for obs_index, obs_qubits in past_layer_lost_data_obs_qubit_sets.items(): - builder.obs_include( - [AtLayer(q, past_save_layer) for q in lost_data & obs_qubits], - obs_index=obs_index, - ) - builder.shift_coords(dt=1) - builder.tick() - - if future_patch.tiles: - measure_patch_legacy( - patch=future_patch, - style=style, - builder=builder, - data_resets={q: future_layer_gain_data_reset_basis(q) for q in gained_data}, - save_layer=future_save_layer, - ) - for next_tile in future_patch.tiles: - if not matches_past_basis(next_tile): - # Anticommutes with time boundary. - continue - - m = next_tile.measurement_qubit - comparison = [AtLayer(m, future_save_layer)] - prev_tile = past_patch.m2tile.get(m) - if prev_tile is not None and not prev_tile.data_set.isdisjoint( - kept_data_qubits - ): - comparison.append(AtLayer(m, past_save_layer)) - for q in prev_tile.data_set: - if q not in kept_data_qubits: - comparison.append(AtLayer(q, past_save_layer)) - builder.detector( - comparison, - pos=m, - mark_as_post_selected=mark_as_post_selected( - AtLayer(m, future_save_layer) - ), - ) - - builder.shift_coords(dt=1) - if future_layer_obs_key_sets: - for obs_index, obs_qubits in future_layer_obs_key_sets.items(): - builder.obs_include( - [AtLayer(q, future_save_layer) for q in obs_qubits], - obs_index=obs_index, - ) - builder.tick() diff --git a/src/gen/_surf/_surface_code_legacy.py b/src/gen/_surf/_surface_code_legacy.py deleted file mode 100644 index 918f81b..0000000 --- a/src/gen/_surf/_surface_code_legacy.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Callable, Any, Literal, AbstractSet, Iterable - -from gen._layers import transpile_to_z_basis_interaction_circuit -from gen._core import Patch, Builder, AtLayer, PauliString, complex_key, sorted_complex - - -def _measure_css( - *, - patch: Patch, - data_resets: dict[complex, str], - data_measures: dict[complex, str], - builder: Builder, - tracker_key: Callable[[complex], Any], - tracker_layer: float, -) -> None: - assert patch.measure_set.isdisjoint(data_resets) - if not patch.tiles: - return - - x_tiles = [tile for tile in patch.tiles if tile.basis == "X"] - z_tiles = [tile for tile in patch.tiles if tile.basis == "Z"] - other_tiles = [ - tile for tile in patch.tiles if tile.basis != "X" and tile.basis != "Z" - ] - reset_bases = { - "X": [tile.measurement_qubit for tile in x_tiles], - "Y": [], - "Z": [tile.measurement_qubit for tile in z_tiles + other_tiles], - } - for q, b in data_resets.items(): - reset_bases[b].append(q) - for b, qs in reset_bases.items(): - if qs: - builder.gate(f"R{b}", qs) - builder.tick() - - (num_layers,) = {len(e.ordered_data_qubits) for e in patch.tiles} - for k in range(num_layers): - pairs = [] - for tile in x_tiles: - q = tile.ordered_data_qubits[k] - if q is not None: - pairs.append((tile.measurement_qubit, q)) - for tile in z_tiles: - q = tile.ordered_data_qubits[k] - if q is not None: - pairs.append((q, tile.measurement_qubit)) - builder.gate2("CX", pairs) - for tile in sorted( - other_tiles, key=lambda tile: complex_key(tile.measurement_qubit) - ): - q = tile.ordered_data_qubits[k] - b = tile.bases[k] - if q is not None: - builder.gate2(f"{b}CX", [(q, tile.measurement_qubit)]) - builder.tick() - - measure_bases = { - "X": [tile.measurement_qubit for tile in x_tiles], - "Y": [], - "Z": [tile.measurement_qubit for tile in z_tiles + other_tiles], - } - for q, b in data_measures.items(): - measure_bases[b].append(q) - for b, qs in measure_bases.items(): - if qs: - builder.measure( - qs, - basis=b, - tracker_key=tracker_key, - save_layer=tracker_layer, - ) - - -def _measure_cz( - *, - patch: Patch, - data_resets: dict[complex, str], - data_measures: dict[complex, str], - builder: Builder, - tracker_key: Callable[[complex], Any], - tracker_layer: float, -) -> None: - out = builder.fork() - _measure_css( - patch=patch, - data_resets=data_resets, - data_measures=data_measures, - builder=out, - tracker_key=tracker_key, - tracker_layer=tracker_layer, - ) - builder.circuit += transpile_to_z_basis_interaction_circuit( - out.circuit, is_entire_circuit=False - ) - - -def _measure_mpp( - patch, - *, - data_resets: dict[complex, str], - data_measures: dict[complex, str], - builder: Builder, - tracker_key: Callable[[complex], Any], - tracker_layer: float, -) -> None: - assert patch.measure_set.isdisjoint(data_resets) - - if data_resets: - for b in "XYZ": - builder.gate(f"R{b}", {q for q, db in data_resets.items() if b == db}) - - for v in patch.tiles: - builder.measure_pauli_string( - PauliString( - {q: b for q, b in zip(v.ordered_data_qubits, v.bases) if q is not None} - ), - key=AtLayer(tracker_key(v.measurement_qubit), tracker_layer), - ) - - if data_measures: - for b in "XYZ": - builder.measure( - {q for q, db in data_measures.items() if b == db}, - basis=b, - tracker_key=tracker_key, - save_layer=tracker_layer, - ) - - -def measure_patch_legacy( - patch, - *, - data_resets: dict[complex, str] | None = None, - data_measures: dict[complex, str] | None = None, - builder: Builder, - tracker_key: Callable[[complex], Any] = lambda e: e, - save_layer: Any, - style: Literal["css", "cz", "mpp"] = "cz", -) -> None: - if data_resets is None: - data_resets = {} - if data_measures is None: - data_measures = {} - if style == "css": - _measure_css( - patch=patch, - data_resets=data_resets, - data_measures=data_measures, - builder=builder, - tracker_key=tracker_key, - tracker_layer=save_layer, - ) - elif style == "cz": - _measure_cz( - patch=patch, - data_resets=data_resets, - data_measures=data_measures, - builder=builder, - tracker_key=tracker_key, - tracker_layer=save_layer, - ) - elif style == "mpp": - _measure_mpp( - patch=patch, - data_resets=data_resets, - data_measures=data_measures, - builder=builder, - tracker_key=tracker_key, - tracker_layer=save_layer, - ) - else: - raise NotImplementedError(f"{style=}") - - -def measure_patch_legacy_detect( - patch, - *, - comparison_overrides: dict[Any, list[Any] | None] | None = None, - skipped_comparisons: Iterable[Any] = (), - singleton_detectors: Iterable[Any] = (), - data_resets: dict[complex, str] | None = None, - data_measures: dict[complex, str] | None = None, - builder: Builder, - repetitions: int | None = None, - tracker_key: Callable[[complex], Any] = lambda e: e, - cmp_layer: Any | None, - save_layer: Any, - tracker_layer_last_rep: Any | None = None, - post_selected_positions: AbstractSet[complex] = frozenset(), - style: Literal["css", "cz", "mpp"] = "cz", -) -> None: - if data_resets is None: - data_resets = {} - if data_measures is None: - data_measures = {} - assert (repetitions is not None) == (tracker_layer_last_rep is not None) - if repetitions is not None: - assert not data_resets - assert not data_measures - if repetitions == 0: - for plaq in patch.tiles: - m = plaq.measurement_qubit - builder.tracker.make_measurement_group( - [AtLayer(m, cmp_layer)], key=AtLayer(m, tracker_layer_last_rep) - ) - return - - child = builder.fork() - pm = builder.tracker.next_measurement_index - measure_patch_legacy( - patch=patch, - data_resets=data_resets, - data_measures=data_measures, - builder=child, - tracker_key=tracker_key, - save_layer=save_layer, - style=style, - ) - num_measurements = builder.tracker.next_measurement_index - pm - - if comparison_overrides is None: - comparison_overrides = {} - assert patch.measure_set.isdisjoint(data_resets) - skipped_comparisons_set = frozenset(skipped_comparisons) - singleton_detectors_set = frozenset(singleton_detectors) - for e in sorted_complex(patch.tiles, key=lambda e2: e2.measurement_qubit): - if all(e is None for e in e.ordered_data_qubits): - continue - failed = False - for q, b in zip(e.ordered_data_qubits, e.bases): - if q is not None and data_resets.get(q, b) != b: - failed = True - if failed: - continue - m = e.measurement_qubit - if m in skipped_comparisons_set: - continue - if m in singleton_detectors_set: - comparisons = [] - elif cmp_layer is not None: - comparisons = comparison_overrides.get(m, [AtLayer(m, cmp_layer)]) - else: - comparisons = [] - if comparisons is None: - continue - assert isinstance( - comparisons, list - ), f"Vs exception must be a list but got {comparisons!r} for {m!r}" - child.detector( - [AtLayer(m, save_layer), *comparisons], - pos=m, - mark_as_post_selected=m in post_selected_positions, - ) - child.circuit.append("SHIFT_COORDS", [], [0, 0, 1]) - specified_reps = repetitions is not None - if repetitions is None: - repetitions = 1 - if specified_reps: - child.tick() - - if repetitions > 1 or tracker_layer_last_rep is not None: - if tracker_layer_last_rep is None: - raise ValueError("repetitions > 1 and tracker_layer_last_rep is None") - offset = num_measurements * (repetitions - 1) - builder.tracker.next_measurement_index += offset - for m in data_measures.keys() | patch.measure_set: - builder.tracker.recorded[AtLayer(m, tracker_layer_last_rep)] = [ - e + offset for e in builder.tracker.recorded[AtLayer(m, save_layer)] - ] - builder.circuit += child.circuit * repetitions diff --git a/src/gen/_surf/_surface_code_test.py b/src/gen/_surf/_surface_code_test.py deleted file mode 100644 index 390585e..0000000 --- a/src/gen/_surf/_surface_code_test.py +++ /dev/null @@ -1,338 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import gen -from gen._surf._patch_outline import PatchOutline -from gen._surf._closed_curve import ClosedCurve -from gen._surf._order import Order_Z, Order_ᴎ, checkerboard_basis -from gen._surf._surface_code import layer_transition, surface_code_patch -from gen._core._builder import Builder -from gen._core._pauli_string import PauliString - - -def test_surface_code_patch(): - patch = surface_code_patch( - width=5, - height=5, - top_basis="Z", - bot_basis="Z", - left_basis="X", - right_basis="X", - rel_order_func=lambda _: Order_Z, - ) - assert len(patch.data_set) == 25 - assert len(patch.tiles) == 24 - - -def test_layer_transition_notched_shift(): - c0 = ClosedCurve.from_cycle( - [ - "Z", - 6 + 0j, - "X", - 6 + 6j, - "Z", - 0 + 6j, - "X", - 0 + 0j, - ] - ) - p0 = PatchOutline([c0]).to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - p2 = PatchOutline([c0.offset_by(2)]).to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - p3 = PatchOutline([c0.offset_by(3)]).to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - - builder = Builder.for_qubits(p0.used_set | p3.used_set | {-1}) - - builder.measure_pauli_string( - PauliString.from_xyzs(zs=[q for q in p0.data_set if q.real == 4] + [-1]), - key="H_INIT", - ) - builder.obs_include(["H_INIT"], obs_index=2) - builder.tick() - builder.measure_pauli_string( - PauliString.from_xyzs(xs=[q for q in p0.data_set if q.imag == 0] + [-1]), - key="V_INIT", - ) - builder.obs_include(["V_INIT"], obs_index=5) - builder.tick() - - builder.measure_patch(patch=p0, save_layer=4) - - builder.tick() - layer_transition( - builder=builder, - past_patch=p0, - future_patch=p2, - kept_data_qubits=p0.data_set & p2.data_set & p3.data_set, - style="mpp", - past_compare_layer=4, - past_save_layer=5, - future_save_layer=6, - past_layer_lost_data_obs_qubit_sets={ - 2: {q for q in p0.data_set | p3.data_set if q.real == 4}, - 5: {q for q in p0.data_set | p3.data_set if q.imag == 0}, - }, - future_layer_gain_data_reset_basis="X", - past_layer_lost_data_measure_basis="X", - ) - - builder.measure_pauli_string( - PauliString.from_xyzs(zs=[q for q in p2.data_set if q.real == 4] + [-1]), - key="H_OUT", - ) - builder.obs_include(["H_OUT"], obs_index=2) - builder.tick() - builder.measure_pauli_string( - PauliString.from_xyzs(xs=[q for q in p2.data_set if q.imag == 0] + [-1]), - key="V_OUT", - ) - builder.obs_include(["V_OUT"], obs_index=5) - builder.tick() - - # Verify that all detectors and observables are deterministic. - builder.circuit.detector_error_model(decompose_errors=True) - - -def test_layer_transition_shrink(): - c_shrunk = ClosedCurve.from_cycle( - [ - "X", - 3 + 0j, - "Z", - 3 + 6j, - "X", - 0 + 6j, - "Z", - 0 + 0j, - ] - ) - - c_full = ClosedCurve.from_cycle( - [ - "X", - 6 + 0j, - "Z", - 6 + 6j, - "X", - 0 + 6j, - "Z", - 0 + 0j, - ] - ) - - p_shrunk = PatchOutline([c_shrunk]).to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - p_full = PatchOutline([c_full]).to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - - builder = Builder.for_qubits(p_full.used_set | {-1}) - - builder.measure_pauli_string( - gen.PauliString.from_xyzs( - xs=[q for q in p_full.data_set if q.real == 0] + [-1] - ), - key="H_INIT", - ) - builder.obs_include(["H_INIT"], obs_index=2) - builder.tick() - builder.measure_pauli_string( - gen.PauliString.from_xyzs( - zs=[q for q in p_full.data_set if q.imag == 0] + [-1] - ), - key="V_INIT", - ) - builder.obs_include(["V_INIT"], obs_index=5) - builder.tick() - - builder.measure_patch(patch=p_full, save_layer=4) - - builder.tick() - layer_transition( - builder=builder, - past_patch=p_full, - future_patch=p_shrunk, - kept_data_qubits=p_shrunk.data_set, - style="mpp", - past_compare_layer=4, - past_save_layer=5, - future_save_layer=6, - past_layer_lost_data_obs_qubit_sets={ - 2: {q for q in p_full.data_set if q.real == 0}, - 5: {q for q in p_full.data_set if q.imag == 0}, - }, - past_layer_lost_data_measure_basis="Z", - future_layer_gain_data_reset_basis="Z", - ) - - builder.measure_pauli_string( - PauliString.from_xyzs(xs=[q for q in p_shrunk.data_set if q.real == 0] + [-1]), - key="H_OUT", - ) - builder.obs_include(["H_OUT"], obs_index=2) - builder.tick() - builder.measure_pauli_string( - PauliString.from_xyzs(zs=[q for q in p_shrunk.data_set if q.imag == 0] + [-1]), - key="V_OUT", - ) - builder.obs_include(["V_OUT"], obs_index=5) - builder.tick() - - # Verify that all detectors and observables are deterministic. - builder.circuit.detector_error_model(decompose_errors=True) - - -def test_layer_transition_full_notch(): - c = ClosedCurve.from_cycle( - [ - "X", - 3 + 0j, - "Z", - 3 + 3j, - "X", - 0 + 3j, - "Z", - 0 + 0j, - ] - ) - - p = PatchOutline([c]).to_patch( - rel_order_func=lambda m: Order_Z if checkerboard_basis(m) == "Z" else Order_ᴎ - ) - builder = Builder.for_qubits(p.used_set) - builder.measure_patch(patch=p, save_layer=4) - builder.tick() - layer_transition( - builder=builder, - past_patch=p, - future_patch=p, - kept_data_qubits=set(), - style="mpp", - past_compare_layer=4, - past_save_layer=5, - future_save_layer=6, - past_layer_lost_data_obs_qubit_sets={}, - past_layer_lost_data_measure_basis="Z", - future_layer_gain_data_reset_basis="Z", - ) - - # Verify that all detectors and observables are deterministic. - builder.circuit.detector_error_model(decompose_errors=True) - assert builder.circuit.num_detectors == 7 * 3 + 8 - - -def test_fused_inner(): - b1 = PatchOutline( - [ - ClosedCurve( - points=[ - 0 + 16j, - 6 + 16j, - 8 + 16j, - 14 + 16j, - 16 + 16j, - 22 + 16j, - 22 + 18j, - 22 + 20j, - 22 + 22j, - 22 + 24j, - 22 + 26j, - 22 + 28j, - 22 + 30j, - 16 + 30j, - 16 + 28j, - 16 + 26j, - 14 + 26j, - 8 + 26j, - 8 + 24j, - 14 + 24j, - 16 + 24j, - 16 + 22j, - 16 + 20j, - 16 + 18j, - 14 + 18j, - 8 + 18j, - 6 + 18j, - 6 + 20j, - 8 + 20j, - 14 + 20j, - 14 + 22j, - 8 + 22j, - 6 + 22j, - 6 + 24j, - 6 + 26j, - 6 + 28j, - 8 + 28j, - 14 + 28j, - 14 + 30j, - 8 + 30j, - 6 + 30j, - 30j, - 28j, - 26j, - 24j, - 22j, - 20j, - 18j, - ], - bases="XXXXXXXXXXXXXXXXXXZXXXXXXXXXXXZXXXXXXXZXXXXXXXXX", - ) - ] - ) - b2 = b1.fused(19 + 29j, 11 + 29j) - - assert len(b2.region_curves) == 2 - assert all(e == "X" for e in b2.region_curves[0].bases) - assert b2.region_curves[1] == ClosedCurve( - points=[ - (16 + 28j), - (16 + 26j), - (14 + 26j), - (8 + 26j), - (8 + 24j), - (14 + 24j), - (16 + 24j), - (16 + 22j), - (16 + 20j), - (16 + 18j), - (14 + 18j), - (8 + 18j), - (6 + 18j), - (6 + 20j), - (8 + 20j), - (14 + 20j), - (14 + 22j), - (8 + 22j), - (6 + 22j), - (6 + 24j), - (6 + 26j), - (6 + 28j), - (8 + 28j), - (14 + 28j), - ], - bases="XXXXZXXXXXXXXXXXZXXXXXXX", - ) - - -def test_distance_2(): - curve = ClosedCurve(points=[1, (1 + 1j), 1j, 0], bases="ZXZX") - plan = PatchOutline([curve]).to_patch(rel_order_func=lambda _: Order_Z) - assert len(plan.tiles) == 3 diff --git a/src/gen/_surf/_trans.py b/src/gen/_surf/_trans.py deleted file mode 100644 index d1b2035..0000000 --- a/src/gen/_surf/_trans.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import AbstractSet, Callable, Any, Literal - -from gen._core._util import sorted_complex -from gen._core._builder import Builder, AtLayer -from gen._core._patch import Patch, Tile -from gen._surf._surface_code_legacy import measure_patch_legacy - - -def build_patch_to_patch_surface_code_transition_rounds( - *, - builder: Builder, - first_layer: Patch, - second_layer: Patch, - style: Literal["css", "mpp"], - cmp_key_prev_layer: Any, - save_key_first_layer: Any, - save_key_second_layer: Any, - kept_data_qubits: AbstractSet[complex], - obs_qubit_sets: dict[int, AbstractSet[complex]], - first_layer_data_measure_basis: None | str | Callable[[complex], str], - second_layer_data_init_basis: None | str | Callable[[complex], str], -): - if isinstance(first_layer_data_measure_basis, str): - fixed_future_time_basis = first_layer_data_measure_basis - first_layer_data_measure_basis = lambda _: fixed_future_time_basis - if isinstance(second_layer_data_init_basis, str): - fixed_past_time_basis = second_layer_data_init_basis - second_layer_data_init_basis = lambda _: fixed_past_time_basis - - assert kept_data_qubits <= first_layer.data_set - assert kept_data_qubits <= second_layer.data_set, repr( - sorted_complex(kept_data_qubits - second_layer.data_set) - ) - lost_data = first_layer.data_set - kept_data_qubits - gained_data = second_layer.data_set - kept_data_qubits - - def matches_1st_layer_measure_basis(tile: Tile) -> bool: - for b, d in zip(tile.bases, tile.ordered_data_qubits): - if ( - d is not None - and d in lost_data - and b != first_layer_data_measure_basis(d) - ): - return False - return True - - if first_layer.tiles: - measure_patch_legacy( - patch=first_layer, - style=style, - builder=builder, - data_measures={q: first_layer_data_measure_basis(q) for q in lost_data}, - save_layer=save_key_first_layer, - ) - - # Detectors from comparing to previous round. - for prev_tile in first_layer.tiles: - m = prev_tile.measurement_qubit - builder.detector( - [AtLayer(m, cmp_key_prev_layer), AtLayer(m, save_key_first_layer)], - pos=m, - ) - - # Detectors from comparing data measurements to stabilizer measurements. - for prev_tile in first_layer.tiles: - m = prev_tile.measurement_qubit - if matches_1st_layer_measure_basis( - prev_tile - ) and prev_tile.data_set.isdisjoint(kept_data_qubits): - builder.detector( - [AtLayer(q, save_key_first_layer) for q in prev_tile.used_set], - pos=m, - t=0.5, - ) - - for obs_index, obs_qubits in obs_qubit_sets.items(): - builder.obs_include( - [AtLayer(q, save_key_first_layer) for q in lost_data & obs_qubits], - obs_index=obs_index, - ) - builder.shift_coords(dt=1) - builder.tick() - - def matches_2nd_layer_init_basis(tile: Tile) -> bool: - for b, d in zip(tile.bases, tile.ordered_data_qubits): - if ( - d is not None - and d in gained_data - and b != second_layer_data_init_basis(d) - ): - return False - return True - - if second_layer.tiles: - measure_patch_legacy( - patch=second_layer, - style=style, - builder=builder, - data_resets={q: second_layer_data_init_basis(q) for q in gained_data}, - save_layer=save_key_second_layer, - ) - - # Detectors from comparing to first layer and/or data resets. - for next_tile in second_layer.tiles: - if not matches_2nd_layer_init_basis(next_tile): - # Anticommutes with time boundary. - continue - - m = next_tile.measurement_qubit - comparison = [AtLayer(m, save_key_second_layer)] - prev_tile = first_layer.m2tile.get(m) - if prev_tile is not None and not prev_tile.data_set.isdisjoint( - kept_data_qubits - ): - comparison.append(AtLayer(m, save_key_first_layer)) - for q in prev_tile.data_set: - if q not in kept_data_qubits: - comparison.append(AtLayer(q, save_key_first_layer)) - builder.detector(comparison, pos=m) - - builder.shift_coords(dt=1) - builder.tick() diff --git a/src/gen/_surf/_viz_patch_outline_svg.py b/src/gen/_surf/_viz_patch_outline_svg.py deleted file mode 100644 index c71ee39..0000000 --- a/src/gen/_surf/_viz_patch_outline_svg.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -from typing import Iterable, TYPE_CHECKING, Literal, Union - -from gen._core._util import min_max_complex - -if TYPE_CHECKING: - from gen._surf._patch_outline import PatchOutline - from gen._surf._patch_transition_outline import PatchTransitionOutline - - -def patch_outline_svg_viewer( - values: Iterable[Union["PatchOutline", "PatchTransitionOutline"]], - *, - other_segments: Iterable[tuple[complex, Literal["X", "Y", "Z"], complex]] = (), - canvas_height: int = 500, -) -> str: - """Returns a picture of the stabilizers measured by various plan.""" - from gen._surf._patch_outline import PatchOutline - from gen._surf._patch_transition_outline import PatchTransitionOutline - - outlines = tuple(values) - control_points = [ - pt for outline in outlines for pt in outline.iter_control_points() - ] - min_c, max_c = min_max_complex(control_points, default=0) - min_c -= 1 + 1j - max_c += 1 + 1j - box_width = max_c.real - min_c.real - box_height = max_c.imag - min_c.imag - pad = max(box_width, box_height) * 0.1 + 1 - box_width += pad - box_height += pad - columns = math.ceil(math.sqrt(len(outlines))) - rows = math.ceil(len(outlines) / max(1, columns)) - total_height = max(1.0, box_height * rows - pad) - total_width = max(1.0, box_width * columns - pad) - scale_factor = canvas_height / max(total_height, 1) - canvas_width = int(math.ceil(canvas_height * (total_width / total_height))) - stroke_width = scale_factor / 25 - - def transform_pt(plan_i2: int, pt2: complex) -> complex: - pt2 += box_width * (plan_i2 % columns) - pt2 += box_height * (plan_i2 // columns) * 1j - pt2 += pad * (0.5 + 0.5j) - pt2 *= scale_factor - return pt2 - - lines = [ - f"""""" - ] - - BASE_COLORS = {"X": "#FF0000", "Z": "#0000FF", "Y": "#00FF00", None: "gray"} - OBS_COLORS = {"X": "#800000", "Z": "#000080", "Y": "#008000", None: "black"} - - lines.append( - f'' - ) - lines.append( - "X" - ) - - lines.append( - f'' - ) - lines.append( - "Y" - ) - - lines.append( - f'' - ) - lines.append( - "Z" - ) - - # Draw interior. - for outline_index, outline in enumerate(outlines): - pieces = [] - if isinstance(outline, PatchOutline): - for curve in outline.region_curves: - pieces.append("M") - for k in range(len(curve)): - a = transform_pt(outline_index, curve.points[k]) - pieces.append(f"{a.real},{a.imag}") - pieces.append("Z") - path = " ".join(pieces) - lines.append(f'') - elif isinstance(outline, PatchTransitionOutline): - fill_color = "#000" - for curve in outline.data_boundary_planes: - fill_color = BASE_COLORS[curve.basis] - pieces.append("M") - for k in range(len(curve)): - a = transform_pt(outline_index, curve.points[k]) - pieces.append(f"{a.real},{a.imag}") - pieces.append("Z") - path = " ".join(pieces) - lines.append(f'') - else: - raise NotImplementedError(f"{outline=}") - - # Trace boundaries. - for outline_index, outline in enumerate(outlines): - if isinstance(outline, PatchOutline): - for curve in outline.region_curves: - for k in range(len(curve)): - a = transform_pt(outline_index, curve.points[k - 1]) - b = transform_pt(outline_index, curve.points[k]) - stroke_color = BASE_COLORS[curve.bases[k]] - lines.append( - f"' - ) - for k, obs_pair in enumerate(outline.observables): - k -= len(outline.observables) / 2 - for basis, obs in obs_pair: - for a, b, _basis in obs.segments: - a = transform_pt(outline_index, a) - b = transform_pt(outline_index, b) - stroke_color = OBS_COLORS[basis] - lines.append( - f"' - ) - for a, basis, b in other_segments: - a = transform_pt(outline_index, a) - b = transform_pt(outline_index, b) - stroke_color = OBS_COLORS[basis] - lines.append( - f"' - ) - - # Trace control points. - for outline_index, outline in enumerate(outlines): - for pt in outline.iter_control_points(): - a = transform_pt(outline_index, pt) - lines.append( - f"' - ) - - # Frames - for outline_index, outline in enumerate(outlines): - a = transform_pt(outline_index, min_c) - b = transform_pt(outline_index, max_c) - lines.append( - f'' - ) - - lines.append("") - return "\n".join(lines) diff --git a/src/gen/_surf/_viz_sequence_3d.py b/src/gen/_surf/_viz_sequence_3d.py deleted file mode 100644 index c7bb60e..0000000 --- a/src/gen/_surf/_viz_sequence_3d.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Tuple - -import numpy as np -import pygltflib - -from gen import PatchOutline -from gen._surf._step_sequence_outline import StepSequenceOutline -from gen._surf._patch_transition_outline import PatchTransitionOutline -from gen._surf._viz_gltf_3d import ( - ColoredTriangleData, - gltf_model_from_colored_triangle_data, - ColoredLineData, -) - -_X_COLOR = (1, 0, 0, 1) -_Z_COLOR = (0, 0, 1, 1) - - -def _coords(c: complex, t: float) -> tuple[float, float, float]: - return c.real, t, c.imag - - -def _patch_transition_to_floor( - *, - trans: PatchTransitionOutline, - t: float, - out_triangles: list[ColoredTriangleData], - out_lines: list[ColoredLineData], - order: int -): - for q in trans.data_set: - for d in range(4): - a = q + 1j**d - b = q + 1j**d * 1j - if a in trans.data_set and b in trans.data_set: - out_triangles.append( - ColoredTriangleData( - rgba=_X_COLOR if q in trans.data_x_set else _Z_COLOR, - triangle_list=np.array( - [ - _coords(q, t), - _coords(a, t), - _coords(b, t), - ][::order], - dtype=np.float32, - ), - ) - ) - for plane in trans.data_boundary_planes: - for _, a, c in plane: - out_lines.append( - ColoredLineData( - rgba=(0, 0, 0, 1), - edge_list=np.array( - [_coords(a, t), _coords(c, t)], dtype=np.float32 - ), - ) - ) - - -def _patch_boundary_to_walls( - *, - boundary: PatchOutline, - t: float, - dt: float, - out_triangles: list[ColoredTriangleData], - out_lines: list[ColoredLineData] -): - for curve in boundary.region_curves: - for basis, p1, p2 in curve: - out_triangles.append( - ColoredTriangleData.square( - rgba=_X_COLOR if basis == "X" else _Z_COLOR, - origin=_coords(p1, t), - d1=_coords(p2 - p1, 0), - d2=_coords(0, dt), - ) - ) - out_triangles.append( - ColoredTriangleData.square( - rgba=_X_COLOR if basis == "X" else _Z_COLOR, - origin=_coords(p1, t), - d1=_coords(p2 - p1, 0), - d2=_coords(0, dt), - ) - ) - for pt in boundary.iter_control_points(): - out_lines.append( - ColoredLineData( - rgba=(0, 0, 0, 1), - edge_list=np.array( - [_coords(pt, t), _coords(pt, t + dt)], dtype=np.float32 - ), - ) - ) - for curve in boundary.region_curves: - for _, a, c in curve: - for t2 in [t, t + dt]: - out_lines.append( - ColoredLineData( - rgba=(0, 0, 0, 1), - edge_list=np.array( - [_coords(a, t2), _coords(c, t2)], dtype=np.float32 - ), - ) - ) - - -def patch_sequence_to_model(sequence: StepSequenceOutline) -> pygltflib.GLTF2: - t = 0 - triangles = [] - lines = [] - for segment in sequence.steps: - _patch_transition_to_floor( - trans=segment.start, - t=t + 0.01, - out_triangles=triangles, - order=-1, - out_lines=lines, - ) - dt = max(1, segment.rounds) - _patch_boundary_to_walls( - boundary=segment.body, t=t, dt=dt, out_triangles=triangles, out_lines=lines - ) - t += dt - _patch_transition_to_floor( - trans=segment.end, t=t, out_triangles=triangles, order=+1, out_lines=lines - ) - return gltf_model_from_colored_triangle_data(triangles, colored_line_data=lines) diff --git a/src/gen/_util.py b/src/gen/_util.py index d218330..5c1765d 100644 --- a/src/gen/_util.py +++ b/src/gen/_util.py @@ -1,20 +1,6 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +import io import pathlib -import sys -from typing import Callable, Any +from typing import Callable, Any, TypeVar, Iterable import stim @@ -173,11 +159,42 @@ def process(sub_circuit: stim.Circuit): return len(used_qubits) -def write_file(path: str | pathlib.Path, content: Any): - if isinstance(content, bytes): +def write_file(path: str | pathlib.Path | io.IOBase, content: Any): + if isinstance(path, io.IOBase): + path.write(content) + return + elif isinstance(content, bytes): with open(path, "wb") as f: print(content, file=f) else: with open(path, "w") as f: print(content, file=f) - print(f"wrote file://{pathlib.Path(path).absolute()}", file=sys.stderr) + print(f"wrote file://{pathlib.Path(path).absolute()}") + + +TItem = TypeVar('TItem') + + +def xor_sorted(vals: Iterable[TItem], *, key: Callable[[TItem], Any] = lambda e: e) -> list[TItem]: + """Sorts items and then cancels pairs of equal items. + + An item will be in the result once if it appeared an odd number of times. + An item won't be in the result if it appeared an even number of times. + """ + result = sorted(vals, key=key) + n = len(result) + skipped = 0 + k = 0 + while k + 1 < n: + if result[k] == result[k + 1]: + skipped += 2 + k += 2 + else: + result[k - skipped] = result[k] + k += 1 + if k < n: + result[k - skipped] = result[k] + while skipped: + result.pop() + skipped -= 1 + return result diff --git a/src/gen/_util_test.py b/src/gen/_util_test.py index 2aa69d5..8ee4804 100644 --- a/src/gen/_util_test.py +++ b/src/gen/_util_test.py @@ -1,20 +1,21 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import stim -from gen._util import estimate_qubit_count_during_postselection +from gen._util import estimate_qubit_count_during_postselection, xor_sorted + + +def test_xor_sorted(): + assert xor_sorted([]) == [] + assert xor_sorted([2]) == [2] + assert xor_sorted([2, 3]) == [2, 3] + assert xor_sorted([3, 2]) == [2, 3] + assert xor_sorted([2, 2]) == [] + assert xor_sorted([2, 2, 2]) == [2] + assert xor_sorted([2, 2, 2, 2]) == [] + assert xor_sorted([2, 2, 3]) == [3] + assert xor_sorted([3, 2, 2]) == [3] + assert xor_sorted([2, 3, 2]) == [3] + assert xor_sorted([2, 3, 3]) == [2] + assert xor_sorted([2, 3, 5, 7, 11, 13, 5]) == [2, 3, 7, 11, 13] def test_estimate_qubit_count_during_postselection(): diff --git a/src/gen/_viz_circuit_html.py b/src/gen/_viz_circuit_html.py index 076ce6f..bd7070c 100644 --- a/src/gen/_viz_circuit_html.py +++ b/src/gen/_viz_circuit_html.py @@ -1,28 +1,16 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import base64 import collections import dataclasses import math import random import sys -from typing import Iterable +from typing import Optional, Union, Iterable import stim -from gen._core._patch import Patch +from gen._viz_patch_svg import svg_path_directions_for_tile +from gen._core import StabilizerCode, Patch +from gen._flows import ChunkInterface PITCH = 48 * 2 DIAM = 32 @@ -107,10 +95,13 @@ def _init_gate_box_labels() -> dict[str, GateStyle]: "SWAP": ("SWAP", "SWAP"), "CXSWAP": ("ZSWAP", "XSWAP"), "SWAPCX": ("XSWAP", "ZSWAP"), + "MXX": ("MXX", "MXX"), + "MYY": ("MYY", "MYY"), + "MZZ": ("MZZ", "MZZ"), } -def tag_str(tag, *, content: bool | str = False, **kwargs) -> str: +def tag_str(tag, *, content: Union[bool, str] = False, **kwargs) -> str: parts = [f"<{tag}"] for k, v in kwargs.items(): parts.append(f"{k.replace('_', '-')}={str(v)!r}") @@ -128,21 +119,23 @@ def tag_str(tag, *, content: bool | str = False, **kwargs) -> str: class _SvgLayer: - def __init__(self): + def __init__(self, patch: Patch): self.svg_instructions: list[str] = [] self.q2i_dict: dict[int, tuple[float, float]] = {} self.used_indices: set[int] = set() self.used_positions: set[tuple[float, float]] = set() self.measurement_positions: dict[int, tuple[float, float]] = {} + if patch is not None: + self.add_patch(patch) - def add(self, tag, *, content: bool | str = False, **kwargs) -> None: + def add(self, tag, *, content: Union[bool, str] = False, **kwargs) -> None: self.svg_instructions.append(" " + tag_str(tag, content=content, **kwargs)) def bounds(self) -> tuple[float, float, float, float]: - min_y = min(e for _, e in self.used_positions) - max_y = max(e for _, e in self.used_positions) - min_x = min(e for e, _ in self.used_positions) - max_x = max(e for e, _ in self.used_positions) + min_y = min([e for _, e in self.used_positions], default=0) + max_y = max([e for _, e in self.used_positions], default=0) + min_x = min([e for e, _ in self.used_positions], default=0) + max_x = max([e for e, _ in self.used_positions], default=0) min_x -= PITCH min_y -= PITCH max_x += PITCH @@ -187,10 +180,32 @@ def add_idles(self, all_used_positions: set[tuple[float, float]]): font_size=24, ) + def add_patch(self, patch: Patch): + BASE_COLORS = {"X": "#FF8080", "Z": "#8080FF", "Y": "#80FF80", None: "gray"} + + for tile in patch.tiles: + path_directions = svg_path_directions_for_tile( + tile=tile, + draw_coord=lambda pt: pt * PITCH, + ) + if path_directions is not None: + self.svg_instructions.append( + f'''''' + ) + for tile in patch.tiles: + path_directions = svg_path_directions_for_tile( + tile=tile, + draw_coord=lambda pt: pt * PITCH, + ) + if path_directions is not None: + self.svg_instructions.append( + f'''''' + ) + def svg( self, *, - html_id: str | None = None, + html_id: Optional[str] = None, as_img_with_data_uri: bool = False, width: int, height: int, @@ -225,8 +240,9 @@ def svg( class _SvgState: - def __init__(self): - self.layers: list[_SvgLayer] = [_SvgLayer()] + def __init__(self, patch: Patch | None): + self.patch: Patch | None = patch + self.layers: list[_SvgLayer] = [_SvgLayer(self.patch)] self.coord_shift: list[int] = [0, 0] self.measurement_layer_indices: list[int] = [] self.detector_index = 0 @@ -239,10 +255,10 @@ def __init__(self): self.control_count = 0 def tick(self) -> None: - self.layers.append(_SvgLayer()) + self.layers.append(_SvgLayer(self.patch)) self.layers[-1].q2i_dict = dict(self.layers[-2].q2i_dict) - def q2i(self, i: int) -> tuple[float, float]: + def i2xy(self, i: int) -> tuple[float, float]: x, y = self.layers[-1].q2i_dict.setdefault(i, (i, 0)) pt = x * PITCH, y * PITCH self.layers[-1].used_indices.add(i) @@ -283,19 +299,28 @@ def add_box( alignment_baseline="central", ) - def add_measurement(self, target: stim.GateTarget) -> None: - assert ( - target.is_qubit_target - or target.is_x_target - or target.is_y_target - or target.is_z_target - ) + def add_measurement(self, *targets: stim.GateTarget) -> None: + for target in targets: + assert ( + target.is_qubit_target + or target.is_x_target + or target.is_y_target + or target.is_z_target + ) m_index = len(self.measurement_layer_indices) self.measurement_layer_indices.append(len(self.layers) - 1) - self.layers[-1].measurement_positions[m_index] = self.q2i(target.value) + x = 0 + y = 0 + for target in targets: + dx, dy = self.i2xy(target.value) + x += dx + y += dy + x /= len(targets) + y /= len(targets) + self.layers[-1].measurement_positions[m_index] = (x, y) def mark_measurements( - self, targets: list[stim.GateTarget], prefix: str, index: int | None + self, targets: list[stim.GateTarget], prefix: str, index: Optional[int] ) -> None: if index is None: assert prefix == "D" @@ -364,6 +389,12 @@ def _draw_endpoint(x: float, y: float, style: str, *, out: _SvgState) -> None: add("circle", cx=x, cy=y, r=RAD / 2, fill="gray") add("line", x1=x - r, x2=x + r, y1=y - r, y2=y + r, stroke="black") add("line", x1=x - r, x2=x + r, y1=y + r, y2=y - r, stroke="black") + elif style == "MXX": + out.add_box(x=x, y=y, text="Mxx", fill='black', text_color='white') + elif style == "MYY": + out.add_box(x=x, y=y, text="Myy", fill='black', text_color='white') + elif style == "MZZ": + out.add_box(x=x, y=y, text="Mzz", fill='black', text_color='white') elif style == "SQRT_ZZ": out.add_box(x=x, y=y, text="√ZZ") elif style == "SQRT_YY": @@ -419,12 +450,15 @@ def _draw_endpoint(x: float, y: float, style: str, *, out: _SvgState) -> None: def _draw_2q(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: style1, style2 = TWO_QUBIT_GATE_STYLES[instruction.name] targets = instruction.targets_copy() - q2i = out.q2i + i2qq = out.i2xy + is_measurement = stim.gate_data(instruction.name).produces_measurements assert len(targets) % 2 == 0 for k in range(0, len(targets), 2): t1 = targets[k] t2 = targets[k + 1] + if is_measurement: + out.add_measurement(t1, t2) if t1.is_measurement_record_target or t2.is_measurement_record_target: if t1.is_qubit_target: t = t1.value @@ -443,7 +477,7 @@ def _draw_2q(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: if instruction.name == "CZ" else "?" ) - x, y = q2i(t) + x, y = i2qq(t) out.add( "text", x=x - RAD + 1, @@ -469,8 +503,8 @@ def _draw_2q(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: continue assert t1.is_qubit_target assert t2.is_qubit_target - x1, y1 = q2i(t1.value) - x2, y2 = q2i(t2.value) + x1, y1 = i2qq(t1.value) + x2, y2 = i2qq(t2.value) dx = x2 - x1 dy = y2 - y1 r = (dx * dx + dy * dy) ** 0.5 @@ -501,7 +535,7 @@ def _draw_mpp(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: targets = instruction.targets_copy() add = out.add add_box = out.add_box - q2i = out.q2i + q2i = out.i2xy chunks = [] start = 0 @@ -513,7 +547,7 @@ def _draw_mpp(instruction: stim.CircuitInstruction, *, out: _SvgState) -> None: start = end end = start + 1 for chunk in chunks: - out.add_measurement(chunk[0]) + out.add_measurement(*chunk) tx, ty = 0, 0 for t in chunk: x, y = q2i(t.value) @@ -555,7 +589,7 @@ def _draw_1q(instruction: stim.CircuitInstruction, *, out: _SvgState): out.add_measurement(t) for t in targets: assert t.is_qubit_target - x, y = out.q2i(t.value) + x, y = out.i2xy(t.value) style = GATE_BOX_LABELS[instruction.name] out.add_box( x, y, style.label, fill=style.fill_color, text_color=style.text_color @@ -603,6 +637,9 @@ def _stim_circuit_to_svg_helper(circuit: stim.Circuit, state: _SvgState) -> None elif instruction.name in NOISY_GATES: for t in instruction.targets_copy(): state.noted_errors.append((t.value, len(state.layers) - 1, "E")) + elif instruction.name == "MPAD": + for t in instruction.targets_copy(): + state.add_measurement(t) else: raise NotImplementedError(repr(instruction)) else: @@ -634,16 +671,20 @@ def append_patch_polygons(*, out: list[str], patch: Patch, q2i: dict[complex, in def stim_circuit_html_viewer( circuit: stim.Circuit, *, - patch: None | Patch | dict[int, Patch] = None, + patch: dict[int, Patch | StabilizerCode | ChunkInterface] | ChunkInterface | StabilizerCode | Patch | None = None, width: int = 500, height: int = 500, - known_error: Iterable[stim.ExplainedError] | None = None, + known_error: Optional[Iterable[stim.ExplainedError]] = None, ) -> str: q2i = { v[0] + 1j * v[1]: k for k, v in circuit.get_final_qubit_coordinates().items() } - state = _SvgState() + if isinstance(patch, StabilizerCode): + patch = patch.patch + if isinstance(patch, ChunkInterface): + patch = patch.to_patch() + state = _SvgState(patch) state.detector_coords = circuit.get_detector_coordinates() if known_error is None: # noinspection PyBroadException @@ -686,22 +727,22 @@ def stim_circuit_html_viewer( x, y = layer.measurement_positions[m] layer.add( "rect", - x=x - RAD, - y=y - RAD, - width=DIAM, - height=DIAM, + x=x - RAD*2, + y=y - RAD*2, + width=DIAM*2, + height=DIAM*2, fill="#FF000080", stroke="#FF0000", ) for qubit, time, basis in state.highlighted_errors: layer = state.layers[time] - x, y = state.q2i(qubit) + x, y = state.i2xy(qubit) layer.add( "text", x=x, y=y, fill="red", - content=basis, + content="," + basis, text_anchor="middle", dominant_baseline="middle", font_size=64, @@ -711,7 +752,7 @@ def stim_circuit_html_viewer( print(f"Error time is past end of circuit: {time}", file=sys.stderr) continue layer = state.layers[time] - x, y = state.q2i(qubit) + x, y = state.i2xy(qubit) layer.add( "text", x=x - RAD, @@ -768,8 +809,8 @@ def stim_circuit_html_viewer( return ( f"""
Loading...
- - + + Open in Crumble
@@ -816,11 +857,11 @@ def stim_circuit_html_viewer( handleLayerIndexChange(); }); document.addEventListener('keydown', ev => { - if (ev.code == "KeyA" && !ev.getModifierState("Control")) { + if (ev.code == "KeyQ" && !ev.getModifierState("Control")) { layer_index -= 1; ev.preventDefault(); handleLayerIndexChange(); - } else if (ev.code == "KeyD") { + } else if (ev.code == "KeyE") { layer_index += 1; ev.preventDefault(); handleLayerIndexChange(); diff --git a/src/gen/_surf/_viz_gltf_3d.py b/src/gen/_viz_gltf_3d.py similarity index 87% rename from src/gen/_surf/_viz_gltf_3d.py rename to src/gen/_viz_gltf_3d.py index a19e8f5..7409a66 100644 --- a/src/gen/_surf/_viz_gltf_3d.py +++ b/src/gen/_viz_gltf_3d.py @@ -1,17 +1,3 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import base64 import collections from typing import Iterable, Sequence @@ -125,7 +111,7 @@ def gltf_model_from_colored_triangle_data( colored_line_data = ColoredLineData.fused(colored_line_data) gltf = pygltflib.GLTF2() - gltf.asset = None + gltf.asset = {'version': '2.0'} material_INDICES = {} for data in colored_triangle_data: @@ -138,7 +124,7 @@ def gltf_model_from_colored_triangle_data( material.alphaMode = None material.alphaCutoff = None material.doubleSided = True - material_INDICES[data.rgba] = len(gltf.materials) + material_INDICES[(data.rgba, 'tri')] = len(gltf.materials) gltf.materials.append(material) for data in colored_line_data: material = pygltflib.Material() @@ -149,7 +135,7 @@ def gltf_model_from_colored_triangle_data( material.emissiveFactor = None material.alphaMode = None material.alphaCutoff = None - material_INDICES[data.rgba] = len(gltf.materials) + material_INDICES[(data.rgba, 'edge')] = len(gltf.materials) gltf.materials.append(material) shared_buffer = pygltflib.Buffer() @@ -182,7 +168,7 @@ def gltf_model_from_colored_triangle_data( bufferView.byteLength = byte_length byte_offset += byte_length bufferView.target = pygltflib.ARRAY_BUFFER - buffer_view_INDICES[data.rgba] = len(gltf.bufferViews) + buffer_view_INDICES[(data.rgba, 'tri')] = len(gltf.bufferViews) gltf.bufferViews.append(bufferView) for data in colored_line_data: bufferView = pygltflib.BufferView() @@ -192,44 +178,44 @@ def gltf_model_from_colored_triangle_data( bufferView.byteLength = byte_length byte_offset += byte_length bufferView.target = pygltflib.ARRAY_BUFFER - buffer_view_INDICES[data.rgba] = len(gltf.bufferViews) + buffer_view_INDICES[(data.rgba, 'edge')] = len(gltf.bufferViews) gltf.bufferViews.append(bufferView) accessor_INDICES = {} for data in colored_triangle_data: accessor = pygltflib.Accessor() - accessor.bufferView = buffer_view_INDICES[data.rgba] + accessor.bufferView = buffer_view_INDICES[(data.rgba, 'tri')] accessor.byteOffset = 0 accessor.componentType = pygltflib.FLOAT accessor.count = data.triangle_list.shape[0] accessor.type = pygltflib.VEC3 accessor.max = [float(e) for e in np.max(data.triangle_list, axis=0)] accessor.min = [float(e) for e in np.min(data.triangle_list, axis=0)] - accessor_INDICES[data.rgba] = len(gltf.accessors) + accessor_INDICES[(data.rgba, 'tri')] = len(gltf.accessors) gltf.accessors.append(accessor) for data in colored_line_data: accessor = pygltflib.Accessor() - accessor.bufferView = buffer_view_INDICES[data.rgba] + accessor.bufferView = buffer_view_INDICES[(data.rgba, 'edge')] accessor.byteOffset = 0 accessor.componentType = pygltflib.FLOAT accessor.count = data.edge_list.shape[0] accessor.type = pygltflib.VEC3 accessor.max = [float(e) for e in np.max(data.edge_list, axis=0)] accessor.min = [float(e) for e in np.min(data.edge_list, axis=0)] - accessor_INDICES[data.rgba] = len(gltf.accessors) + accessor_INDICES[(data.rgba, 'edge')] = len(gltf.accessors) gltf.accessors.append(accessor) mesh0 = pygltflib.Mesh() for data in colored_triangle_data: primitive = pygltflib.Primitive() - primitive.material = material_INDICES[data.rgba] - primitive.attributes.POSITION = accessor_INDICES[data.rgba] + primitive.material = material_INDICES[(data.rgba, 'tri')] + primitive.attributes.POSITION = accessor_INDICES[(data.rgba, 'tri')] primitive.mode = pygltflib.TRIANGLES mesh0.primitives.append(primitive) for data in colored_line_data: primitive = pygltflib.Primitive() - primitive.material = material_INDICES[data.rgba] - primitive.attributes.POSITION = accessor_INDICES[data.rgba] + primitive.material = material_INDICES[(data.rgba, 'edge')] + primitive.attributes.POSITION = accessor_INDICES[(data.rgba, 'edge')] primitive.mode = pygltflib.LINES mesh0.primitives.append(primitive) mesh0_INDEX = len(gltf.meshes) @@ -266,7 +252,7 @@ def viz_3d_gltf_model_html(model: pygltflib.GLTF2) -> str: + model_data_uri + r"""">Download 3D Model as .GLTF File
Mouse Wheel = Zoom. Left Drag = Orbit. Right Drag = Strafe. -
+
JavaScript Blocked?
@@ -274,8 +260,11 @@ def viz_3d_gltf_model_html(model: pygltflib.GLTF2) -> str: /// BEGIN TERRIBLE HACK. /// Get the object by ID then change the ID. /// This is a workaround for https://github.com/jupyter/notebook/issues/6598 + let outerContainer = document.getElementById("stim-outer-container"); let container = document.getElementById("stim-3d-viewer-scene-container"); container.id = "stim-3d-viewer-scene-container-USED"; + outerContainer.id = "stim-outer-container-USED"; + outerContainer.style.height = `${window.innerHeight-100}px`; let downloadLink = document.getElementById("stim-3d-viewer-download-link"); downloadLink.id = "stim-3d-viewer-download-link-USED"; /// END TERRIBLE HACK. @@ -287,7 +276,7 @@ def viz_3d_gltf_model_html(model: pygltflib.GLTF2) -> str: /// /// What this SHOULD be is: /// - /// import {Box3, Scene, Color, PerspectiveCamera, WebGLRenderer, DirectionalLight} from "three"; + /// import {Box3, Scene, Color, OrthographicCamera, PerspectiveCamera, WebGLRenderer, DirectionalLight} from "three"; /// import {OrbitControls} from "three-orbitcontrols"; /// import {GLTFLoader} from "three-gltf-loader"; /// @@ -337,14 +326,20 @@ def viz_3d_gltf_model_html(model: pygltflib.GLTF2) -> str: scene.add(gltf.scene); // Point the camera at the center, far enough back to see everything. - let camera = new PerspectiveCamera(35, container.clientWidth / container.clientHeight, 0.1, 100000); - let controls = new OrbitControls(camera, container); let bounds = new Box3().setFromObject(scene); + let w = container.clientWidth; + let h = container.clientHeight; + let camera = new OrthographicCamera(-w/2, w/2, h/2, -h/2, 0.1, 100000); + let controls = new OrbitControls(camera, container); let mid = new Vector3( (bounds.min.x + bounds.max.x) * 0.5, (bounds.min.y + bounds.max.y) * 0.5, (bounds.min.z + bounds.max.z) * 0.5, ); + let max_dx = bounds.max.x - bounds.min.x; + let max_dy = bounds.max.y - bounds.min.y; + let max_dz = bounds.max.z - bounds.min.z; + let max_d = Math.sqrt(max_dx*max_dx + max_dy*max_dy + max_dz*max_dz); let boxPoints = []; for (let dx of [0, 0.5, 1]) { for (let dy of [0, 0.5, 1]) { @@ -360,14 +355,16 @@ def viz_3d_gltf_model_html(model: pygltflib.GLTF2) -> str: let isInView = p => { p = new Vector3(p.x, p.y, p.z); p.project(camera); - return Math.abs(p.x) < 1 && Math.abs(p.y) < 1 && p.z >= 0 && p.z < 1; + return Math.abs(p.x) < 1 && Math.abs(p.y) < 1; }; let unit = new Vector3(0.3, 0.4, -1.8); unit.normalize(); let setCameraDistance = d => { controls.target.copy(mid); camera.position.copy(mid); - camera.position.addScaledVector(unit, d); + camera.position.addScaledVector(unit, max_d); + camera.zoom = 1/d; + camera.updateProjectionMatrix(); controls.update(); return boxPoints.every(isInView); }; @@ -407,8 +404,14 @@ def viz_3d_gltf_model_html(model: pygltflib.GLTF2) -> str: // Render whenever any important changes have occurred. requestAnimationFrame(() => renderer.render(scene, camera)); new ResizeObserver(() => { - camera.aspect = container.clientWidth / container.clientHeight; + let w = container.clientWidth; + let h = container.clientHeight; + camera.left = -w/2; + camera.right = w/2; + camera.top = h/2; + camera.bottom = -h/2; camera.updateProjectionMatrix(); + controls.update(); renderer.setSize(container.clientWidth, container.clientHeight); renderer.render(scene, camera); }).observe(container); diff --git a/src/gen/_surf/_viz_gltf_3d_test.py b/src/gen/_viz_gltf_3d_test.py similarity index 86% rename from src/gen/_surf/_viz_gltf_3d_test.py rename to src/gen/_viz_gltf_3d_test.py index 1bd7092..944cbf0 100644 --- a/src/gen/_surf/_viz_gltf_3d_test.py +++ b/src/gen/_viz_gltf_3d_test.py @@ -1,22 +1,8 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import json import numpy as np -from gen._surf._viz_gltf_3d import ( +from gen._viz_gltf_3d import ( ColoredTriangleData, gltf_model_from_colored_triangle_data, viz_3d_gltf_model_html, @@ -77,6 +63,7 @@ def test_gltf_model_from_colored_triangle_data(): {"buffer": 0, "byteOffset": 0, "byteLength": 36, "target": 34962}, {"buffer": 0, "byteOffset": 36, "byteLength": 36, "target": 34962}, ], + 'asset': {'version': '2.0'}, "buffers": [ { "uri": "data:application/octet-stream;base64,AACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/", diff --git a/src/gen/_viz_patch_svg.py b/src/gen/_viz_patch_svg.py index 7cb23bd..7c58e12 100644 --- a/src/gen/_viz_patch_svg.py +++ b/src/gen/_viz_patch_svg.py @@ -1,28 +1,15 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +import collections import math -from typing import Iterable, Union, Literal, TYPE_CHECKING, Sequence, Callable +from typing import Iterable, Union, TYPE_CHECKING, Sequence, Callable, \ + Any -from gen._core._patch import Patch, Tile from gen._core._util import min_max_complex if TYPE_CHECKING: import gen -def is_colinear(a: complex, b: complex, c: complex) -> bool: +def is_collinear(a: complex, b: complex, c: complex) -> bool: d1 = b - a d2 = c - a return abs(d1.real * d2.imag - d2.real * d1.imag) < 1e-4 @@ -110,7 +97,7 @@ def _path_commands_for_points_with_many_points( prev_q = pts[k - 1] q = pts[k] next_q = pts[(k + 1) % len(pts)] - if (is_colinear(prev_q, q, next_q) or is_colinear(prev_prev_q, prev_q, q)): + if is_collinear(prev_q, q, next_q) or is_collinear(prev_prev_q, prev_q, q): prev_pt = draw_coord(prev_q) cur_pt = draw_coord(q) d = cur_pt - prev_pt @@ -126,7 +113,7 @@ def _path_commands_for_points_with_many_points( def svg_path_directions_for_tile( - *, tile: "gen.Tile", draw_coord: Callable[[complex], complex] + *, tile: "gen.Tile", draw_coord: Callable[[complex], complex], contract_towards: complex | None = None, ) -> str | None: hint_point = tile.measurement_qubit if any(abs(q - hint_point) < 1e-4 for q in tile.data_set): @@ -158,6 +145,10 @@ def svg_path_directions_for_tile( ) ) + if contract_towards is not None: + c = 0.85 + points = [p*c + (1-c)*contract_towards for p in points] + return " ".join( _path_commands_for_points_with_many_points( pts=points, @@ -166,145 +157,154 @@ def svg_path_directions_for_tile( ) -BASE_COLORS = {"X": "#FF8080", "Z": "#8080FF", "Y": "#80FF80", None: "gray"} - - -def _data_span_sort(tile: "gen.Tile") -> float: - min_c, max_c = min_max_complex(tile.data_set, default=0) - return max_c.real - min_c.real + max_c.imag - min_c.imag - - -def _patch_svg_viewer_helper_single_patch_wraparound_clip( - *, - patch: Patch, - transform_pt: Callable[[complex], complex], - out_lines: list[str], - show_order: str, - opacity: float, - show_data_qubits: bool, - show_measure_qubits: bool, - available_qubits: frozenset[complex], - extra_used_coords: frozenset[complex], - clip_path_id_ptr: list[int], -): - if len(patch.without_wraparound_tiles().tiles) == len(patch.tiles): - _patch_svg_viewer_helper_single_patch( - patch=patch, - transform_pt=transform_pt, - out_lines=out_lines, - show_order=show_order, - opacity=opacity, - show_data_qubits=show_data_qubits, - show_measure_qubits=show_measure_qubits, - clip_path_id_ptr=clip_path_id_ptr, - available_qubits=available_qubits, - extra_used_coords=extra_used_coords, - ) - return - - p_min, p_max = min_max_complex(patch.data_set, default=0) - w = p_max.real - p_min.real - h = p_max.imag - p_min.imag - left = p_min.real + w * 0.1 - right = p_min.real + w * 0.9 - top = p_min.imag + h * 0.1 - bot = p_min.imag + h * 0.9 - pad_w = 1 - pad_h = 1 - pad_shift = -pad_w - 1j*pad_h - pad_shift /= 2 - w += pad_w - h += pad_h - def new_transform(q: complex) -> complex: - q -= p_min - q /= (p_max - p_min).real - q *= w - q += p_min - return transform_pt(q) - - def is_normal_tile(tile: Tile) -> bool: - t_min, t_max = min_max_complex(tile.data_set, default=0) - if t_min.real < left and t_max.real > right: - return False - if t_min.imag < top and t_max.imag > bot: - return False - return True - - def unwraparound(tile: Tile): - t_min, t_max = min_max_complex(tile.data_set, default=0) - dw = w - dh = h - if not (t_min.real < left and t_max.real > right): - dw = 0 - if not (t_min.imag < top and t_max.imag > bot): - dh = 0 - def trans(q: complex) -> complex: - if q.real < w / 2: - q += dw - if q.imag < h / 2: - q += dh*1j - return q - return trans - - new_tiles = [] - for tile in patch.tiles: - if is_normal_tile(tile): - new_tiles.append(tile) - continue - new_tile_1 = tile.with_transformed_coords(unwraparound(tile)) - new_tile_2 = new_tile_1.with_transformed_coords(lambda q: q - w) - new_tile_3 = new_tile_1.with_transformed_coords(lambda q: q - h*1j) - new_tile_4 = new_tile_1.with_transformed_coords(lambda q: q - w - h*1j) - new_tiles.append(new_tile_1) - new_tiles.append(new_tile_2) - new_tiles.append(new_tile_3) - new_tiles.append(new_tile_4) - - clip_ip = clip_path_id_ptr[0] - clip_path_id_ptr[0] += 1 - out_lines.append(f'''''') - tl = transform_pt(p_min) - br = transform_pt(p_max) - out_lines.append(f''' ''') - out_lines.append(f'''''') - _patch_svg_viewer_helper_single_patch( - patch=Patch(new_tiles).with_transformed_coords(lambda q: q + pad_shift), - transform_pt=new_transform, - out_lines=out_lines, - show_order=show_order, - opacity=opacity, - show_data_qubits=show_data_qubits, - show_measure_qubits=show_measure_qubits, - clip_path_id_ptr=clip_path_id_ptr, - available_qubits=frozenset([q + pad_shift for q in available_qubits]), - extra_used_coords=frozenset([q + pad_shift for q in extra_used_coords]), - extra_clip_path_arg=f'clip-path="url(#clipPath{clip_ip})" ', - ) - - -def _patch_svg_viewer_helper_single_patch( - *, - patch: Patch, - transform_pt: Callable[[complex], complex], - out_lines: list[str], - show_order: str, - opacity: float, - show_data_qubits: bool, - show_measure_qubits: bool, - clip_path_id_ptr: list[int], - available_qubits: frozenset[complex], - extra_used_coords: frozenset[complex], - extra_clip_path_arg: str = '', +def _draw_patch( + *, + patch: Union['gen.Patch', 'gen.StabilizerCode', 'gen.ChunkInterface'], + q2p: Callable[[complex], complex], + show_coords: bool, + show_obs: bool, + opacity: float, + show_data_qubits: bool, + show_measure_qubits: bool, + expected_points: frozenset[complex], + clip_path_id_ptr: list[int], + out_lines: list[str], + show_order: bool, + find_logical_err_max_weight: int | None, + tile_color_func: Callable[['gen.Tile'], str] | None = None ): layer_1q2 = [] layer_1q = [] fill_layer2q = [] - stroke_layer2q = [] fill_layer_mq = [] stroke_layer_mq = [] - stroke_width = abs(transform_pt(0.02) - transform_pt(0)) + scale_factor = abs(q2p(1) - q2p(0)) + + from gen._core._stabilizer_code import StabilizerCode + from gen._flows._chunk_interface import ChunkInterface + if isinstance(patch, ChunkInterface): + patch = patch.to_code() + + labels: list[tuple[complex, str, dict[str, Any]]] = [] + if isinstance(patch, StabilizerCode): + if find_logical_err_max_weight is not None: + err = patch.find_logical_error(max_search_weight=find_logical_err_max_weight) + for e in err: + for loc in e.circuit_error_locations: + for loc2 in loc.flipped_pauli_product: + r, i = loc2.coords + q = r + 1j*i + p = loc2.gate_target.pauli_type + labels.append((q, p + '!', { + 'text-anchor': 'middle', + 'dominant-baseline': 'central', + 'font-size': scale_factor * 1.1, + 'fill': BASE_COLORS_DARK[p], + })) + + if show_obs: + k = 0 + while True: + if k < 10: + suffix = "₀₁₂₃₄₅₆₇₈₉"[k] + else: + suffix = str(k) + work = False + if k < len(patch.observables_x): + for q, basis2 in patch.observables_x[k].qubits.items(): + if not patch.observables_z: + label = basis2 + suffix + elif basis2 != 'X': + label = 'X' + suffix + '[' + basis2 + ']' + else: + label = 'X' + suffix + labels.append((q, label, { + 'text-anchor': 'end', + 'dominant-baseline': 'hanging', + 'font-size': scale_factor * 0.6, + 'fill': BASE_COLORS_DARK[basis2], + })) + work = True + if k < len(patch.observables_z): + for q, basis2 in patch.observables_z[k].qubits.items(): + if basis2 != 'Z': + basis_suffix = '[' + basis2 + ']' + else: + basis_suffix = '' + labels.append((q, 'Z' + suffix + basis_suffix, { + 'text-anchor': "start", + 'dominant-baseline': 'bottom', + 'font-size': scale_factor * 0.6, + 'fill': BASE_COLORS_DARK[basis2], + })) + work = True + if not work: + break + k += 1 + for q, s, ts in labels: + loc2 = q2p(q) + terms = { + 'x': loc2.real, + 'y': loc2.imag, + **ts, + } + layer_1q2.append( + "{s}" + ) + + all_points = patch.used_set | expected_points + if show_coords and all_points: + all_x = sorted({q.real for q in all_points}) + all_y = sorted({q.imag for q in all_points}) + left = min(all_x) - 1 + top = min(all_y) - 1 + + for x in all_x: + if x == int(x): + x = int(x) + loc2 = q2p(x + top*1j) + stroke_layer_mq.append( + "{x}" + ) + for y in all_y: + if y == int(y): + y = int(y) + loc2 = q2p(y*1j + left) + stroke_layer_mq.append( + "{y}i" + ) + + sorted_tiles = sorted(patch.tiles, key=tile_data_span, reverse=True) + d2tiles = collections.defaultdict(list) + + def contraction_point(tile) -> complex | None: + if len(tile.data_set) <= 2: + return None + for d in tile.data_set: + for other_tile in d2tiles[d]: + if other_tile is not tile: + if tile.data_set < other_tile.data_set or (tile.data_set == other_tile.data_set and tile.basis < other_tile.basis): + return sum(other_tile.data_set) / len(other_tile.data_set) + return None + + for tile in sorted_tiles: + for d in tile.data_set: + d2tiles[d].append(tile) - sorted_tiles = sorted(patch.tiles, key=_data_span_sort, reverse=True) for tile in sorted_tiles: c = tile.measurement_qubit if any(abs(q - c) < 1e-4 for q in tile.data_set): @@ -315,359 +315,276 @@ def _patch_svg_viewer_helper_single_patch( ) if not dq: continue - common_basis = tile.basis - fill_color = BASE_COLORS[common_basis] - + fill_color = BASE_COLORS[tile.basis] + if tile_color_func is not None: + fill_color = tile_color_func(tile) + + if len(tile.data_set) == 1: + fl = layer_1q + sl = stroke_layer_mq + elif len(tile.data_set) == 2: + fl = fill_layer2q + sl = stroke_layer_mq + else: + fl = fill_layer_mq + sl = stroke_layer_mq + cp = contraction_point(tile) path_directions = svg_path_directions_for_tile( tile=tile, - draw_coord=transform_pt, + draw_coord=q2p, + contract_towards=cp, ) - path_cmd_start = None if path_directions is not None: - if len(tile.data_set) == 1: - fl = layer_1q - sl = layer_1q2 - elif len(tile.data_set) == 2: - fl = fill_layer2q - sl = stroke_layer2q - else: - fl = fill_layer_mq - sl = stroke_layer_mq fl.append( f'''""" - + f''' />''' ) - - # # Draw lines from data qubits to measurement qubit. - # for d in tile.data_set: - # pd = transform_pt(d) - # pc = transform_pt(c) - # sl.append( - # f'''""" - # ) - - if show_order != "undirected": + if cp is None: sl.append( f'''""" ) - else: - cur_pt = transform_pt(dq[-1]) - path_cmd_start = f'' - ) - if show_order != "undirected": - stroke_layer_mq.append( - f"{path_cmd_start} " - f'stroke-width="{stroke_width}" ' - f'stroke="black" ' - f""" {extra_clip_path_arg} """ - f'fill="none" />' + + # Add basis glows around data qubits in multi-basis stabilizers. + if path_directions is not None and tile.basis is None and tile_color_func is None: + clip_path_id_ptr[0] += 1 + fl.append(f'') + fl.append(f''' ''') + fl.append(f"") + for k, q in enumerate(tile.ordered_data_qubits): + if q is None: + continue + v = q2p(q) + fl.append( + f"' ) if show_measure_qubits: m = tile.measurement_qubit - if show_order == "undirected": - m = m * 0.8 + c * 0.2 - p = transform_pt(m) + loc2 = q2p(m) layer_1q2.append( f"""" + f'stroke-width="{scale_factor * 0.02}" ' + f"""stroke="black" />""" ) + if show_data_qubits: for d in tile.data_set: - p = transform_pt(d) + loc2 = q2p(d) layer_1q2.append( f"""" + f"""stroke="none" />""" ) - if common_basis is None and path_cmd_start is not None: - clip_path_id_ptr[0] += 1 - fill_layer_mq.append(f'') - fill_layer_mq.append(f" {path_cmd_start} />") - fill_layer_mq.append(f"") - for k, q in enumerate(tile.ordered_data_qubits): - if q is None: - continue - v = transform_pt(q) - fill_layer_mq.append( + for q in all_points: + if q not in patch.data_set and q not in patch.measure_set: + loc2 = q2p(q) + layer_1q2.append( f"' + f'cx="{loc2.real}" ' + f'cy="{loc2.imag}" ' + f'r="{scale_factor * 0.1}" ' + f'fill="black" ' + f"""stroke="none" />""" ) - out_lines.extend(fill_layer_mq) - out_lines.extend(stroke_layer_mq) - out_lines.extend(fill_layer2q) - out_lines.extend(stroke_layer2q) - out_lines.extend(layer_1q) - out_lines.extend(layer_1q2) - - if available_qubits | extra_used_coords: - for q in available_qubits | (patch.used_set | extra_used_coords): - fill_color = "black" if q in available_qubits else "orange" - if ( - not show_measure_qubits - and not q in available_qubits - and q in patch.measure_set - ): - continue - q2 = transform_pt(q) - out_lines.append( - f"" + + out_lines += fill_layer_mq + out_lines += stroke_layer_mq + out_lines += fill_layer2q + out_lines += layer_1q + out_lines += layer_1q2 + + # Draw each element's measurement order as a zig zag arrow. + if show_order: + for tile in patch.tiles: + _draw_tile_order_arrow( + q2p=q2p, + tile=tile, + out_lines=out_lines, ) +BASE_COLORS = {"X": "#FF8080", "Z": "#8080FF", "Y": "#80FF80", None: "gray"} +BASE_COLORS_DARK = {"X": "#B01010", "Z": "#1010B0", "Y": "#10B010", None: "black"} + + +def tile_data_span(tile: "gen.Tile") -> Any: + min_c, max_c = min_max_complex(tile.data_set, default=0) + return max_c.real - min_c.real + max_c.imag - min_c.imag, tile.bases + + +def _draw_tile_order_arrow( + *, + tile: 'gen.Tile', + q2p: Callable[[complex], complex], + out_lines: list[str], +): + scale_factor = abs(q2p(1) - q2p(0)) + + c = tile.measurement_qubit + if len(tile.data_set) == 3 or c in tile.data_set: + c = 0 + for q in tile.data_set: + c += q + c /= len(tile.data_set) + pts: list[complex] = [] + + path_cmd_start = f'' + ) + delay = 0 + prev = v + else: + delay += 1 + path_cmd_start = path_cmd_start.strip() + path_cmd_start += ( + f'" fill="none" ' + f'stroke-width="{scale_factor * 0.02}" ' + f'stroke="{arrow_color}" />' + ) + out_lines.append(path_cmd_start) + + # Draw arrow at end of arrow. + if len(pts) > 1: + p = pts[-1] + d2 = p - pts[-2] + if d2: + d2 /= abs(d2) + d2 *= 4 * scale_factor * 0.02 + a = p + d2 + b = p + d2 * 1j + c = p + d2 * -1j + out_lines.append( + f"' + ) + + def patch_svg_viewer( - patches: Iterable[Patch], + patches: Iterable[Union['gen.Patch', 'gen.StabilizerCode', 'gen.ChunkInterface']], *, canvas_height: int = 500, - show_order: Union[bool, Literal["undirected", "3couplerspecial"]] = True, + show_order: bool = True, + show_obs: bool = True, opacity: float = 1, show_measure_qubits: bool = True, show_data_qubits: bool = False, - available_qubits: Iterable[complex] = (), + expected_points: Iterable[complex] = (), extra_used_coords: Iterable[complex] = (), - wraparound_clip: bool = False, + show_coords: bool = True, + find_logical_err_max_weight: int | None = None, + rows: int | None = None, + cols: int | None = None, + tile_color_func: Callable[['gen.Tile'], str] | None = None ) -> str: """Returns a picture of the stabilizers measured by various plan.""" + expected_points = frozenset(expected_points) - available_qubits = frozenset(available_qubits) extra_used_coords = frozenset(extra_used_coords) patches = tuple(patches) - min_c, max_c = min_max_complex( - [ - q - for plan in patches - for q in plan.used_set | available_qubits | extra_used_coords - ], - default=0, - ) + all_points = { + q + for patch in patches + for q in patch.used_set | expected_points | extra_used_coords + } + min_c, max_c = min_max_complex(all_points, default=0) min_c -= 1 + 1j max_c += 1 + 1j + if show_coords: + min_c -= 1 + 1j box_width = max_c.real - min_c.real box_height = max_c.imag - min_c.imag pad = max(box_width, box_height) * 0.1 + 1 - box_width += pad - box_height += pad - columns = math.ceil(math.sqrt(len(patches) + 2)) - rows = math.ceil(len(patches) / max(1, columns)) - total_height = max(1.0, box_height * rows - pad) - total_width = max(1.0, box_width * columns + 1) - scale_factor = canvas_height / max(total_height + 1, 1) + box_x_pitch = box_width + pad + box_y_pitch = box_height + pad + if cols is None and rows is None: + cols = math.ceil(math.sqrt(len(patches))) + rows = math.ceil(len(patches) / max(1, cols)) + elif cols is None: + cols = math.ceil(len(patches) / max(1, rows)) + elif rows is None: + rows = math.ceil(len(patches) / max(1, cols)) + else: + assert cols * rows >= len(patches) + total_height = max(1.0, box_y_pitch * rows - pad) + total_width = max(1.0, box_x_pitch * cols - pad) + scale_factor = canvas_height / max(total_height, 1) canvas_width = int(math.ceil(canvas_height * (total_width / total_height))) - def transform_pt(plan_i2: int, pt2: complex) -> complex: - pt2 += box_width * (plan_i2 % columns) - pt2 += box_height * (plan_i2 // columns) * 1j - pt2 += pad * (0.5 + 0.5j) - pt2 += pad - pt2 -= min_c + 1 + 1j - pt2 *= scale_factor - return pt2 - - def transform_dif(dif: complex) -> complex: - return dif * scale_factor - - def pt(plan_i2: int, q2: complex) -> str: - return f"{transform_pt(plan_i2, q2).real},{transform_pt(plan_i2, q2).imag}" + def patch_q2p(patch_index: int, q: complex) -> complex: + q -= min_c + q += box_x_pitch * (patch_index % cols) + q += box_y_pitch * (patch_index // cols) * 1j + q *= scale_factor + return q lines = [ f"""""" ] - # Draw each plan element as a polygon. clip_path_id_ptr = [0] - - lines.append( - f'' - ) - lines.append( - "X" - ) - - lines.append( - f'' - ) - lines.append( - "Z" - ) - - helper = _patch_svg_viewer_helper_single_patch_wraparound_clip if wraparound_clip else _patch_svg_viewer_helper_single_patch - for plan_i, patch in enumerate(patches): - helper( - patch=patch, - transform_pt=lambda q: transform_pt(plan_i, q), - out_lines=lines, - show_order=show_order, + for plan_i, plan in enumerate(patches): + _draw_patch( + patch=plan, + q2p=lambda q: patch_q2p(plan_i, q), + show_coords=show_coords, opacity=opacity, show_data_qubits=show_data_qubits, show_measure_qubits=show_measure_qubits, + expected_points=expected_points, clip_path_id_ptr=clip_path_id_ptr, - available_qubits=available_qubits, - extra_used_coords=extra_used_coords, + out_lines=lines, + show_order=show_order, + show_obs=show_obs, + find_logical_err_max_weight=find_logical_err_max_weight, + tile_color_func=tile_color_func, ) - stroke_width = abs(transform_pt(0, 0.02) - transform_pt(0, 0)) - - # Draw each element's measurement order as a zig zag arrow. - assert show_order in [False, True, "3couplerspecial", "undirected"] - if show_order: - for plan_i, plan in enumerate(patches): - for tile in plan.tiles: - c = tile.measurement_qubit - if len(tile.data_set) == 3 or c in tile.data_set: - c = 0 - for q in tile.data_set: - c += q - c /= len(tile.data_set) - pts: list[complex] = [] - - path_cmd_start = f'' - ) - delay = 0 - prev = v - else: - delay += 1 - path_cmd_start = path_cmd_start.strip() - path_cmd_start += ( - f'" fill="none" ' - f'stroke-width="{stroke_width}" ' - f'stroke="{arrow_color}" />' - ) - lines.append(path_cmd_start) - - # Draw arrow at end of arrow. - if show_order is True and len(pts) > 1: - p = pts[-1] - d2 = p - pts[-2] - if d2: - d2 /= abs(d2) - d2 *= 4 * stroke_width - a = p + d2 - b = p + d2 * 1j - c = p + d2 * -1j - lines.append( - f"' - ) - if show_order == "3couplerspecial" and len(pts) > 2: - # Show location of measurement qubit. - p = transform_pt( - plan_i, tile.ordered_data_qubits[-2] * 0.6 + c * 0.4 - ) - lines.append( - f"' - ) - - # Frames + # Draw frame outlines for outline_index, outline in enumerate(patches): - if wraparound_clip and len(outline.without_wraparound_tiles().tiles) < len(outline.tiles): - p_min, p_max = min_max_complex(outline.data_set, default=0) - a = transform_pt(outline_index, p_min) - b = transform_pt(outline_index, p_max) - else: - a = transform_pt(outline_index, min_c + 0.1j + 0.1) - b = transform_pt(outline_index, max_c - 0.1j - 0.1) + a = patch_q2p(outline_index, min_c) + b = patch_q2p(outline_index, max_c) lines.append( f'' ) diff --git a/src/gen/_viz_patch_svg_test.py b/src/gen/_viz_patch_svg_test.py index 5a47445..938ad2c 100644 --- a/src/gen/_viz_patch_svg_test.py +++ b/src/gen/_viz_patch_svg_test.py @@ -1,21 +1,7 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import gen -def test_patch_svg_exact(): +def test_patch_svg_runs(): patch = gen.Patch( tiles=[ gen.Tile( @@ -61,61 +47,4 @@ def test_patch_svg_exact(): ] ) svg_content = gen.patch_svg_viewer([patch]) - assert ( - svg_content.strip() - == """ - - -X - -Z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - """.strip() - ) + assert svg_content is not None diff --git a/test_data/old_fail_to_configure_cases/detector_reorder_superdense_round.stim b/test_data/old_fail_to_configure_cases/detector_reorder_superdense_round.stim new file mode 100644 index 0000000..463c384 --- /dev/null +++ b/test_data/old_fail_to_configure_cases/detector_reorder_superdense_round.stim @@ -0,0 +1,122 @@ +QUBIT_COORDS(0, 0) 0 +QUBIT_COORDS(1, 0) 1 +QUBIT_COORDS(1, 1) 2 +QUBIT_COORDS(1, 2) 3 +QUBIT_COORDS(2, 0) 4 +QUBIT_COORDS(2, 1) 5 +QUBIT_COORDS(2, 2) 6 +QUBIT_COORDS(2, 3) 7 +QUBIT_COORDS(3, 0) 8 +QUBIT_COORDS(3, 1) 9 +QUBIT_COORDS(3, 2) 10 +QUBIT_COORDS(3, 3) 11 +QUBIT_COORDS(3, 4) 12 +QUBIT_COORDS(3, 5) 13 +QUBIT_COORDS(4, 0) 14 +QUBIT_COORDS(4, 1) 15 +QUBIT_COORDS(4, 2) 16 +QUBIT_COORDS(4, 3) 17 +QUBIT_COORDS(4, 4) 18 +QUBIT_COORDS(4, 5) 19 +QUBIT_COORDS(4, 6) 20 +QUBIT_COORDS(5, 0) 21 +QUBIT_COORDS(5, 1) 22 +QUBIT_COORDS(5, 2) 23 +QUBIT_COORDS(5, 3) 24 +QUBIT_COORDS(5, 4) 25 +QUBIT_COORDS(5, 5) 26 +QUBIT_COORDS(6, 0) 27 +QUBIT_COORDS(6, 1) 28 +QUBIT_COORDS(6, 2) 29 +QUBIT_COORDS(6, 3) 30 +QUBIT_COORDS(6, 4) 31 +QUBIT_COORDS(7, 0) 32 +QUBIT_COORDS(7, 1) 33 +QUBIT_COORDS(7, 2) 34 +QUBIT_COORDS(8, 0) 35 +QUBIT_COORDS(8, 1) 36 +MPP X0*X8*X14*X32*X35 X0*X2*X5*X8 Z0*Z2*Z5*Z8 X2*X5*X7*X10 Z2*Z5*Z7*Z10 X5*X8*X10*X14*X16*X22 Z5*Z8*Z10*Z14*Z16*Z22 X7*X10*X12*X16*X18*X24 Z7*Z10*Z12*Z16*Z18*Z24 X12*X18*X20*X26 Z12*Z18*Z20*Z26 X14*X22*X28*X32 Z14*Z22*Z28*Z32 X16*X22*X24*X28*X30*X34 Z16*Z22*Z24*Z28*Z30*Z34 X18*X24*X26*X30 Z18*Z24*Z26*Z30 X28*X32*X34*X35 Z28*Z32*Z34*Z35 +OBSERVABLE_INCLUDE(0) rec[-19] +TICK +RX 1 3 9 11 13 21 23 25 33 +R 4 6 15 17 19 27 29 31 36 +X_ERROR(0.001) 4 6 15 17 19 27 29 31 36 +Z_ERROR(0.001) 1 3 9 11 13 21 23 25 33 +DEPOLARIZE1(0.001) 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +CX 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE2(0.001) 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE1(0.001) 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +CX 1 0 4 8 6 10 9 5 11 7 15 22 17 24 19 26 21 14 23 16 25 18 27 32 29 34 33 28 +DEPOLARIZE2(0.001) 1 0 4 8 6 10 9 5 11 7 15 22 17 24 19 26 21 14 23 16 25 18 27 32 29 34 33 28 +DEPOLARIZE1(0.001) 2 3 12 13 20 30 31 35 36 +TICK +CX 3 2 6 5 9 8 11 10 13 12 15 14 17 16 19 18 23 22 25 24 29 28 31 30 33 32 36 35 +DEPOLARIZE2(0.001) 3 2 6 5 9 8 11 10 13 12 15 14 17 16 19 18 23 22 25 24 29 28 31 30 33 32 36 35 +DEPOLARIZE1(0.001) 0 1 4 7 20 21 26 27 34 +TICK +CX 1 2 4 5 6 7 9 10 11 12 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 33 34 +DEPOLARIZE2(0.001) 1 2 4 5 6 7 9 10 11 12 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 33 34 +DEPOLARIZE1(0.001) 0 3 8 13 14 31 32 35 36 +TICK +CX 2 1 5 4 7 6 10 9 12 11 16 15 18 17 20 19 22 21 24 23 26 25 28 27 30 29 34 33 +DEPOLARIZE2(0.001) 2 1 5 4 7 6 10 9 12 11 16 15 18 17 20 19 22 21 24 23 26 25 28 27 30 29 34 33 +DEPOLARIZE1(0.001) 0 3 8 13 14 31 32 35 36 +TICK +CX 2 3 5 6 8 9 10 11 12 13 14 15 16 17 18 19 22 23 24 25 28 29 30 31 32 33 35 36 +DEPOLARIZE2(0.001) 2 3 5 6 8 9 10 11 12 13 14 15 16 17 18 19 22 23 24 25 28 29 30 31 32 33 35 36 +DEPOLARIZE1(0.001) 0 1 4 7 20 21 26 27 34 +TICK +CX 0 1 8 4 10 6 5 9 7 11 22 15 24 17 26 19 14 21 16 23 18 25 32 27 34 29 28 33 +DEPOLARIZE2(0.001) 0 1 8 4 10 6 5 9 7 11 22 15 24 17 26 19 14 21 16 23 18 25 32 27 34 29 28 33 +DEPOLARIZE1(0.001) 2 3 12 13 20 30 31 35 36 +TICK +CX 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE2(0.001) 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE1(0.001) 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +MX(0.001) 1 3 9 11 13 21 23 25 33 +M(0.001) 4 6 15 17 19 27 29 31 36 +DETECTOR(0.5, 0, 0, 0) rec[-36] rec[-18] +DETECTOR(1, 1.5, 0, 1) rec[-34] rec[-17] +DETECTOR(1, 0, 0, 3) rec[-35] rec[-9] +DETECTOR(1.5, 1.5, 0, 4) rec[-33] rec[-8] +DETECTOR(2.5, 1, 0, 2) rec[-32] rec[-16] +DETECTOR(2.5, 3, 0, 0) rec[-30] rec[-15] +DETECTOR(3, 4.5, 0, 1) rec[-28] rec[-14] +DETECTOR(3, 1, 0, 5) rec[-31] rec[-7] +DETECTOR(3, 3, 0, 3) rec[-29] rec[-6] +DETECTOR(3.5, 4.5, 0, 4) rec[-27] rec[-5] +DETECTOR(4.5, 0, 0, 0) rec[-26] rec[-13] +DETECTOR(4.5, 2, 0, 1) rec[-24] rec[-12] +DETECTOR(4.5, 4, 0, 2) rec[-22] rec[-11] +DETECTOR(5, 0, 0, 3) rec[-25] rec[-4] +DETECTOR(5, 2, 0, 4) rec[-23] rec[-3] +DETECTOR(5, 4, 0, 5) rec[-21] rec[-2] +DETECTOR(6.5, 1, 0, 2) rec[-20] rec[-10] +DETECTOR(7, 1, 0, 5) rec[-19] rec[-1] +OBSERVABLE_INCLUDE(0) rec[-18] rec[-16] rec[-13] rec[-10] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 1 3 9 11 13 21 23 25 33 4 6 15 17 19 27 29 31 36 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +MPP X0*X8*X14*X32*X35 X0*X2*X5*X8 Z0*Z2*Z5*Z8 X2*X5*X7*X10 Z2*Z5*Z7*Z10 X5*X8*X10*X14*X16*X22 Z5*Z8*Z10*Z14*Z16*Z22 X7*X10*X12*X16*X18*X24 Z7*Z10*Z12*Z16*Z18*Z24 X12*X18*X20*X26 Z12*Z18*Z20*Z26 X14*X22*X28*X32 Z14*Z22*Z28*Z32 X16*X22*X24*X28*X30*X34 Z16*Z22*Z24*Z28*Z30*Z34 X18*X24*X26*X30 Z18*Z24*Z26*Z30 X28*X32*X34*X35 Z28*Z32*Z34*Z35 +OBSERVABLE_INCLUDE(0) rec[-19] +DETECTOR(0.5, 0, 0, 0) rec[-37] rec[-36] rec[-18] +DETECTOR(1, 0, 0, 3) rec[-28] rec[-17] +DETECTOR(1, 1.5, 0, 1) rec[-37] rec[-16] +DETECTOR(1.5, 1.5, 0, 4) rec[-27] rec[-15] +DETECTOR(2.5, 1, 0, 2) rec[-34] rec[-14] +DETECTOR(3, 1, 0, 5) rec[-26] rec[-13] +DETECTOR(2.5, 3, 0, 0) rec[-35] rec[-33] rec[-12] +DETECTOR(3, 3, 0, 3) rec[-25] rec[-11] +DETECTOR(3.5, 4.5, 0, 4) rec[-24] rec[-9] +DETECTOR(4.5, 0, 0, 0) rec[-32] rec[-31] rec[-8] +DETECTOR(5, 0, 0, 3) rec[-23] rec[-7] +DETECTOR(5, 2, 0, 4) rec[-22] rec[-5] +DETECTOR(4.5, 4, 0, 2) rec[-31] rec[-4] +DETECTOR(5, 4, 0, 5) rec[-21] rec[-3] +DETECTOR(6.5, 1, 0, 2) rec[-2] +DETECTOR(7, 1, 0, 5) rec[-20] rec[-1] +DETECTOR(3, 4.5, 0, 1) rec[-34] rec[-10] +DETECTOR(4.5, 2, 0, 1) rec[-32] rec[-30] rec[-6] diff --git a/test_data/old_fail_to_configure_cases/detector_unreorder_superdense_round.stim b/test_data/old_fail_to_configure_cases/detector_unreorder_superdense_round.stim new file mode 100644 index 0000000..88e6063 --- /dev/null +++ b/test_data/old_fail_to_configure_cases/detector_unreorder_superdense_round.stim @@ -0,0 +1,122 @@ +QUBIT_COORDS(0, 0) 0 +QUBIT_COORDS(1, 0) 1 +QUBIT_COORDS(1, 1) 2 +QUBIT_COORDS(1, 2) 3 +QUBIT_COORDS(2, 0) 4 +QUBIT_COORDS(2, 1) 5 +QUBIT_COORDS(2, 2) 6 +QUBIT_COORDS(2, 3) 7 +QUBIT_COORDS(3, 0) 8 +QUBIT_COORDS(3, 1) 9 +QUBIT_COORDS(3, 2) 10 +QUBIT_COORDS(3, 3) 11 +QUBIT_COORDS(3, 4) 12 +QUBIT_COORDS(3, 5) 13 +QUBIT_COORDS(4, 0) 14 +QUBIT_COORDS(4, 1) 15 +QUBIT_COORDS(4, 2) 16 +QUBIT_COORDS(4, 3) 17 +QUBIT_COORDS(4, 4) 18 +QUBIT_COORDS(4, 5) 19 +QUBIT_COORDS(4, 6) 20 +QUBIT_COORDS(5, 0) 21 +QUBIT_COORDS(5, 1) 22 +QUBIT_COORDS(5, 2) 23 +QUBIT_COORDS(5, 3) 24 +QUBIT_COORDS(5, 4) 25 +QUBIT_COORDS(5, 5) 26 +QUBIT_COORDS(6, 0) 27 +QUBIT_COORDS(6, 1) 28 +QUBIT_COORDS(6, 2) 29 +QUBIT_COORDS(6, 3) 30 +QUBIT_COORDS(6, 4) 31 +QUBIT_COORDS(7, 0) 32 +QUBIT_COORDS(7, 1) 33 +QUBIT_COORDS(7, 2) 34 +QUBIT_COORDS(8, 0) 35 +QUBIT_COORDS(8, 1) 36 +MPP X0*X8*X14*X32*X35 X0*X2*X5*X8 Z0*Z2*Z5*Z8 X2*X5*X7*X10 Z2*Z5*Z7*Z10 X5*X8*X10*X14*X16*X22 Z5*Z8*Z10*Z14*Z16*Z22 X7*X10*X12*X16*X18*X24 Z7*Z10*Z12*Z16*Z18*Z24 X12*X18*X20*X26 Z12*Z18*Z20*Z26 X14*X22*X28*X32 Z14*Z22*Z28*Z32 X16*X22*X24*X28*X30*X34 Z16*Z22*Z24*Z28*Z30*Z34 X18*X24*X26*X30 Z18*Z24*Z26*Z30 X28*X32*X34*X35 Z28*Z32*Z34*Z35 +OBSERVABLE_INCLUDE(0) rec[-19] +TICK +RX 1 3 9 11 13 21 23 25 33 +R 4 6 15 17 19 27 29 31 36 +X_ERROR(0.001) 4 6 15 17 19 27 29 31 36 +Z_ERROR(0.001) 1 3 9 11 13 21 23 25 33 +DEPOLARIZE1(0.001) 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +CX 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE2(0.001) 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE1(0.001) 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +CX 1 0 4 8 6 10 9 5 11 7 15 22 17 24 19 26 21 14 23 16 25 18 27 32 29 34 33 28 +DEPOLARIZE2(0.001) 1 0 4 8 6 10 9 5 11 7 15 22 17 24 19 26 21 14 23 16 25 18 27 32 29 34 33 28 +DEPOLARIZE1(0.001) 2 3 12 13 20 30 31 35 36 +TICK +CX 3 2 6 5 9 8 11 10 13 12 15 14 17 16 19 18 23 22 25 24 29 28 31 30 33 32 36 35 +DEPOLARIZE2(0.001) 3 2 6 5 9 8 11 10 13 12 15 14 17 16 19 18 23 22 25 24 29 28 31 30 33 32 36 35 +DEPOLARIZE1(0.001) 0 1 4 7 20 21 26 27 34 +TICK +CX 1 2 4 5 6 7 9 10 11 12 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 33 34 +DEPOLARIZE2(0.001) 1 2 4 5 6 7 9 10 11 12 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 33 34 +DEPOLARIZE1(0.001) 0 3 8 13 14 31 32 35 36 +TICK +CX 2 1 5 4 7 6 10 9 12 11 16 15 18 17 20 19 22 21 24 23 26 25 28 27 30 29 34 33 +DEPOLARIZE2(0.001) 2 1 5 4 7 6 10 9 12 11 16 15 18 17 20 19 22 21 24 23 26 25 28 27 30 29 34 33 +DEPOLARIZE1(0.001) 0 3 8 13 14 31 32 35 36 +TICK +CX 2 3 5 6 8 9 10 11 12 13 14 15 16 17 18 19 22 23 24 25 28 29 30 31 32 33 35 36 +DEPOLARIZE2(0.001) 2 3 5 6 8 9 10 11 12 13 14 15 16 17 18 19 22 23 24 25 28 29 30 31 32 33 35 36 +DEPOLARIZE1(0.001) 0 1 4 7 20 21 26 27 34 +TICK +CX 0 1 8 4 10 6 5 9 7 11 22 15 24 17 26 19 14 21 16 23 18 25 32 27 34 29 28 33 +DEPOLARIZE2(0.001) 0 1 8 4 10 6 5 9 7 11 22 15 24 17 26 19 14 21 16 23 18 25 32 27 34 29 28 33 +DEPOLARIZE1(0.001) 2 3 12 13 20 30 31 35 36 +TICK +CX 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE2(0.001) 1 4 3 6 9 15 11 17 13 19 21 27 23 29 25 31 33 36 +DEPOLARIZE1(0.001) 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +MX(0.001) 1 3 9 11 13 21 23 25 33 +M(0.001) 4 6 15 17 19 27 29 31 36 +DETECTOR(0.5, 0, 0, 0) rec[-36] rec[-18] +DETECTOR(1, 1.5, 0, 1) rec[-34] rec[-17] +DETECTOR(1, 0, 0, 3) rec[-35] rec[-9] +DETECTOR(1.5, 1.5, 0, 4) rec[-33] rec[-8] +DETECTOR(2.5, 1, 0, 2) rec[-32] rec[-16] +DETECTOR(2.5, 3, 0, 0) rec[-30] rec[-15] +DETECTOR(3, 4.5, 0, 1) rec[-28] rec[-14] +DETECTOR(3, 1, 0, 5) rec[-31] rec[-7] +DETECTOR(3, 3, 0, 3) rec[-29] rec[-6] +DETECTOR(3.5, 4.5, 0, 4) rec[-27] rec[-5] +DETECTOR(4.5, 0, 0, 0) rec[-26] rec[-13] +DETECTOR(4.5, 2, 0, 1) rec[-24] rec[-12] +DETECTOR(4.5, 4, 0, 2) rec[-22] rec[-11] +DETECTOR(5, 0, 0, 3) rec[-25] rec[-4] +DETECTOR(5, 2, 0, 4) rec[-23] rec[-3] +DETECTOR(5, 4, 0, 5) rec[-21] rec[-2] +DETECTOR(6.5, 1, 0, 2) rec[-20] rec[-10] +DETECTOR(7, 1, 0, 5) rec[-19] rec[-1] +OBSERVABLE_INCLUDE(0) rec[-18] rec[-16] rec[-13] rec[-10] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 1 3 9 11 13 21 23 25 33 4 6 15 17 19 27 29 31 36 0 2 5 7 8 10 12 14 16 18 20 22 24 26 28 30 32 34 35 +TICK +MPP X0*X8*X14*X32*X35 X0*X2*X5*X8 Z0*Z2*Z5*Z8 X2*X5*X7*X10 Z2*Z5*Z7*Z10 X5*X8*X10*X14*X16*X22 Z5*Z8*Z10*Z14*Z16*Z22 X7*X10*X12*X16*X18*X24 Z7*Z10*Z12*Z16*Z18*Z24 X12*X18*X20*X26 Z12*Z18*Z20*Z26 X14*X22*X28*X32 Z14*Z22*Z28*Z32 X16*X22*X24*X28*X30*X34 Z16*Z22*Z24*Z28*Z30*Z34 X18*X24*X26*X30 Z18*Z24*Z26*Z30 X28*X32*X34*X35 Z28*Z32*Z34*Z35 +OBSERVABLE_INCLUDE(0) rec[-19] +DETECTOR(0.5, 0, 0, 0) rec[-37] rec[-36] rec[-18] +DETECTOR(1, 0, 0, 3) rec[-28] rec[-17] +DETECTOR(1, 1.5, 0, 1) rec[-37] rec[-16] +DETECTOR(1.5, 1.5, 0, 4) rec[-27] rec[-15] +DETECTOR(2.5, 1, 0, 2) rec[-34] rec[-14] +DETECTOR(3, 1, 0, 5) rec[-26] rec[-13] +DETECTOR(2.5, 3, 0, 0) rec[-35] rec[-33] rec[-12] +DETECTOR(3, 3, 0, 3) rec[-25] rec[-11] +DETECTOR(3, 4.5, 0, 1) rec[-34] rec[-10] +DETECTOR(3.5, 4.5, 0, 4) rec[-24] rec[-9] +DETECTOR(4.5, 0, 0, 0) rec[-32] rec[-31] rec[-8] +DETECTOR(5, 0, 0, 3) rec[-23] rec[-7] +DETECTOR(4.5, 2, 0, 1) rec[-32] rec[-30] rec[-6] +DETECTOR(5, 2, 0, 4) rec[-22] rec[-5] +DETECTOR(4.5, 4, 0, 2) rec[-31] rec[-4] +DETECTOR(5, 4, 0, 5) rec[-21] rec[-3] +DETECTOR(6.5, 1, 0, 2) rec[-2] +DETECTOR(7, 1, 0, 5) rec[-20] rec[-1] diff --git a/test_data/old_fail_to_configure_cases/hard_config.stim b/test_data/old_fail_to_configure_cases/hard_config.stim new file mode 100644 index 0000000..82d3207 --- /dev/null +++ b/test_data/old_fail_to_configure_cases/hard_config.stim @@ -0,0 +1,392 @@ +QUBIT_COORDS(-2, -3) 0 +QUBIT_COORDS(-2, -2) 1 +QUBIT_COORDS(-1, -3) 2 +QUBIT_COORDS(-1, -2) 3 +QUBIT_COORDS(-1, -1) 4 +QUBIT_COORDS(0, -3) 5 +QUBIT_COORDS(0, -2) 6 +QUBIT_COORDS(0, -1) 7 +QUBIT_COORDS(0, 0) 8 +QUBIT_COORDS(0, 1) 9 +QUBIT_COORDS(1, -3) 10 +QUBIT_COORDS(1, -2) 11 +QUBIT_COORDS(1, -1) 12 +QUBIT_COORDS(1, 0) 13 +QUBIT_COORDS(1, 1) 14 +QUBIT_COORDS(1, 2) 15 +QUBIT_COORDS(2, -3) 16 +QUBIT_COORDS(2, -2) 17 +QUBIT_COORDS(2, -1) 18 +QUBIT_COORDS(2, 0) 19 +QUBIT_COORDS(2, 1) 20 +QUBIT_COORDS(2, 2) 21 +QUBIT_COORDS(3, -3) 22 +QUBIT_COORDS(3, -2) 23 +QUBIT_COORDS(3, -1) 24 +QUBIT_COORDS(3, 0) 25 +QUBIT_COORDS(3, 1) 26 +QUBIT_COORDS(3, 2) 27 +QUBIT_COORDS(4, -3) 28 +QUBIT_COORDS(4, -2) 29 +QUBIT_COORDS(4, -1) 30 +QUBIT_COORDS(4, 0) 31 +QUBIT_COORDS(4, 1) 32 +QUBIT_COORDS(4, 2) 33 +QUBIT_COORDS(5, -3) 34 +QUBIT_COORDS(5, -2) 35 +QUBIT_COORDS(5, -1) 36 +QUBIT_COORDS(5, 0) 37 +QUBIT_COORDS(5, 1) 38 +QUBIT_COORDS(6, -3) 39 +RX 9 19 21 32 +R 8 13 15 20 26 31 33 37 14 25 27 38 +X_ERROR(0.001) 8 13 15 20 26 31 33 37 14 25 27 38 +Z_ERROR(0.001) 9 19 21 32 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 34 35 36 39 +TICK +CX 9 14 19 25 21 27 32 38 +DEPOLARIZE2(0.001) 9 14 19 25 21 27 32 38 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 39 +TICK +CX 13 19 20 14 26 32 31 25 +DEPOLARIZE2(0.001) 13 19 20 14 26 32 31 25 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 15 16 17 18 21 22 23 24 27 28 29 30 33 34 35 36 37 38 39 +TICK +CX 15 14 20 21 26 27 33 32 +DEPOLARIZE2(0.001) 15 14 20 21 26 27 33 32 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 16 17 18 19 22 23 24 25 28 29 30 31 34 35 36 37 38 39 +TICK +CX 8 9 13 14 15 21 20 19 26 25 31 32 33 27 37 38 +DEPOLARIZE2(0.001) 8 9 13 14 15 21 20 19 26 25 31 32 33 27 37 38 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 34 35 36 39 +TICK +CX 14 20 19 13 25 31 32 26 +DEPOLARIZE2(0.001) 14 20 19 13 25 31 32 26 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 15 16 17 18 21 22 23 24 27 28 29 30 33 34 35 36 37 38 39 +TICK +CX 14 15 21 20 27 26 32 33 +DEPOLARIZE2(0.001) 14 15 21 20 27 26 32 33 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 16 17 18 19 22 23 24 25 28 29 30 31 34 35 36 37 38 39 +TICK +CX 9 8 14 13 19 20 21 15 25 26 27 33 32 31 38 37 +DEPOLARIZE2(0.001) 9 8 14 13 19 20 21 15 25 26 27 33 32 31 38 37 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 34 35 36 39 +TICK +CX 9 14 19 25 21 27 32 38 +DEPOLARIZE2(0.001) 9 14 19 25 21 27 32 38 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 39 +TICK +MX(0.001) 9 19 21 32 +M(0.001) 14 25 27 38 +DETECTOR(1, 1, 0, 3, 999) rec[-4] +DETECTOR(3, 0, 0, 4, 999) rec[-3] +DETECTOR(3, 2, 0, 5, 999) rec[-2] +DETECTOR(5, 1, 0, 3, 999) rec[-1] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 9 19 21 32 14 25 27 38 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 39 +TICK +RX 9 19 21 32 +R 14 25 27 38 +X_ERROR(0.001) 14 25 27 38 +Z_ERROR(0.001) 9 19 21 32 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 39 +TICK +CX 9 14 19 25 21 27 32 38 +DEPOLARIZE2(0.001) 9 14 19 25 21 27 32 38 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 39 +TICK +CX 13 19 20 14 26 32 31 25 +DEPOLARIZE2(0.001) 13 19 20 14 26 32 31 25 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 15 16 17 18 21 22 23 24 27 28 29 30 33 34 35 36 37 38 39 +TICK +CX 15 14 20 21 26 27 33 32 +DEPOLARIZE2(0.001) 15 14 20 21 26 27 33 32 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 16 17 18 19 22 23 24 25 28 29 30 31 34 35 36 37 38 39 +TICK +CX 8 9 13 14 15 21 20 19 26 25 31 32 33 27 37 38 +DEPOLARIZE2(0.001) 8 9 13 14 15 21 20 19 26 25 31 32 33 27 37 38 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 34 35 36 39 +TICK +CX 14 20 19 13 25 31 32 26 +DEPOLARIZE2(0.001) 14 20 19 13 25 31 32 26 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 15 16 17 18 21 22 23 24 27 28 29 30 33 34 35 36 37 38 39 +TICK +CX 14 15 21 20 27 26 32 33 +DEPOLARIZE2(0.001) 14 15 21 20 27 26 32 33 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 16 17 18 19 22 23 24 25 28 29 30 31 34 35 36 37 38 39 +TICK +CX 9 8 14 13 19 20 21 15 25 26 27 33 32 31 38 37 +DEPOLARIZE2(0.001) 9 8 14 13 19 20 21 15 25 26 27 33 32 31 38 37 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 34 35 36 39 +TICK +CX 9 14 19 25 21 27 32 38 +DEPOLARIZE2(0.001) 9 14 19 25 21 27 32 38 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 39 +TICK +MX(0.001) 9 19 21 32 +M(0.001) 14 25 27 38 +MX(0.001) 37 +DETECTOR(1, 1, 0, 3, 999) rec[-5] +DETECTOR(3, 0, 0, 4, 999) rec[-12] rec[-11] rec[-4] +DETECTOR(3, 2, 0, 5, 999) rec[-12] rec[-11] rec[-3] +DETECTOR(5, 1, 0, 3, 999) rec[-2] +DETECTOR(0, 1, 0, 0, 999) rec[-17] rec[-9] +DETECTOR(2, 0, 0, 1, 999) rec[-16] rec[-8] +DETECTOR(2, 2, 0, 2, 999) rec[-15] rec[-7] +DETECTOR(4, 1, 0, 0, 999) rec[-14] rec[-6] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 9 19 21 32 14 25 27 38 37 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 39 +TICK +RX 32 27 25 21 19 14 9 +Z_ERROR(0.001) 32 27 25 21 19 14 9 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 38 39 +TICK +CX 9 8 14 13 19 20 21 15 25 26 27 33 32 31 +DEPOLARIZE2(0.001) 9 8 14 13 19 20 21 15 25 26 27 33 32 31 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 34 35 36 37 38 39 +TICK +CX 14 9 20 21 26 27 +DEPOLARIZE2(0.001) 14 9 20 21 26 27 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 19 22 23 24 25 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +CX 20 14 26 32 +DEPOLARIZE2(0.001) 20 14 26 32 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 15 16 17 18 19 21 22 23 24 25 27 28 29 30 31 33 34 35 36 37 38 39 +TICK +CX 20 26 +DEPOLARIZE2(0.001) 20 26 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 21 22 23 24 25 27 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +MX(0.001) 20 +M(0.001) 32 27 26 21 14 9 +DETECTOR(2, 1, 0, -1, 999) rec[-16] rec[-13] rec[-12] rec[-10] rec[-8] rec[-7] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 20 32 27 26 21 14 9 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 19 22 23 24 25 28 29 30 31 33 34 35 36 37 38 39 +TICK +RX 20 +R 9 14 21 26 27 32 +X_ERROR(0.001) 9 14 21 26 27 32 +Z_ERROR(0.001) 20 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 19 22 23 24 25 28 29 30 31 33 34 35 36 37 38 39 +TICK +CX 20 26 +DEPOLARIZE2(0.001) 20 26 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 21 22 23 24 25 27 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +CX 20 14 26 32 +DEPOLARIZE2(0.001) 20 14 26 32 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 15 16 17 18 19 21 22 23 24 25 27 28 29 30 31 33 34 35 36 37 38 39 +TICK +CX 14 9 20 21 26 27 +DEPOLARIZE2(0.001) 14 9 20 21 26 27 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 19 22 23 24 25 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +CX 9 8 14 13 19 20 21 15 25 26 27 33 32 31 +DEPOLARIZE2(0.001) 9 8 14 13 19 20 21 15 25 26 27 33 32 31 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 34 35 36 37 38 39 +TICK +MX(0.001) 9 14 19 21 25 27 32 +OBSERVABLE_INCLUDE(0) rec[-3] rec[-5] +DETECTOR(2, 1, 0, -1, 999) rec[-14] rec[-7] rec[-6] rec[-5] rec[-4] rec[-3] rec[-2] rec[-1] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 9 14 19 21 25 27 32 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 33 34 35 36 37 38 39 +TICK +RX 9 19 21 +R 14 25 27 +X_ERROR(0.001) 14 25 27 +Z_ERROR(0.001) 9 19 21 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +CX 9 14 19 25 21 27 +DEPOLARIZE2(0.001) 9 14 19 25 21 27 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +CX 13 19 20 14 31 25 +DEPOLARIZE2(0.001) 13 19 20 14 31 25 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 15 16 17 18 21 22 23 24 26 27 28 29 30 32 33 34 35 36 37 38 39 +TICK +CX 15 14 20 21 26 27 +DEPOLARIZE2(0.001) 15 14 20 21 26 27 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 16 17 18 19 22 23 24 25 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +CX 8 9 13 14 15 21 20 19 26 25 33 27 +DEPOLARIZE2(0.001) 8 9 13 14 15 21 20 19 26 25 33 27 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 31 32 34 35 36 37 38 39 +TICK +CX 14 20 19 13 25 31 +DEPOLARIZE2(0.001) 14 20 19 13 25 31 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 15 16 17 18 21 22 23 24 26 27 28 29 30 32 33 34 35 36 37 38 39 +TICK +CX 14 15 21 20 27 26 +DEPOLARIZE2(0.001) 14 15 21 20 27 26 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 16 17 18 19 22 23 24 25 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +CX 9 8 14 13 19 20 21 15 25 26 27 33 +DEPOLARIZE2(0.001) 9 8 14 13 19 20 21 15 25 26 27 33 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 10 11 12 16 17 18 22 23 24 28 29 30 31 32 34 35 36 37 38 39 +TICK +CX 9 14 19 25 21 27 +DEPOLARIZE2(0.001) 9 14 19 25 21 27 +DEPOLARIZE1(0.001) 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +MX(0.001) 9 19 21 +M(0.001) 14 25 27 +DETECTOR(1, 1, 0, 3, 999) rec[-16] rec[-14] rec[-3] +DETECTOR(3, 0, 0, 4, 999) rec[-24] rec[-23] rec[-19] rec[-15] rec[-2] +DETECTOR(3, 2, 0, 5, 999) rec[-24] rec[-23] rec[-18] rec[-16] rec[-1] +DETECTOR(0, 1, 0, 0, 999) rec[-29] rec[-25] rec[-11] rec[-6] +DETECTOR(2, 0, 0, 1, 999) rec[-28] rec[-25] rec[-23] rec[-11] rec[-9] rec[-5] +DETECTOR(2, 2, 0, 2, 999) rec[-27] rec[-25] rec[-23] rec[-11] rec[-9] rec[-4] +OBSERVABLE_INCLUDE(0) rec[-13] rec[-12] rec[-11] rec[-10] rec[-9] rec[-8] rec[-7] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 9 19 21 14 25 27 0 1 2 3 4 5 6 7 8 10 11 12 13 15 16 17 18 20 22 23 24 26 28 29 30 31 32 33 34 35 36 37 38 39 +TICK +RX 1 2 16 22 24 34 +R 0 3 4 5 6 11 17 18 28 29 35 39 +X_ERROR(0.001) 0 3 4 5 6 11 17 18 28 29 35 39 +Z_ERROR(0.001) 1 2 16 22 24 34 +DEPOLARIZE1(0.001) 7 8 9 10 12 13 14 15 19 20 21 23 25 26 27 30 31 32 33 36 37 38 +TICK +CX 1 3 2 5 16 17 22 28 24 18 34 39 +DEPOLARIZE2(0.001) 1 3 2 5 16 17 22 28 24 18 34 39 +DEPOLARIZE1(0.001) 0 4 6 7 8 9 10 11 12 13 14 15 19 20 21 23 25 26 27 29 30 31 32 33 35 36 37 38 +TICK +CX 1 0 3 4 5 6 17 11 28 29 34 35 +DEPOLARIZE2(0.001) 1 0 3 4 5 6 17 11 28 29 34 35 +DEPOLARIZE1(0.001) 2 7 8 9 10 12 13 14 15 16 18 19 20 21 22 23 24 25 26 27 30 31 32 33 36 37 38 39 +TICK +CX 0 1 4 3 6 5 11 17 29 28 39 34 +DEPOLARIZE2(0.001) 0 1 4 3 6 5 11 17 29 28 39 34 +DEPOLARIZE1(0.001) 2 7 8 9 10 12 13 14 15 16 18 19 20 21 22 23 24 25 26 27 30 31 32 33 35 36 37 38 +TICK +RX 1 5 7 9 17 19 21 28 30 +R 3 10 12 14 23 25 27 34 36 +X_ERROR(0.001) 3 10 12 14 23 25 27 34 36 +Z_ERROR(0.001) 1 5 7 9 17 19 21 28 30 +DEPOLARIZE1(0.001) 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 32 33 35 37 38 39 +TICK +CX 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE2(0.001) 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE1(0.001) 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 32 33 35 37 38 39 +TICK +CX 1 0 3 2 7 6 9 8 12 11 14 13 17 16 19 18 21 20 23 22 25 24 27 26 30 29 36 35 +DEPOLARIZE2(0.001) 1 0 3 2 7 6 9 8 12 11 14 13 17 16 19 18 21 20 23 22 25 24 27 26 30 29 36 35 +DEPOLARIZE1(0.001) 4 5 10 15 28 31 32 33 34 37 38 39 +TICK +CX 3 4 5 6 7 8 10 11 12 13 14 15 17 18 19 20 23 24 25 26 28 29 30 31 34 35 +DEPOLARIZE2(0.001) 3 4 5 6 7 8 10 11 12 13 14 15 17 18 19 20 23 24 25 26 28 29 30 31 34 35 +DEPOLARIZE1(0.001) 0 1 2 9 16 21 22 27 32 33 36 37 38 39 +TICK +CX 3 6 5 2 7 4 10 16 12 18 14 20 17 11 19 13 21 15 23 29 25 31 27 33 28 22 30 24 34 39 +DEPOLARIZE2(0.001) 3 6 5 2 7 4 10 16 12 18 14 20 17 11 19 13 21 15 23 29 25 31 27 33 28 22 30 24 34 39 +DEPOLARIZE1(0.001) 0 1 8 9 26 32 35 36 37 38 +TICK +CX 0 1 2 3 6 7 8 9 11 12 13 14 16 17 18 19 20 21 22 23 24 25 26 27 29 30 35 36 +DEPOLARIZE2(0.001) 0 1 2 3 6 7 8 9 11 12 13 14 16 17 18 19 20 21 22 23 24 25 26 27 29 30 35 36 +DEPOLARIZE1(0.001) 4 5 10 15 28 31 32 33 34 37 38 39 +TICK +CX 4 3 6 5 8 7 11 10 13 12 15 14 18 17 20 19 24 23 26 25 29 28 31 30 35 34 +DEPOLARIZE2(0.001) 4 3 6 5 8 7 11 10 13 12 15 14 18 17 20 19 24 23 26 25 29 28 31 30 35 34 +DEPOLARIZE1(0.001) 0 1 2 9 16 21 22 27 32 33 36 37 38 39 +TICK +CX 2 5 4 7 6 3 11 17 13 19 15 21 16 10 18 12 20 14 22 28 24 30 29 23 31 25 33 27 39 34 +DEPOLARIZE2(0.001) 2 5 4 7 6 3 11 17 13 19 15 21 16 10 18 12 20 14 22 28 24 30 29 23 31 25 33 27 39 34 +DEPOLARIZE1(0.001) 0 1 8 9 26 32 35 36 37 38 +TICK +CX 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE2(0.001) 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE1(0.001) 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 32 33 35 37 38 39 +TICK +MX(0.001) 1 5 7 9 17 19 21 28 30 +M(0.001) 3 10 12 14 23 25 27 34 36 +OBSERVABLE_INCLUDE(0) rec[-10] rec[-13] rec[-14] rec[-15] rec[-16] rec[-18] +DETECTOR(-2, -2, 0, 0) rec[-18] +DETECTOR(-1, -2, 0, 3) rec[-9] +DETECTOR(0, -3, 0, 1) rec[-17] +DETECTOR(0, 1, 0, 0, 999) rec[-24] rec[-15] +DETECTOR(1, -3, 0, 4) rec[-8] +DETECTOR(1, 1, 0, 3, 999) rec[-6] +DETECTOR(2, -2, 0, 0) rec[-14] +DETECTOR(2, 0, 0, 1, 999) rec[-23] rec[-13] +DETECTOR(2, 2, 0, 2, 999) rec[-22] rec[-12] +DETECTOR(3, -2, 0, 3) rec[-5] +DETECTOR(3, 0, 0, 4, 999) rec[-20] rec[-19] rec[-4] +DETECTOR(3, 2, 0, 5, 999) rec[-20] rec[-19] rec[-3] +DETECTOR(4, -3, 0, 1) rec[-11] +DETECTOR(5, -3, 0, 4) rec[-2] +SHIFT_COORDS(0, 0, 1) +DEPOLARIZE1(0.001) 1 5 7 9 17 19 21 28 30 3 10 12 14 23 25 27 34 36 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 32 33 35 37 38 39 +TICK +RX 1 5 7 9 17 19 21 28 30 +R 3 10 12 14 23 25 27 34 36 +X_ERROR(0.001) 3 10 12 14 23 25 27 34 36 +Z_ERROR(0.001) 1 5 7 9 17 19 21 28 30 +DEPOLARIZE1(0.001) 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 32 33 35 37 38 39 +TICK +CX 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE2(0.001) 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE1(0.001) 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 32 33 35 37 38 39 +TICK +CX 1 0 3 2 7 6 9 8 12 11 14 13 17 16 19 18 21 20 23 22 25 24 27 26 30 29 36 35 +DEPOLARIZE2(0.001) 1 0 3 2 7 6 9 8 12 11 14 13 17 16 19 18 21 20 23 22 25 24 27 26 30 29 36 35 +DEPOLARIZE1(0.001) 4 5 10 15 28 31 32 33 34 37 38 39 +TICK +CX 3 4 5 6 7 8 10 11 12 13 14 15 17 18 19 20 23 24 25 26 28 29 30 31 34 35 +DEPOLARIZE2(0.001) 3 4 5 6 7 8 10 11 12 13 14 15 17 18 19 20 23 24 25 26 28 29 30 31 34 35 +DEPOLARIZE1(0.001) 0 1 2 9 16 21 22 27 32 33 36 37 38 39 +TICK +CX 3 6 5 2 7 4 10 16 12 18 14 20 17 11 19 13 21 15 23 29 25 31 27 33 28 22 30 24 34 39 +DEPOLARIZE2(0.001) 3 6 5 2 7 4 10 16 12 18 14 20 17 11 19 13 21 15 23 29 25 31 27 33 28 22 30 24 34 39 +DEPOLARIZE1(0.001) 0 1 8 9 26 32 35 36 37 38 +TICK +CX 0 1 2 3 6 7 8 9 11 12 13 14 16 17 18 19 20 21 22 23 24 25 26 27 29 30 35 36 +DEPOLARIZE2(0.001) 0 1 2 3 6 7 8 9 11 12 13 14 16 17 18 19 20 21 22 23 24 25 26 27 29 30 35 36 +DEPOLARIZE1(0.001) 4 5 10 15 28 31 32 33 34 37 38 39 +TICK +CX 4 3 6 5 8 7 11 10 13 12 15 14 18 17 20 19 24 23 26 25 29 28 31 30 35 34 +DEPOLARIZE2(0.001) 4 3 6 5 8 7 11 10 13 12 15 14 18 17 20 19 24 23 26 25 29 28 31 30 35 34 +DEPOLARIZE1(0.001) 0 1 2 9 16 21 22 27 32 33 36 37 38 39 +TICK +CX 2 5 4 7 6 3 11 17 13 19 15 21 16 10 18 12 20 14 22 28 24 30 29 23 31 25 33 27 39 34 +DEPOLARIZE2(0.001) 2 5 4 7 6 3 11 17 13 19 15 21 16 10 18 12 20 14 22 28 24 30 29 23 31 25 33 27 39 34 +DEPOLARIZE1(0.001) 0 1 8 9 26 32 35 36 37 38 +TICK +CX 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE2(0.001) 1 3 5 10 7 12 9 14 17 23 19 25 21 27 28 34 30 36 +DEPOLARIZE1(0.001) 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 32 33 35 37 38 39 +TICK +MX(0.001) 1 5 7 9 17 19 21 28 30 +M(0.001) 3 10 12 14 23 25 27 34 36 +MX(0.001) 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 33 35 39 +DEPOLARIZE1(0.001) 1 5 7 9 17 19 21 28 30 3 10 12 14 23 25 27 34 36 0 2 4 6 8 11 13 15 16 18 20 22 24 26 29 31 33 35 39 32 37 38 +OBSERVABLE_INCLUDE(0) rec[-29] rec[-32] rec[-33] rec[-34] rec[-35] rec[-37] +DETECTOR(-2, -2, 0, 0) rec[-37] +DETECTOR(-1, -2, 0, 3) rec[-46] rec[-28] +DETECTOR(0, -3, 0, 1) rec[-54] rec[-53] rec[-36] +DETECTOR(0, -1, 0, 2) rec[-54] rec[-52] rec[-35] +DETECTOR(0, 1, 0, 0) rec[-53] rec[-34] +DETECTOR(1, -3, 0, 4) rec[-45] rec[-27] +DETECTOR(1, -1, 0, 5) rec[-44] rec[-26] +DETECTOR(1, 1, 0, 3) rec[-43] rec[-25] +DETECTOR(2, -2, 0, 0) rec[-50] rec[-33] +DETECTOR(2, 0, 0, 1) rec[-51] rec[-49] rec[-32] +DETECTOR(2, 2, 0, 2) rec[-50] rec[-49] rec[-31] +DETECTOR(3, -2, 0, 3) rec[-42] rec[-24] +DETECTOR(3, 0, 0, 4) rec[-41] rec[-23] +DETECTOR(3, 2, 0, 5) rec[-40] rec[-22] +DETECTOR(4, -3, 0, 1) rec[-48] rec[-47] rec[-30] +DETECTOR(4, -1, 0, 2) rec[-48] rec[-29] +DETECTOR(5, -3, 0, 4) rec[-39] rec[-21] +DETECTOR(5, -1, 0, 5) rec[-38] rec[-20] +SHIFT_COORDS(0, 0, 1) +DETECTOR(-2, -2, 0, 0) rec[-19] rec[-18] rec[-17] rec[-16] +DETECTOR(0, -3, 0, 1) rec[-36] rec[-35] rec[-18] rec[-16] rec[-14] rec[-11] +DETECTOR(0, -1, 0, 2) rec[-36] rec[-34] rec[-17] rec[-16] rec[-15] rec[-14] rec[-13] rec[-10] +DETECTOR(0, 1, 0, 0) rec[-35] rec[-15] rec[-13] rec[-12] rec[-9] +DETECTOR(2, -2, 0, 0) rec[-32] rec[-14] rec[-11] rec[-10] rec[-8] rec[-7] rec[-5] +DETECTOR(2, 0, 0, 1) rec[-33] rec[-31] rec[-13] rec[-10] rec[-9] rec[-7] rec[-6] rec[-4] +DETECTOR(2, 2, 0, 2) rec[-32] rec[-31] rec[-12] rec[-9] rec[-6] rec[-3] +DETECTOR(4, -3, 0, 1) rec[-30] rec[-29] rec[-8] rec[-5] rec[-2] rec[-1] +DETECTOR(4, -1, 0, 2) rec[-30] rec[-7] rec[-5] rec[-4] rec[-2] +OBSERVABLE_INCLUDE(0) rec[-19] rec[-18] rec[-17] rec[-16] rec[-15] rec[-14] rec[-13] rec[-12] rec[-11] rec[-10] rec[-9] rec[-8] rec[-7] rec[-6] rec[-5] rec[-4] rec[-3] rec[-2] rec[-1] +SHIFT_COORDS(0, 0, 1) diff --git a/test_data/phenom_color_code_d5_r5_p1000_with_ignored.stim b/test_data/phenom_color_code_d5_r5_p1000_with_ignored.stim new file mode 100644 index 0000000..bc309a5 --- /dev/null +++ b/test_data/phenom_color_code_d5_r5_p1000_with_ignored.stim @@ -0,0 +1,83 @@ +QUBIT_COORDS(0, 0) 0 +QUBIT_COORDS(0, 1) 1 +QUBIT_COORDS(1, 1) 2 +QUBIT_COORDS(1, 2) 3 +QUBIT_COORDS(1, 3) 4 +QUBIT_COORDS(1, 4) 5 +QUBIT_COORDS(2, 1) 6 +QUBIT_COORDS(2, 2) 7 +QUBIT_COORDS(2, 3) 8 +QUBIT_COORDS(2, 4) 9 +QUBIT_COORDS(2, 5) 10 +QUBIT_COORDS(2, 6) 11 +QUBIT_COORDS(2, 7) 12 +QUBIT_COORDS(3, 1) 13 +QUBIT_COORDS(3, 2) 14 +QUBIT_COORDS(3, 3) 15 +QUBIT_COORDS(3, 4) 16 +QUBIT_COORDS(3, 5) 17 +QUBIT_COORDS(4, 1) 18 +QUBIT_COORDS(4, 2) 19 +MPP X0*X1*X2*X3*X4*X5*X6*X7*X8*X9*X10*X11*X12*X13*X14*X15*X16*X17*X18*X19 +OBSERVABLE_INCLUDE(0) rec[-1] +MPP Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8*Z9*Z10*Z11*Z12*Z13*Z14*Z15*Z16*Z17*Z18*Z19 +OBSERVABLE_INCLUDE(1) rec[-1] +MPP X1*X2*X3*X4 X2*X3*X6*X7 X3*X4*X5*X7*X8*X9 X5*X9*X10*X11 X6*X7*X8*X13*X14*X15 X13*X14*X18*X19 X8*X9*X10*X15*X16*X17 X10*X11*X12*X17 X14*X15*X16*X19 Z1*Z2*Z3*Z4 Z2*Z3*Z6*Z7 Z3*Z4*Z5*Z7*Z8*Z9 Z5*Z9*Z10*Z11 Z6*Z7*Z8*Z13*Z14*Z15 Z13*Z14*Z18*Z19 Z8*Z9*Z10*Z15*Z16*Z17 Z10*Z11*Z12*Z17 Z14*Z15*Z16*Z19 +TICK +REPEAT 5 { + # The stabilizers of a wild perfect code appear. + MPP X100*Z101*Z102*X103 X101*Z102*Z103*X104 X102*Z103*Z104*X100 X103*Z104*Z100*X101 + DEPOLARIZE1(0.001) 100 101 102 103 014 + MPP X100*Z101*Z102*X103 X101*Z102*Z103*X104 X102*Z103*Z104*X100 X103*Z104*Z100*X101 + DETECTOR(0, 0, 0, -1) rec[-1] rec[-5] + DETECTOR(0, 0, 0, -1) rec[-2] rec[-6] + DETECTOR(0, 0, 0, -1) rec[-3] rec[-7] + DETECTOR(0, 0, 0, -1) rec[-4] rec[-8] + M 100 100 + + DEPOLARIZE1(0.001) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + MPP(0.001) X1*X2*X3*X4 X2*X3*X6*X7 X3*X4*X5*X7*X8*X9 X5*X9*X10*X11 X6*X7*X8*X13*X14*X15 X13*X14*X18*X19 X8*X9*X10*X15*X16*X17 X10*X11*X12*X17 X14*X15*X16*X19 Z1*Z2*Z3*Z4 Z2*Z3*Z6*Z7 Z3*Z4*Z5*Z7*Z8*Z9 Z5*Z9*Z10*Z11 Z6*Z7*Z8*Z13*Z14*Z15 Z13*Z14*Z18*Z19 Z8*Z9*Z10*Z15*Z16*Z17 Z10*Z11*Z12*Z17 Z14*Z15*Z16*Z19 + DETECTOR(1, 1, 0, 1) rec[-46] rec[-18] + DETECTOR(1, 2, 0, 2) rec[-45] rec[-17] + DETECTOR(1, 4, 0, 0) rec[-44] rec[-16] + DETECTOR(2, 4, 0, 1) rec[-43] rec[-15] + DETECTOR(3, 1, 0, 1) rec[-42] rec[-14] + DETECTOR(3, 2, 0, 2) rec[-41] rec[-13] + DETECTOR(3, 3, 0, 2) rec[-40] rec[-12] + DETECTOR(3, 5, 0, 0) rec[-39] rec[-11] + DETECTOR(4, 2, 0, 0) rec[-38] rec[-10] + DETECTOR(1.125, 1, 0, 4) rec[-37] rec[-9] + DETECTOR(1.125, 2, 0, 5) rec[-36] rec[-8] + DETECTOR(1.125, 4, 0, 3) rec[-35] rec[-7] + DETECTOR(2.125, 4, 0, 4) rec[-34] rec[-6] + DETECTOR(3.125, 1, 0, 4) rec[-33] rec[-5] + DETECTOR(3.125, 2, 0, 5) rec[-32] rec[-4] + DETECTOR(3.125, 3, 0, 5) rec[-31] rec[-3] + DETECTOR(3.125, 5, 0, 3) rec[-30] rec[-2] + DETECTOR(4.125, 2, 0, 3) rec[-29] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK +} +MPP X1*X2*X3*X4 X2*X3*X6*X7 X3*X4*X5*X7*X8*X9 X5*X9*X10*X11 X6*X7*X8*X13*X14*X15 X13*X14*X18*X19 X8*X9*X10*X15*X16*X17 X10*X11*X12*X17 X14*X15*X16*X19 Z1*Z2*Z3*Z4 Z2*Z3*Z6*Z7 Z3*Z4*Z5*Z7*Z8*Z9 Z5*Z9*Z10*Z11 Z6*Z7*Z8*Z13*Z14*Z15 Z13*Z14*Z18*Z19 Z8*Z9*Z10*Z15*Z16*Z17 Z10*Z11*Z12*Z17 Z14*Z15*Z16*Z19 +DETECTOR(1, 1, 0, 1) rec[-36] rec[-18] +DETECTOR(1, 2, 0, 2) rec[-35] rec[-17] +DETECTOR(1, 4, 0, 0) rec[-34] rec[-16] +DETECTOR(2, 4, 0, 1) rec[-33] rec[-15] +DETECTOR(3, 1, 0, 1) rec[-32] rec[-14] +DETECTOR(3, 2, 0, 2) rec[-31] rec[-13] +DETECTOR(3, 3, 0, 2) rec[-30] rec[-12] +DETECTOR(3, 5, 0, 0) rec[-29] rec[-11] +DETECTOR(4, 2, 0, 0) rec[-28] rec[-10] +DETECTOR(1.125, 1, 0, 4) rec[-27] rec[-9] +DETECTOR(1.125, 2, 0, 5) rec[-26] rec[-8] +DETECTOR(1.125, 4, 0, 3) rec[-25] rec[-7] +DETECTOR(2.125, 4, 0, 4) rec[-24] rec[-6] +DETECTOR(3.125, 1, 0, 4) rec[-23] rec[-5] +DETECTOR(3.125, 2, 0, 5) rec[-22] rec[-4] +DETECTOR(3.125, 3, 0, 5) rec[-21] rec[-3] +DETECTOR(3.125, 5, 0, 3) rec[-20] rec[-2] +DETECTOR(4.125, 2, 0, 3) rec[-19] rec[-1] +MPP X0*X1*X2*X3*X4*X5*X6*X7*X8*X9*X10*X11*X12*X13*X14*X15*X16*X17*X18*X19 +OBSERVABLE_INCLUDE(0) rec[-1] +MPP Z0*Z1*Z2*Z3*Z4*Z5*Z6*Z7*Z8*Z9*Z10*Z11*Z12*Z13*Z14*Z15*Z16*Z17*Z18*Z19 +OBSERVABLE_INCLUDE(1) rec[-1]