Physical Surface-Code Memory

gadgets/01_surface_memory.py stores logical zero using physical ancillas, CNOT gates, and measurements. It verifies three reusable gadgets, applies standard physical noise, exports a Stim circuit, and derives the same experiment’s detector error model.

This is the second gadget example. Start with Verify an authored gadget to see how a custom gadget’s contract is declared. Here, the Surface recipe supplies the gadgets and their contracts.

Run from the repository root with CUDA-Q Logical and cudaq-edem installed:

python3 preview/logical/examples/gadgets/01_surface_memory.py

The default run writes surface-memory/memory.stim and surface-memory/memory.edemb without requiring Stim. Each # %% cell can also be run interactively.

Select preparation, extraction, and readout

The parameter cell chooses a distance-three code, three syndrome-extraction rounds, and physical error probability p = 0.001:

Configure the physical-memory experiment.
distance = 3
rounds = 3  # Includes one extraction in preparation and one in readout; >= 2.
p = 0.001
output = Path("surface-memory")
shots = 0  # Set > 0 to sample with Stim (pip install 'stim>=1.16,<2').
seed = 7

Select the recipe’s ancilla-based interleaved N/Z realization and connect its contracts in execution order:

Connect the reusable physical gadgets (examples/gadgets/01_surface_memory.py).
implementation = surface.interleaved_nz_definitions(distance)
prep = surface.gadget_contract(implementation.prepare_zero)
sec = surface.gadget_contract(implementation.syndrome_extraction)
readout = surface.gadget_contract(implementation.measure_z_with_syndrome)
contracts = (prep, *((sec,) * (rounds - 2)), readout)

prepare_zero prepares logical +Z and extracts a syndrome. syndrome_extraction measures the checks while preserving the logical state. measure_z_with_syndrome extracts a syndrome and then destructively measures the data, returning logical Z. Preparation has no encoded input; readout has no encoded output. Together they close the experiment’s code boundaries.

Both endpoints include an extraction, so rounds must be at least two. At rounds = 3, the sequence is:

prepare_zero[SEC] -> syndrome_extraction -> measure_z_with_syndrome[SEC + data MZ]

Verify the ideal behavior

Check each reusable gadget before adding noise:

Verify preparation, identity extraction, and destructive logical-Z readout.
verification = {
    name: cql.gadgets.verify_gadget(contract)
    for name, contract in (("prep", prep), ("SEC", sec), ("readout", readout))
}
assert all(receipt.verified for receipt in verification.values())
print("Verified prep, SEC, readout: "
      "ideal logical actions and detector contracts")

The receipts establish the declared ideal logical actions and deterministic, complete detector contracts. The readout check includes the parity returned as its logical measurement result. These stabilizer checks do not establish fault tolerance or noisy circuit distance; see Detector contracts for the supported claims.

Emit a circuit and derive its model

Use one policy for depolarization after Clifford gates, flips before measurements, and errors after resets:

noise = cql.compiler.StandardDepolarizingNoise(
    after_clifford_depolarization=p,
    before_measure_flip_probability=p,
    after_reset_flip_probability=p)

The same contracts and noise feed both circuit emission and model derivation:

Emit the noisy circuit and derive the composed model.
text = cql.compiler.emit_stim(contracts, noise=noise)
composed = cql.analysis.derive_edem_chain(contracts, noise=noise)
projected = edem.project_DEM(composed)

An extended detector error model (EDEM) records each gadget’s fault effects on detectors, logical outcomes, and its open code boundaries. This boundary information lets one gadget’s residual error propagate into later gadgets. derive_edem_chain derives and composes these models, reusing a derived model where the same gadget and fault set repeat. Once preparation and readout close the boundaries, project_DEM produces the experiment’s detector error model (DEM): fault probabilities and the detectors and observables each fault flips.

Try rounds = 10. The circuit and composed model grow to cover the additional extractions, while repeated extraction occurrences reuse the same derived gadget model. The EDEM tutorial explains the boundary coordinates and custom fault sets in detail.

Save and optionally sample

The export cell writes both artifacts:

output = Path(output)
output.mkdir(parents=True, exist_ok=True)
(output / "memory.stim").write_text(text)
(output / "memory.edemb").write_bytes(projected.to_bytes())
print(f"d={distance}, rounds={rounds}: "
      f"{sum(projected.detectors)} detectors, "
      f"{sum(projected.observables)} observable")
print(f"Artifacts: {output}")

memory.stim contains the physical circuit, its noise, detectors, and logical observable. memory.edemb contains the projected model with the original categorical fault laws. Individual depolarizing-channel alternatives remain mutually exclusive in the saved model.

To sample the physical circuit, install stim>=1.16,<2 and change shots to a positive number. The final cell uses Stim’s detector sampler:

if shots:
    import stim

    circuit = stim.Circuit(text)
    detectors, observables = circuit.compile_detector_sampler(seed=seed).sample(
        shots, separate_observables=True)
    print(f"Sampled {shots} shots")
    print(f"Shots with detection events: {detectors.any(axis=1).sum()}/{shots}")
    print("Shots with uncorrected observable flips: "
          f"{observables.any(axis=1).sum()}/{shots}")

The counts report raw detector events and uncorrected logical-observable flips. The example does not run a decoder.

For individual gadget-model exports, explicit data depolarization, a Stim reference comparison, and circuit or DEM sampling, continue to Surface-memory reference comparison. The optional Stim-emission and custom-noise tutorials instead use direct ideal MPP stabilizer measurements.

Back to examples