Add triangular color code plugin with superdense memory circuit#658
Add triangular color code plugin with superdense memory circuit#658kvmto wants to merge 12 commits into
Conversation
Codes may declare record->detector feedback corrections as data via two new virtuals (get_inlined_feedback / get_observable_inlined_feedback, empty default = existing behavior); memory_circuit folds the declared records into its detector and observable definitions, so measurement schemes with readout byproducts get deterministic detectors through sample_memory_circuit without a physical correction gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: kvmto <kmato@nvidia.com>
…d_feedback The record->detector composition rule now lives in detector_error_model as a CSR layout builder; the memory_circuit kernel is a thin emitter walking the layout. Behavior is bit-identical; all suites pass unchanged. Signed-off-by: kvmto <kmato@nvidia.com>
…amed device helpers The kernel keeps its flat-matrix signature; all record-composition logic lives in reusable __qpu__ functions in device/inlined_feedback.h. The host CSR layout builder (build_inlined_feedback_layout) is retained for record-stream consumers and pinned to the kernel by an M2D conformance test Signed-off-by: kvmto <kmato@nvidia.com>
Signed-off-by: kvmto <kmato@nvidia.com>
Validate feedback matrices before conversion, clarify raw sampling semantics, reject unsupported qpp-cpu Python sampling, and expand detector/observable conformance coverage. Signed-off-by: kvmto <kmato@nvidia.com>
…ry circuit Registers the triangular color code as the Python plugin 'color_code' (geometry, CSS stabilizers/observables, patch kernels) and adds a superdense memory circuit for it: the paired-ancilla 8-CNOT-layer schedule with the measurement feedforward absorbed into explicit cudaq.detector / cudaq.logical_observable annotations. DEM extraction and detector-ordered sampling via cudaq.dem_from_kernel / cudaq.sample; validated against a reference stim construction (identical error mechanisms, both bases). Signed-off-by: kvmto <kmato@nvidia.com>
Signed-off-by: kvmto <kmato@nvidia.com>
with the structure of the existing code plugins; the plugin is used through the standard qec.sample_memory_circuit / dem_from_memory_circuit API. The geometry retains the pre-decoder mapping surface (CNN grid embedding, syndrome-to-grid indices, superdense plaquette labels) for upcoming work. Signed-off-by: kvmto <kmato@nvidia.com>
The registered stabilizer_round now implements the reference superdense CX schedule (captured host-built layers) with the readout byproduct handled via the code-declared inlined-feedback matrices instead of an in-circuit correction. The framework-path DEM matches the reference construction mechanism-for-mechanism (golden tests, both bases, exact probabilities). Signed-off-by: kvmto <kmato@nvidia.com>
Signed-off-by: kvmto <kmato@nvidia.com>
eliotheinrich
left a comment
There was a problem hiding this comment.
Looks great, Kevin! I left some suggestions, the only thing I would consider blocking is the input validation on the distance and some hint for reproducing the golden values in the .dem files if possible.
| # Reference superdense color-code DEM, d=5, 5 rounds, DEPOLARIZE2(0.01) | ||
| # per CX, ALL ROUNDS NOISY (physically stricter convention). | ||
| # Generator: reference superdense construction (superdense_memory kernel, | ||
| # cudaq.dem_from_kernel). The perfect-final-round originals are | ||
| # byte-reproduced by the same generator with its two noise guards restored | ||
| # (R > 1 and r < R - 1); here both are relaxed to p_cx > 0.0 so every | ||
| # stabilizer round is noisy. |
There was a problem hiding this comment.
Would it be feasible to add a reproduction script for this DEM in the header comments here so that if anyone needs to rebuild this golden value down the road are able to? (ditto for the *_Z.dem)
| f"Unsupported w4 geometry for plaq_idx={plaq_idx}: distinct_rows={len(rows)}, distinct_cols={len(cols)}, coords={coords}" | ||
| ) | ||
|
|
||
| return out |
There was a problem hiding this comment.
This is unreachable; any reason for it to be here?
| raise RuntimeError( | ||
| "[color_code] distance not provided. distance must be " | ||
| "provided via qec.get_code('color_code', distance=d).") | ||
| self.distance = kwargs['distance'] |
There was a problem hiding this comment.
Should add some kind of input validation (i.e. make sure that distance is a positive integer)
| for tt in range(1, 9): | ||
| edges: List[Tuple[int, int]] = [] | ||
|
|
||
| if tt == 1 or tt == 8: | ||
| # CNOT between the two ancilla qubits: control |+> (a1), | ||
| # target |0> (a2) | ||
| index = num_data | ||
| for _ in range(num_plaquettes): | ||
| a1 = index | ||
| a2 = index + 1 | ||
| edges.append((a1, a2)) | ||
| index += 2 | ||
|
|
||
| elif tt == 2: | ||
| # First sequence (data = control) | ||
| data_qubit_index = d | ||
| index_ancilla = num_data | ||
| cols = d - 1 | ||
| for rr in range(num_rows_data_qubits - 1): | ||
| if rr % 3 == 0: | ||
| for _ in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 1 | ||
| elif rr % 3 == 1: | ||
| for cc in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| data_qubit_index += 1 | ||
| if cc == cols - 1: | ||
| index_ancilla += 3 | ||
| else: | ||
| index_ancilla += 1 | ||
| else: # rr % 3 == 2 | ||
| for _ in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 1 | ||
|
|
||
| elif tt == 3: | ||
| # Second sequence (data = control) | ||
| data_qubit_index = 0 | ||
| index_ancilla = num_data | ||
| cols = d - 1 | ||
| for rr in range(num_rows_data_qubits - 1): | ||
| if rr % 3 == 0: | ||
| for cc in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| index_ancilla += 1 | ||
| if cc == cols - 1: | ||
| data_qubit_index += 3 | ||
| else: | ||
| data_qubit_index += 1 | ||
| cols -= 1 | ||
| elif rr % 3 == 1: | ||
| for cc in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| data_qubit_index += 1 | ||
| if cc == cols - 1: | ||
| index_ancilla += 3 | ||
| else: | ||
| index_ancilla += 1 | ||
| else: # rr % 3 == 2 | ||
| for _ in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 1 | ||
|
|
||
| elif tt == 4: | ||
| # Third sequence (data = control) | ||
| data_qubit_index = 1 | ||
| index_ancilla = num_data + d - 1 | ||
| cols = d - 1 | ||
| for rr in range(num_rows_data_qubits - 2): | ||
| if rr % 3 == 0: | ||
| for _ in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| elif rr % 3 == 1: | ||
| for _ in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 2 | ||
| else: # rr % 3 == 2 | ||
| for cc in range(cols): | ||
| edges.append((data_qubit_index, index_ancilla)) | ||
| index_ancilla += 1 | ||
| if cc == cols - 1: | ||
| data_qubit_index += 3 | ||
| else: | ||
| data_qubit_index += 1 | ||
|
|
||
| elif tt == 5: | ||
| # First sequence (data = target) => ancilla is control | ||
| data_qubit_index = d | ||
| index_ancilla = num_data | ||
| cols = d - 1 | ||
| for rr in range(num_rows_data_qubits - 1): | ||
| if rr % 3 == 0: | ||
| for _ in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 1 | ||
| elif rr % 3 == 1: | ||
| for cc in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| data_qubit_index += 1 | ||
| if cc == cols - 1: | ||
| index_ancilla += 3 | ||
| else: | ||
| index_ancilla += 1 | ||
| else: # rr % 3 == 2 | ||
| for _ in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 1 | ||
|
|
||
| elif tt == 6: | ||
| # Second sequence (data = target) => ancilla is control | ||
| data_qubit_index = 0 | ||
| index_ancilla = num_data | ||
| cols = d - 1 | ||
| for rr in range(num_rows_data_qubits - 1): | ||
| if rr % 3 == 0: | ||
| for cc in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| index_ancilla += 1 | ||
| if cc == cols - 1: | ||
| data_qubit_index += 3 | ||
| else: | ||
| data_qubit_index += 1 | ||
| cols -= 1 | ||
| elif rr % 3 == 1: | ||
| for cc in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| data_qubit_index += 1 | ||
| if cc == cols - 1: | ||
| index_ancilla += 3 | ||
| else: | ||
| index_ancilla += 1 | ||
| else: # rr % 3 == 2 | ||
| for _ in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 1 | ||
|
|
||
| else: # tt == 7 | ||
| # Third sequence (data = target) => ancilla is control | ||
| data_qubit_index = 1 | ||
| index_ancilla = num_data + d - 1 | ||
| cols = d - 1 | ||
| for rr in range(num_rows_data_qubits - 2): | ||
| if rr % 3 == 0: | ||
| for _ in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| elif rr % 3 == 1: | ||
| for _ in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| data_qubit_index += 1 | ||
| index_ancilla += 1 | ||
| cols -= 2 | ||
| else: # rr % 3 == 2 | ||
| for cc in range(cols): | ||
| edges.append((index_ancilla, data_qubit_index)) | ||
| index_ancilla += 1 | ||
| if cc == cols - 1: | ||
| data_qubit_index += 3 | ||
| else: | ||
| data_qubit_index += 1 | ||
|
|
||
| layers.append(sorted(edges)) |
There was a problem hiding this comment.
Two comments:
1.) Nesting conditionals inside a for-loop doesn't make much sense since this is just a sequence; just push each conditional up one layer of scope
2.) The final three layers are a mirror image of the three layers before them under exchange of target and control qubits. Can make this explicit in the code
| (qec.operation.prepp, 'x', 'get_observables_x', 0), | ||
| (qec.operation.prepm, 'x', 'get_observables_x', 1), | ||
| ]) | ||
| @pytest.mark.parametrize("nRounds", [2, 4]) |
There was a problem hiding this comment.
nRounds == 1 is a bit of an edge case since no inter-round detectors are formed. Easy and IMO worthwhile to add to the parametrized test here
| # x_stabilizers / z_stabilizers arrive from the framework but are | ||
| # unused: the captured schedule alone drives the gate sequence. |
There was a problem hiding this comment.
It might be reasonable to add some checks that the *_stabilizers passed correspond correctly to the stabilizers of the code, even if they aren't used. If bad stabilizers are passed to _round, then something has gone wrong, and we should probably throw.
Signed-off-by: kvmto <kmato@nvidia.com>
|
Looks great! I would just recommend to test it (at least some spot tests) at |
Description
Adds the triangular color code to CUDA-QX as a Python QEC code plugin. The registered
stabilizer_roundimplements a superdense (paired-ancilla) syndrome-extraction schedule that measures both stabilizer bases of every plaquette in a single depth-8 CNOT pass; the record-conditioned readout byproduct inherent to this scheme is handled through the code-declared inlined-feedback mechanism of #665 rather than by an in-circuit correction gate. The plugin runs end-to-end through the standard framework API (sample_memory_circuit,dem_from_memory_circuit), and the resulting detector error model is validated mechanism-for-mechanism against an independently constructed reference DEM.Depends on #665 (this branch contains its commits; review the top commit here). The feedback-declaration API (
get_inlined_feedback,get_observable_inlined_feedback_z/_x) is defined there.Design
ColorCodeGeometry— triangular color code with a rectangular-grid embedding (data-qubit grid, algorithmic plaquette generation, syndrome-to-grid mapping, superdense plaquette labeling), ported from an internal reference implementation. Retains the pre-decoder mapping surface (CNN grid embedding, syndrome-to-grid indices) for upcoming decoder work.z_side_data,superdense_schedule) — host-side construction of the per-round CNOT schedule from the geometry: 8 layers over a unified index space, with the ancilla-pair partition of each plaquette's support determined by plaquette weight and color.stabilizer_round, and the boundary-plaquette partition is color-dependent, i.e. not derivable from parity rows alone. The plugin therefore registers a@cudaq.kerneldefined in a factory that captures the host-built schedule; the fixed framework signature is unchanged and the captured lists survive the sub-kernel dispatch from the C++memory_circuit(multiple instances remain independent). Records are returned in[Z][X]order.F(S) = {a : |z_side(a) ∩ S| odd}: the detector matrix hasfb(j,k) = 1iffj,k < Pand plaquettek ∈ F(supp(plaquette j)); the Z-basis observable row is the indicator ofF(logical-operator support); the X basis declares none. Diagonal entries exploit the XOR multiset semantics ofcudaq::detector(a duplicated record cancels), which is what allows a detector to exclude its own previous-round record.__init__asserts the minimum data-qubit index is strictly increasing across grid-ordered plaquettes, which guarantees the framework's stabilizer re-sorting is the identity permutation (record k ↔ plaquette k).Validation
prep0/1/p/m) are noiseless-deterministic with correct logical readout for d=3 at 2 and 4 rounds; DEM extraction succeeds in both bases under two-qubit depolarizing noise; a decode smoke test shows strict logical-error reduction.Notes for reviewers
Follow-ups (out of scope here)
🤖 Generated with Claude Code