A common practice for simulating CSS code is to only apply the decoder for logical X (or Z) errors. Stim includes example circuits (from stim.Circuit.generated) for "surface_code:unrotated_memory_z" and "surface_code:unrotated_memory_x". What's the difference between them? If I feed them to PyMatching using sinter, will PyMatching only decode for the logical Z errors on one and the logical X errors on the other?
Another common practice is to include one perfect round of stabilizer measurement after d rounds of noisy stabilizer measurements. If we use Stim together with PyMatching for circuit-level noise simulation as in the tutorial notebook, do we need to include the perfect rounds in the input circuit that is used to generate the detector noise model? The example circuits in Stim (from stim.Circuit.generated) do not include them - only noisy rounds. Is it unnecessary to include them when using Stim/PyMatching?
A key thing to realize about Stim is that it takes a circuit-first approach to quantum error correction. Stim doesn't know about CSS codes or charge conservation or many other ideas used to define an understand QEC codes. What Stim knows is circuits. The downside of this approach is you have to learn to express your ideas in the form of a circuit. The upside is that you end up with a circuit, meaning you have done the work necessary to simulate under circuit noise and to create an artifact that can be run on hardware.
If I feed [stim's rotated Z memory circuit] to PyMatching using sinter, will PyMatching only decode for the logical Z errors [..]?
Yes. The decoder is still given both the X and Z stabilizers, and it still performs matching of detection events from both types of stabilizer, but the memory circuit only declares one observable (X observable for X memory, Z observable for Z memory). The decoder will use its internal matching to produce a prediction for that one declared observable, and sinter checks that one prediction against what was reported by the simulator.
It may sound wasteful to include the X stabilizers when checking the Z observable, but actually this information is useful. Y errors can flip the observable and leave traces in both the X and Z stabilizers. PyMatching doesn't take advantage of this, but some decoders do and they do noticeably better when you include all the information.
The underlying reason that the circuits only declare one observable is because the observables declared in a stim circuit must be deterministic. They must correspond to things you can check in practice. Since X anticommutes with Z, you can't create a circuit where they are both checked.
If you really need both, then you can create a circuit that noiselessly initializes and entangles the logical qubit with a noiseless ancilla qubit and then noiselessly verifies this entanglement later. This is possible to encode because $X_a X_L$ commutes with $Z_a Z_L$. Of course, it won't be executable on hardware due to the magically noiseless steps being crucial to the whole thing working.
do we need to include the perfect rounds in the input circuit that is used to generate the detector noise model
Real memory experiments have to initialize and measure the logical qubit using noisy operations. That is what Stim's built-in memory experiments do, while being fault tolerant.
If you want the first round to be noiseless, you can go into the circuit and delete the noise instructions placed in the first round. This is not necessary, since those circuits are designed to work despite noise during the first and last round, but nothing will stop you from doing it. Probably you will eventually have to write code to generate exactly the surface code circuits you want, as Stim's built-in ones are really just examples for getting started.
When I am making QEC circuits I do often start by using a noiseless round at the start to initialize the logical qubit, and a noiseless round at the end to measure it. I use
MPP instructions to measure every stabilizer and also the logical observable, and place no noise channels by those MPP instructions. When you're just trying to get things working and make progress, this is the simplest thing to write. But ultimately it is necessary to find an initialization strategy that is fault tolerant, because otherwise the circuit won't work on hardware.