Detector Error Models

A detector error model (DEM) is a detector error matrix (capturing which detectors each error mechanism flips) together with a noise model that assigns a likelihood to each error mechanism. It is the input a decoder needs to infer which errors occurred from a circuit’s measurement record.

In CUDA-Q you declare the parity checks (detectors) and logical observables directly inside a kernel, then extract the DEM with cudaq::dem_from_kernel (C++) or cudaq.dem_from_kernel (Python) as text in Stim’s standard .dem file format, which stim.DetectorErrorModel parses back into a decoder-ready model. The measurements that feed the declarations are measurement handles (Measuring Kernels).

Three kernel-side declarations are available in both C++ and Python : detector(m0, m1, ...) declares one detector as a parity constraint over the given measurements; detectors(prev, curr) declares N detectors by pairing two equal-length handle vectors element-wise (the standard form for cross-round detectors); and logical_observable(m0, m1, ...) declares a logical observable.

The example below is a three-qubit bit-flip memory experiment: each round measures the data qubits and pairs them with the previous round via detectors, with a final logical_observable reading out the register. In-kernel apply_noise seeds the error mechanisms. Each call applies a single-qubit bit-flip channel (cudaq::x_error in C++, cudaq.XError in Python) that applies a Pauli X with the given probability, so a flipped data qubit shows up as a parity change in the next detectors pair. See the C++ and Python API references for apply_noise and the other predefined noise channels.

# A 3-qubit bit-flip memory experiment. Each round measures the data qubits;
# cross-round detectors pair each measurement with its value in the previous
# round, and a final logical observable reads out the register. In-kernel
# `apply_noise` seeds the error mechanisms the detector error model reports.
@cudaq.kernel
def memory_experiment(rounds: int):
    data = cudaq.qvector(3)
    prev = mz(data)

    for r in range(rounds):
        cudaq.apply_noise(cudaq.XError, 0.01, data[0])
        cudaq.apply_noise(cudaq.XError, 0.01, data[1])
        cudaq.apply_noise(cudaq.XError, 0.01, data[2])

        curr = mz(data)
        # One detector per qubit, pairing this round with the previous one.
        cudaq.detectors(prev, curr)
        prev = curr

    cudaq.logical_observable(prev[0], prev[1], prev[2])


// A 3-qubit bit-flip memory experiment. Each round measures the data qubits;
// cross-round detectors pair each measurement with its value in the previous
// round, and a final logical observable reads out the register. In-kernel
// `apply_noise` seeds the error mechanisms the detector error model reports.
__qpu__ void memory_experiment(int rounds) {
  cudaq::qvector data(3);
  auto prev = mz(data);

  for (int r = 0; r < rounds; ++r) {
    cudaq::apply_noise<cudaq::x_error>(0.01, data[0]);
    cudaq::apply_noise<cudaq::x_error>(0.01, data[1]);
    cudaq::apply_noise<cudaq::x_error>(0.01, data[2]);

    auto curr = mz(data);
    // One detector per qubit, pairing this round with the previous one.
    cudaq::detectors(prev, curr);
    prev = curr;
  }
  cudaq::logical_observable(prev[0], prev[1], prev[2]);
}

Pass the kernel (and a noise model) to dem_from_kernel to extract the DEM.

# Generate the detector error model as Stim `.dem` text. A noise model must be
# supplied for the in-kernel `apply_noise` mechanisms to take effect. Parse the
# text with `stim.DetectorErrorModel(dem)` to drive a decoder.
noise = cudaq.NoiseModel()
dem = cudaq.dem_from_kernel(memory_experiment, 2, noise_model=noise)
print(dem)
  // Generate the detector error model as Stim `.dem` text. A noise model must
  // be supplied for the in-kernel `apply_noise` mechanisms to take effect.
  // Parse the text with Stim (`stim::DetectorErrorModel{dem}`) to drive a
  // decoder.
  cudaq::noise_model noise;
  std::string dem =
      cudaq::dem_from_kernel(memory_experiment, &noise, /*rounds=*/2);
  std::printf("%s\n", dem.c_str());

The .dem text is a list of independent error mechanisms. Each error(p) D... L... line gives one mechanism: its probability p, the detectors it flips (its symptoms, D), and the logical observables it flips (its frame changes, L).

Output DEM: With two rounds and three data qubits, there are six independent error mechanisms: one bit-flip per qubit per round at the in-kernel probability 0.01 (printed at full floating-point precision). Each error flips one detector together with the logical observable L0:

error(0.01000000000000000021) D0 L0
error(0.01000000000000000021) D1 L0
error(0.01000000000000000021) D2 L0
error(0.01000000000000000021) D3 L0
error(0.01000000000000000021) D4 L0
error(0.01000000000000000021) D5 L0

Limitations

  • Stabilizer (Clifford) circuits only. The DEM formalism requires detectors to be deterministic under noise-free execution, which is only well defined for Clifford circuits; a non-Clifford gate raises a diagnostic.

  • No measurement-conditional control flow. Branching on a measurement result changes the measurement count shot-to-shot and breaks the detector matrix model; such kernels are rejected.

  • Independent Pauli noise. Each error mechanism is assumed independent..

  • Pre-decomposition. The DEM reflects the abstract kernel circuit, not the hardware-decomposed circuit.