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:
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:
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:
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:
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.