QCHackers / tqec

Design automation software tools for Topological Quantum Error Correction
https://tqec.app
Apache License 2.0
58 stars 15 forks source link

WIP: Add detector automation but without anti-commute grouping #227

Closed inmzhang closed 2 weeks ago

inmzhang commented 1 month ago

This is a very basic attempt to adress #199. I tried to implement the proposal by Austin at the level of stim instead of cirq because I want this part of work can be applied to other stim-based projects more easily. The main pipeline can be splitted into the following steps:

  1. Split the stim.Circuit into fragments. By design, a fragment is the sub-circuit of a single error correction round with related resets/measurements collapsing operations.
  2. Use a strategy similar to that Craig Gideny used in the midout paper source code to compile each fragment. When compiling the fragment, the boundary stabilizers are constructed and matched to form the detectors. Note that I haven't implement anticommute grouping as proposed by Austin yet.

Currently, there are some problems in the codebase as far as I can notice and remember:

  1. The structure of the round-based error correction circuit is not well-tested. The boundary of a fragment is identified by the collapsing moments, which tried to deal with the consequtive measurements/resets moments and the circuit repeat block, this part is error-prone.
  2. At the early stage, I thought a boundary stabilizer with trivial stabilizer(all pauli operator is Identity) should alway form a detector by itself. However, consider a simplest circuit R -- MR -- M, the second detector should be DETECTOR rec[-1][-2] instead of DETECTOR rec[-1]. Though the two definitions seem equivalent when multiplying the first detector into it, there are critical differences when decoding the circuits. Thus I always try to match the boundary stabilizers between two consequtive fragments even when the weight of the stabilizer is zero.
  3. Currently, the detector annotation for the simple r=1 repetition code circuit will fail. Consider the following circuit: 图片, if we allow resets on data qubits to generate the boundary stabilizers, it might generate detectors unwanted. Maybe we can forbid resets on some specific set of qubits to generate end stabilizers to handle it. However, when the circuit gets more complex, the forbiden set can be changed over the circuit(e.g. the wiggling/sliding circuit in http://arxiv.org/abs/2302.02192). I did not have a good solution to this yet.
  4. Though the performance of the code and algorithm is not optimized, it already took around 14s to run the tests for 80 small circuits in the test cases. Note that the unimplemented anticommute grouping step might increase the time further significantly. We might need to start considering whether we should implement this in pure Python.
  5. I have passed the tests for the surface code circuits generated by stim. In my plan, I would like to test it more on the various circuits in this paper. Currently I have only included a single circuit in the directory extra_test_circuits for anticommute grouping test purpose(so it should fail for now).

Ideally, I would have completed the remaining tasks and submitted a finalized PR. However, I'm quite occupied these days and I'm not sure when I will be able to finish this. Therefore, I'm submitting it as a draft and would appreciate any help from the community to improve, refactor or even complete it.

nelimee commented 4 weeks ago

So I have been exploring this code for the past 2 days. I also made a tentative at implementing a generic method to compute stabilizers a few months ago.

From that, here are my main conclusions.

First, the problem of finding stabilizers can be split into 3 sub-problems:

  1. Splitting correctly the circuit according to the collapsing operations (resets and measurements) contained in this circuit.
  2. Pre-computing various quantities on the split sub-circuits (stabilizer propagation for example).
  3. Using the data pre-computed in step 2. to match stabilizer flows and find detectors.

Now, in more details.

1. Splitting the input circuit

The collapsing operations (measurement and resets) are the main point of interest here. What I did in my attempt, and what I think you also did in this PR, is to constraint the input circuit to avoid a (probably large) number of edge cases. The constraint I have in mind is: collapsing operations (again, measurements and resets) cannot appear everywhere in the circuit and can only appear in their own moment (sub-circuit between two TICK instructions).

This constraint makes sense as Austin already told us, this is in fact something that most hardware will want to ensure.

With that constraint in mind, there are still a few edge cases that need to be handled:

In the end, I dislike how I split the circuit in my previous attempt because I was using cirq (which would be best avoided here, I agree) and I was storing a lot of stim.Tableau.

I do not fully adhere to your method either, I feel like the code is too complex for what it is doing. In particular, as you noted, the split_stim_circuit_into_fragments is quite hard to reason about.

I think my preferred way of solving that sub-problem would be to re-use your code, but ensuring that Fragment instances start with a moment of resets and end with a moment of measurements, splitting the combined operations into two separate operations if needed. This simple change in the input circuit would remove the need to store begin/end stabilizer sources and to keep track of the sources for the next Fragment. Basically, only performing stim.Circuit manipulation and not computing anything (yet).

2. Pre-computation

We need to pre-compute a lot of information in order to hope to perform step 3 efficiently. In particular, for each collapsing operation, we need to compute how the stabilizer it propagates evolves in the circuit. Here, there is a difference between measurements and resets:

These differences mean that we will need to store enough information to recover the exact measurement associated with a given stabilizer propagation, but we do not really care about the reset associated to a propagation.

In our case, I fully agree with the step-by-step description by Austin. So our Fragment instances have beginning and end stabilizers, the beginning stabilizers being annotated with the measurement that started the (back-)propagation and the end stabilizers storing the measurement(s) that commuted and anti-commuted with the pauli string that propagated from the reset.

The goal of that step is simply to produce as efficiently as possible a data-structure that we will be able to query afterward to match stabilizers together.

If we only restrict ourselves to detectors only including measurements from the current and previous rounds, it is sufficient in this step to pre-compute the stabilizers for each Fragment separately: evolve forward each reset-generated stabilizer, evolve backward each measurement-generated stabilizer, and store the resulting stabilizer with the commuting/anti-commuting measurements in a data-structure that will be accessed at the next step.

3. Matching stabilizer flows

The approach described by Austin and implemented in your different match_* methods works for me. I only have a doubt on a specific part of the code in find_cover:

def find_cover(
    target: BoundaryStabilizer,
    sources: list[BoundaryStabilizer],
) -> list[BoundaryStabilizer] | None:
    subsets = [
        source
        for source in sources
        if target.after_collapse.contains(source.after_collapse)
    ]

I am not sure the restriction to sources that are contained by target is correct, as some sources might cancel out the Pauli terms that are not contained in target and end up forming a stabilizer.

Overall opinion

All in all, I really think that we should look at these 3 sub-problems separately for several reasons:

If you agree with what I wrote above, my plan is to reformat the code in this PR following these 3 sub-problems.

inmzhang commented 4 weeks ago

The sub-problems are reasonably well to me and it's exactly the mental model I followed when I wrote the code.

I am not sure the restriction to sources that are contained by target is correct, as some sources might cancel out the Pauli terms that are not contained in target and end up forming a stabilizer.

I guess you are right and I really cannot remember why I made this assumption, maybe because it just work well for the usual surface code circuit.

I have added some comment on the split_stim_circuit_into_fragments method, but it's still complex and kind of messy. A well-structured Fragment as you proposed should work better than the current one.

nelimee commented 4 weeks ago

I have made significant (in size) but quite trivial (in actual code change) changes on your PR in #248. I am currently starting fresh to explore the idea exposed above, with the plan to copy-paste large portions from here or #248, and adapt a few things. I will likely open another PR to show these changes, and discuss.

afowler commented 3 weeks ago

I would vote for banning the measure-reset (MR) gate and insisting on reset always being a separate gate that always follows measurement before any other gates are allowed on that qubit. I'd also vote for banning repeat blocks.

nelimee commented 3 weeks ago

I would vote for banning the measure-reset (MR) gate and insisting on reset always being a separate gate that always follows measurement before any other gates are allowed on that qubit. I'd also vote for banning repeat blocks.

The solution proposed in #249 bans all combined gates (MR, MRZ, MRX, MRY) and replaces them with their equivalent (minor note on that in #249) measurement + reset gates.

Repeat blocks are not that hard to handle, and should be already handled correctly by this PR (and #249 that started from here and made significant changes to the code).

inmzhang commented 2 weeks ago

Closes as there is the more robust and correct implementation in #249 .