find_detectors#
- find_detectors(func=None, *, return_circuits=False)[source]#
Decorator that automatically identifies stim detectors and returns them.
find_detectorsleverages tqecd to automatically discover detector parity checks in quantum error correction circuits. The detected parities are returned as an additional value alongside the decorated function’s original return values.Note
This feature requires the optional
tqecdpackage. Install it as described here.Pitfalls & Limitations#
The
find_detectorsfeature comes with some constraints:- QuantumArray arguments must be QuantumBool
All
QuantumArrayarguments to the decorated function must haveqtype=QuantumBool(). Other quantum types will raise aTypeError. Static parameters (integers, booleans, etc.) are allowed and will be passed through to the function unchanged.- Only Clifford gates are supported
tqecd analyses stabilizer flows, which are only well-defined for Clifford circuits. If the decorated function contains non-Clifford gates (e.g. T, Toffoli, arbitrary-angle rotations), stim will raise an error during circuit construction or tqecd will silently produce incorrect results.
- No real-time classical computation inside the decorated function
The circuit is traced symbolically. Conditional logic that depends on measurement outcomes (real-time
if/else) cannot be expressed in the stim circuit and will break analysis.- Composite detectors from flow products are not discoverable
tqecd finds detectors by tracking individual stabilizer flows from resets through gates to measurements. It cannot discover composite detectors that arise as products of multiple stabilizer flows. For example, in a GHZ-state circuit the parity \(Z_3 Z_4\) is a product of \(Z_0 Z_3\) and \(Z_0 Z_4\), but tqecd will not identify it.
- Multi-round circuits require explicit resets
For tqecd to detect round-to-round detectors (comparing syndrome measurements across rounds), ancilla qubits must be explicitly reset between rounds. Without resets, tqecd cannot establish the start of a new stabilizer flow and will miss cross-round detectors.
This is critical: in a two-round repetition code without resets between rounds, tqecd will only find first-round detectors and miss the temporal detectors that compare round 1 vs round 2.
- Detectors referencing non-returned measurements are discarded
If a detector involves a measurement whose result is not part of the decorated function’s return value, the detector is silently dropped. Make sure all relevant measurements are returned.
For example, if you measure both syndrome qubits but only return one measurement, detectors involving the unreturned measurement will be filtered out.
- Multiple QuantumArray arguments receive disjoint qubit slices
When the decorated function takes more than one QuantumArray argument, it is assumed that each argument indeed represents a distinct set of qubits. This can not be guaranteed at tracing time and needs to be ensured from user side. Each array is mapped to its own contiguous slice of qubits during analysis. The slices are allocated in positional order.
- May discover more detectors than expected
In some circuits,
find_detectorsmay identify more valid detectors than a minimal set. For example, in 2-round codes, tqecd can discover boundary detectors from data qubit measurements that are mathematically valid but redundant with syndrome-based detectors. All discovered detectors are correct (they will sample to zero in noiseless circuits); the extra ones represent additional stabilizer flows through the circuit.
- Parameters:
- funccallable, optional
The function to decorate. When using keyword arguments (e.g.
@find_detectors(return_circuits=True)), func isNoneand the decorator returns a wrapper that accepts func.The decorated function must accept at least one
QuantumArrayargument (withqtype=QuantumBool()). Additional non-QuantumArray arguments (integers, booleans, strings, etc.) are treated as static parameters: they are captured by closure before JAX tracing so they behave as compile-time constants inside the function body.- return_circuitsbool, default
False When
True, three additional items are appended to the return value (after the detector list and the original returns):raw_stim — the stim circuit obtained directly from
to_qcto_stim, before any restructuring.tqecd_input — the circuit after
_prepare_for_tqecdhas re-arranged it into proper fragments (the input fed to tqecd).annotated — the circuit that comes back from
tqecd.annotate_detectors_automatically, containing theDETECTORinstructions.
This is useful for debugging or understanding why detectors were or were not discovered.
- Returns:
- The decorated function returns:
(detector_bools, *original_returns)
- or, when
return_circuits=True: (detector_bools, *original_returns, raw_stim, tqecd_input, annotated) where detector_bools is a
listof Jasp-traced boolean values, one per discovered detector, each representing theparity()of the measurements that constitute that detector. The original return values from the decorated function are unpacked and appended after the detector list.
Examples
Example 1: Single-round syndrome extraction
Three data qubits with two ancilla parity checks:
from qrisp import * from qrisp.jasp.evaluation_tools.stim_extraction import extract_stim from qrisp.misc.stim_tools import find_detectors @find_detectors def syndrome(data, ancilla): # Reset ancilla qubits reset(ancilla[0]) reset(ancilla[1]) # Syndrome extraction cx(data[0], ancilla[0]) cx(data[1], ancilla[0]) cx(data[1], ancilla[1]) cx(data[2], ancilla[1]) # Measure syndromes return measure(ancilla[0]), measure(ancilla[1]) @extract_stim def main(): data = QuantumArray(qtype=QuantumBool(), shape=(3,)) ancilla = QuantumArray(qtype=QuantumBool(), shape=(2,)) detectors, m0, m1 = syndrome(data, ancilla) return detectors, m0, m1 # Run and extract stim circuit result = main() stim_circ = result[-1] print(f"Found {stim_circ.num_detectors} detectors")
Example 2: Two-round repetition code
Distance-3 repetition code with temporal detectors comparing rounds:
@find_detectors def rep_code_2rounds(data, ancilla): measurements = [] for round_num in range(2): # Reset ancillas reset(ancilla[0]) reset(ancilla[1]) # Parity checks cx(data[0], ancilla[0]) cx(data[1], ancilla[0]) cx(data[1], ancilla[1]) cx(data[2], ancilla[1]) # Measure syndromes measurements.append(measure(ancilla[0])) measurements.append(measure(ancilla[1])) # Final data readout for i in range(3): measurements.append(measure(data[i])) return tuple(measurements) @extract_stim def main(): data = QuantumArray(qtype=QuantumBool(), shape=(3,)) ancilla = QuantumArray(qtype=QuantumBool(), shape=(2,)) detectors, *measurements = rep_code_2rounds(data, ancilla) return (detectors,) + tuple(measurements) result = main() stim_circ = result[-1] # Will find >= 6 detectors (may discover more valid boundary detectors): # - 2 detectors in round 1 (vs initial reset) # - 2 detectors in round 2 (vs round 1) # - 2+ data-boundary detectors (tqecd may find additional valid ones) print(f"Found {stim_circ.num_detectors} detectors")
Example 3: Debug mode with circuit inspection
Use
return_circuits=Trueto inspect intermediate circuits:@find_detectors(return_circuits=True) def debug_syndrome(data, ancilla): reset(ancilla[0]) cx(data[0], ancilla[0]) cx(data[1], ancilla[0]) return measure(ancilla[0]) @extract_stim def main(): data = QuantumArray(qtype=QuantumBool(), shape=(2,)) ancilla = QuantumArray(qtype=QuantumBool(), shape=(1,)) detectors, m, raw_stim, tqecd_input, annotated = debug_syndrome(data, ancilla) print("Raw stim circuit:") print(raw_stim) print("\nAfter tqecd restructuring:") print(tqecd_input) print("\nWith detectors annotated:") print(annotated) return detectors, m main()
Example 4: Multi-array arguments
Separate data and ancilla qubits:
@find_detectors def surface_check(data_qubits, ancilla_qubits): # Reset ancilla reset(ancilla_qubits[0]) # X-stabilizer (Hadamard + CNOTs) h(ancilla_qubits[0]) cx(ancilla_qubits[0], data_qubits[0]) cx(ancilla_qubits[0], data_qubits[1]) cx(ancilla_qubits[0], data_qubits[2]) cx(ancilla_qubits[0], data_qubits[3]) h(ancilla_qubits[0]) return measure(ancilla_qubits[0]) @extract_stim def main(): data = QuantumArray(qtype=QuantumBool(), shape=(4,)) ancilla = QuantumArray(qtype=QuantumBool(), shape=(1,)) detectors, m = surface_check(data, ancilla) return detectors, m result = main() # Note: This finds 0 detectors because the X-stabilizer measurement # on Z-basis |0⟩ initialized qubits is not deterministic. # For detectors, would need X-basis initialization (RX) on data qubits.
Example 5: Three-round code with explicit resets
Shows importance of reset between rounds:
@find_detectors def three_rounds(data, ancilla): results = [] for _ in range(3): # CRITICAL: Reset between rounds reset(ancilla[0]) # Syndrome extraction cx(data[0], ancilla[0]) cx(data[1], ancilla[0]) results.append(measure(ancilla[0])) return tuple(results) @extract_stim def main(): data = QuantumArray(qtype=QuantumBool(), shape=(2,)) ancilla = QuantumArray(qtype=QuantumBool(), shape=(1,)) detectors, m1, m2, m3 = three_rounds(data, ancilla) return detectors, m1, m2, m3 result = main() stim_circ = result[-1] # Will find >= 3 detectors (one per round plus temporal) print(f"Found {stim_circ.num_detectors} detectors") # Verify all detectors are valid (noiseless samples = 0) sampler = stim_circ.compile_detector_sampler() samples = sampler.sample(shots=1000) assert samples.sum() == 0, "All detectors should be deterministic!"
Example 6: Partial measurement return
Only detectors involving returned measurements are kept:
@find_detectors def partial_return(data, ancilla): reset(ancilla[0]) reset(ancilla[1]) cx(data[0], ancilla[0]) cx(data[1], ancilla[0]) cx(data[1], ancilla[1]) cx(data[2], ancilla[1]) m0 = measure(ancilla[0]) m1 = measure(ancilla[1]) # Only return m0, discard m1 return m0 @extract_stim def main(): data = QuantumArray(qtype=QuantumBool(), shape=(3,)) ancilla = QuantumArray(qtype=QuantumBool(), shape=(2,)) detectors, m = partial_return(data, ancilla) return detectors, m result = main() stim_circ = result[-1] # Only detectors involving m0 (the returned measurement) are kept. # Detectors that depend on m1 (unreturned) are filtered out. # Typically finds 1 detector from the m0 measurement vs initial reset. print(f"Found {stim_circ.num_detectors} detectors")
Example 7: Static parameters (integers, booleans)
The decorated function can accept non-QuantumArray arguments such as integers or booleans. These are bound via closure before tracing so that JAX treats them as compile-time constants:
@find_detectors def configurable_syndrome(data, ancilla, num_rounds, use_extra_gate): results = [] for _ in range(num_rounds): reset(ancilla[0]) cx(data[0], ancilla[0]) cx(data[1], ancilla[0]) if use_extra_gate: cx(data[1], ancilla[0]) cx(data[1], ancilla[0]) # cancels out results.append(measure(ancilla[0])) return tuple(results) @extract_stim def main(): data = QuantumArray(qtype=QuantumBool(), shape=(2,)) ancilla = QuantumArray(qtype=QuantumBool(), shape=(1,)) detectors, *ms = configurable_syndrome(data, ancilla, 3, True) return (detectors,) + tuple(ms) result = main() stim_circ = result[-1] print(f"Found {stim_circ.num_detectors} detectors")