extend-new-code · git:20260526.5b92aea · 2026-05-26 · sha256 6ab57e19b93e0d0d
extend-new-code git:20260526.5b92aeaA
Immutable. This exact content is served forever at /api/v1/blob/6ab57e19b93e0d0d.
---
name: extend-new-code
description: >
Add a new QEC code to LightStim by implementing QECPatch and a syndrome
extraction block. Use this skill whenever the user asks to implement a new
quantum error correcting code, extend LightStim with a custom code family,
define stabilizers and logical operators for a new code, create a new
SE_block, or understand the minimal interface a code needs to satisfy.
user-invocable: true
---
# Extend with a New QEC Code
Every LightStim code requires two classes:
1. **`QECPatch` subclass** — geometry (qubit positions) + physics (stabilizers + logicals)
2. **Extraction block** — builds one noiseless SE round as a `stim.Circuit`
## Part 1: `QECPatch` subclass
### The three build phases
```python
from lightstim.ir.qec_patch import QECPatch
class MyCode(QECPatch):
def _process_params(self):
self.distance = self.params['distance'] # validate params here
def build(self):
d = self.distance
# Phase 1: geometry — register every qubit at its (x, y) coordinate
self.add_qubit(x, y, role='data') # data qubit
self.add_qubit(x, y, role='syndrome_z') # Z-ancilla
self.add_qubit(x, y, role='syndrome_x') # X-ancilla
# Phase 2: physics — one stabilizer per ancilla
# target_dict: {(x, y): 'X'|'Z'|'Y'} for each data qubit in the stabilizer support
self.create_stim_stabilizer(
target_dict={(x1, y1): 'Z', (x2, y2): 'Z'},
syn_coord=(sx, sy), # coordinate of the ancilla qubit
type='Z', # 'X' or 'Z' (or 'Mixed' for YY etc.)
)
# Phase 3: logical operators (must register both X and Z representatives)
self.create_stim_logical({(x, y): 'Z', ...}, op_type='Z')
self.create_stim_logical({(x, y): 'X', ...}, op_type='X')
self.num_logicals = 1 # k = number of encoded logical qubits
```
### Key invariants
- `add_qubit` role must be one of `'data'`, `'syndrome_x'`, `'syndrome_z'`.
Both `syndrome_x` and `syndrome_z` populate `syndrome_indices_x` / `syndrome_indices_z`
— the SE block uses these to emit Hadamard gates and distinguish CX vs CZ layers.
- `create_stim_stabilizer` looks up `syn_coord` via `self.index_map` — register
the ancilla with `add_qubit` BEFORE calling it.
- `create_stim_logical` only takes data qubit coordinates — syndrome coords are ignored.
- You MUST set `self.num_logicals` in `build()`.
### CSS vs non-CSS
For CSS codes (most common: SC, BB, toric), stabilizers are either all-X or all-Z.
Use `type='X'` or `type='Z'` respectively. The SE block will separate them.
For non-CSS codes with Y stabilizers, use `type='Mixed'` and handle gate decomposition
in the SE block manually.
---
## Part 2: Syndrome Extraction block
The SE block generates exactly **one noiseless SE round** as a `stim.Circuit`.
Noise is injected later by `builder.build_noisy_circuit()` — never put noise here.
### Structure
```python
import stim
class MyCodeExtractionBlock:
def __init__(self, system):
self.system = system
self.circuit = stim.Circuit()
self._build()
def _build(self):
c = self.circuit
# 1. Reset all active syndrome qubits (both X and Z ancillas)
syn_z = sorted(self.system.active_syndrome_indices_z)
syn_x = sorted(self.system.active_syndrome_indices_x)
c.append("R", syn_z)
c.append("RX", syn_x)
# 2. TICK with SE_start tag — marks the idle window for noise injection
c.append("TICK", tag="SE_start")
# 3. Gate layers — one TICK between each layer
# For Z-stabilizers: CX(data → ancilla) [ancilla is "target" of CX]
# For X-stabilizers: CX(ancilla → data) [ancilla is "control" of CX]
#
# Ordering matters for hook errors — follow the code's canonical gate schedule.
# Read system.active_stabilizers_z / _x and system.index_map to get global indices.
for stab in self.system.active_stabilizers_z:
for data_idx in stab['data_indices']:
c.append("CX", [data_idx, stab['syn_idx']])
c.append("TICK")
for stab in self.system.active_stabilizers_x:
for data_idx in stab['data_indices']:
c.append("CX", [stab['syn_idx'], data_idx])
c.append("TICK")
# 4. Basis change for X-ancillas: H before measure
if syn_x:
c.append("H", syn_x)
c.append("TICK")
# 5. Measure — MUST be the LAST instruction (CircuitBuilder analyzes it)
# Z-ancillas measured in Z basis (M), X-ancillas in X basis (MX)
if syn_z: c.append("M", syn_z)
if syn_x: c.append("MX", syn_x)
```
### Critical constraints on the SE block
1. **The last instruction must be `M` or `MX`** on syndrome qubits.
`CircuitBuilder._get_back_propagated_pauli()` reads this last instruction to determine
the measurement basis and which qubits were measured.
2. **Gate ordering within one SE round affects hook errors.** For an unrotated SC, the
canonical order is: North → East → West → South neighbors. For your code, pick an
order and be consistent across all rounds.
3. **Use `system.active_stabilizers_z` / `_x` not `system.stabilizers`.**
When couplers are active, `active_stabilizers` includes coupler stabilizers.
The SE block must work correctly in both coupled and uncoupled regimes.
4. **Use global indices from `system.index_map[(x, y)]` or `stab['syn_idx']`.**
Never use local patch indices in the SE block circuit.
5. **No noise in the SE block.** The noise injector handles it via the `SE_start` tag.
---
## File layout
```
lightstim/qec_code/<your-code>/
├── __init__.py # export patch class + extraction block
├── code_patch.py # QECPatch subclass
└── SE_block.py # extraction block class
```
---
## Verification sequence
After writing both classes, verify in this order:
```python
# 1. Patch builds without errors
patch = MyCode(distance=3)
print(patch.num_qubits, len(patch.stabilizers), patch.num_logicals)
# 2. System registration
system = QECSystem()
system.add_patch(patch, name="p")
# 3. SE block builds without errors
se = MyCodeExtractionBlock(system)
# 4. Full noiseless circuit (use builder directly)
from lightstim.ir.tracker import SyndromeTracker
from lightstim.ir.builder import CircuitBuilder
tracker = SyndromeTracker(system.num_qubits, expected_num_logicals=system.num_logicals)
builder = CircuitBuilder(tracker, system)
builder.write_coordinates()
builder.initialize({q: "Z" for q in system.data_indices}, n=system.num_qubits)
builder.apply_syndrome_extraction(se.circuit, rounds=3)
builder.apply_data_readout({q: "Z" for q in system.data_indices})
# 5. Must see 0 detection events and 0 observable flips
dets, obs = builder.circuit.compile_detector_sampler().sample(100, separate_observables=True)
assert not dets.any(), "Noiseless circuit fires detectors — stabilizer or SE bug"
assert not obs.any(), "Noiseless circuit flips observable — logical operator bug"
```
## Reference script
Read `scripts/template.py` for a complete `BitFlipStrip` repetition-code example
that you can run directly to verify the pattern works end-to-end.