A concatenated soft-decision error-correcting codec for DNA data storage, in Rust.
Try it in your browser → — drop an image, watch it become a few thousand strands of DNA, damage it, and get it back. Runs entirely client-side; nothing is uploaded.
Status: the codec works end to end and is byte-exact through a simulated channel. The measurements that would justify comparing it to published work do not exist yet. See Milestones for exactly what is and is not done.
An implementation of the architecture that currently performs best for storing data in synthesised DNA: an outer maximum-distance-separable erasure code over GF(2^16) interleaved across strands, a per-strand CRC that converts errors into erasures, and an inner binary LDPC code decoded from soft information rather than from hard base calls.
write: bytes -> outer MDS -> frame + CRC-32C -> LDPC -> scramble -> 2 bit/nt
-> screen -> address -> strand
read: reads -> length gate -> address -> bucket -> per-base evidence as LLRs
-> descramble -> min-sum BP -> OSD -> CRC gate -> outer MDS
- No wet lab. This is a codec plus a channel-simulator harness. Every number that will eventually appear here is in silico.
- Indels are not corrected, and the length gate does not catch them. This was the design's assumption and measuring it on a real channel showed it to be wrong. Sequencing reads a fixed number of cycles regardless of molecule length, so a deletion does not shorten the read — it shifts everything after it and lets one extra base in at the end. Every DT4DDS decode reported zero wrong-length reads. Indels therefore arrive as correct-length reads that are confidently wrong from the deletion onwards, and the codec has no defence against them beyond the outer code's erasure budget. At 6% frameshifted reads that holds; at 72%, which is what electrochemical synthesis produces, nothing does. See docs/DT4DDS.md.
- Read grouping is address-based, not blind clustering. Every strand carries a 16-nucleotide barcode, and reads are bucketed by decoding it. Clustering noisy reads without such an anchor is the genuinely hard open problem in this field (Cao et al., Cell Reports 43:113699, 2024); it is out of scope here, and comparisons against codecs that do it blind are not like-for-like.
- Not a density record. Storage density in this field is reported as exabytes per
gram, and that figure is
code_rate / physical_redundancy × 113.7— dominated by how few physical copies you need, not by how many bits you pack per base. Measured on the DT4DDS high-fidelity channel this codec reaches 38 EB/g at three physical copies per sequence: level with HEDGES, well behind DNA-RS and DNA-Aeon at around 120, and it does not decode the low-fidelity scenario at all.
Grouped by what was actually done with them, because a flat bibliography would imply this codec follows all of them, and it follows some and deliberately contradicts others.
-
Cao, B. et al. (2024). Efficient data reconstruction: the bottleneck of large-scale application of DNA storage. Cell Reports 43(4):113699. doi:10.1016/j.celrep.2024.113699 · PubMed — The paper that shaped the architecture most. Its argument is that the bottleneck is reconstruction — clustering and consensus of noisy reads — rather than synthesis or sequencing. This codec responds by writing an address into every strand and grouping reads by decoding it. Worth being clear that this sidesteps the problem rather than solving it, at a cost of 10% of each strand.
-
Banal, J. L. (2026). DNA storage approaching the information-theoretic ceiling. arXiv:2604.20810 — Prior art for the idea of never collapsing reads to a consensus: its abstract describes "log-product fusion across reads" feeding ordered-statistics decoding. That idea is theirs and is cited as theirs. Its reference implementation is licensed PolyForm Noncommercial; no code from it is used here. Note that its framing — retaining "the sequencer's per-position posterior distributions" — is what this project ended up questioning, since on the channel it evaluates against the sequencer emits nothing.
-
Fossorier, M. P. C. & Lin, S. (1995). Soft-decision decoding of linear block codes based on ordered statistics. IEEE Trans. Inf. Theory 41(5):1379–1396. doi:10.1109/18.412683 —
ldpc/osd.rs. Implemented on the parity-check matrix rather than the generator, which is about thirty times less work at these parameters. -
Chen, J., Dholakia, A., Eleftheriou, E., Fossorier, M. P. C. & Hu, X.-Y. (2005). Reduced-complexity decoding of LDPC codes. IEEE Trans. Commun. 53(8):1288–1299. — Normalised min-sum,
ldpc/bp.rs. The scaling factor defaults to 0.75 rather than the frequently quoted 0.8, because that figure is for degree-6 checks and this code has degree 18. Seedocs/ERROR_BUDGET.mdfor what a sweep says about it. -
Hu, X.-Y., Eleftheriou, E. & Arnold, D. M. (2005). Regular and irregular progressive edge-growth Tanner graphs. IEEE Trans. Inf. Theory 51(1):386–398. doi:10.1109/TIT.2004.839541 — Cited as the method this project tried and abandoned. At mean check degree 18 the breadth-first tree from any variable node reaches every check at depth two, so the distance criterion is vacuous and progressive edge growth degenerates into lowest-degree greedy. Measured: it never once placed all 288 columns.
ldpc/construct.rsexplains what replaced it.
-
Gimpel, A. L., Stark, W. J., Heckel, R. & Grass, R. N. (2023). A digital twin for DNA data storage based on comprehensive quantification of errors and biases. Nat Commun 14:6026. doi:10.1038/s41467-023-41729-1 — DT4DDS, the channel every published figure here comes from. Its substitution matrices are also what the nucleotide-to-bit mapping was chosen against.
-
Gimpel, A. L., Remschak, A., Stark, W. J., Heckel, R. & Grass, R. N. (2026). Comparison of state-of-the-art error-correction coding for sequence-based DNA data storage. Nat Commun. doi:10.1038/s41467-026-70548-3 — The benchmark this codec is placed against in
docs/DT4DDS.md, and the source of every competitor figure quoted there. Also the source of the useful warning that synthetic error benchmarks are poor predictors of codec performance.
- Grass, R. N., Heckel, R., Puddu, M., Paunescu, D. & Stark, W. J. (2015). Robust chemical preservation of digital information on DNA in silica with error-correcting codes. Angew. Chem. Int. Ed. 54:2552–2555. doi:10.1002/anie.201411378
- Erlich, Y. & Zielinski, D. (2017). DNA Fountain enables a robust and efficient
storage architecture. Science 355:950–954.
doi:10.1126/science.aaj2038
— Fountain coding for DNA storage is encumbered (Erlich/Columbia). This project uses a
Cauchy MDS outer code partly for that reason and says so in
outer.rs. - Press, W. H., Hawkins, J. A., Jones, S. K., Schaub, J. M. & Finkelstein, I. J. (2020). HEDGES error-correcting code for DNA storage corrects indels and allows sequence constraints. PNAS 117(31):18489–18496. doi:10.1073/pnas.2004821117 — The codec this one lands level with on density and loses to on capability, since HEDGES corrects insertions and deletions and this does not.
- Ping, Z. et al. (2022). Towards practical and robust DNA-based data archiving using the yin–yang codec system. Nat. Comput. Sci. 2:234–242. doi:10.1038/s43588-022-00231-2
- Welzel, M. et al. (2023). DNA-Aeon provides flexible arithmetic coding for constraint adherence and error correction in DNA storage. Nat Commun 14:628. doi:10.1038/s41467-023-36297-3
-
Goldman, N. et al. (2013). Towards practical, high-capacity, low-maintenance information storage in synthesized DNA. Nature 494:77–80. doi:10.1038/nature11875 — Solves the same three problems this codec solves: homopolymer avoidance, redundancy, indexing. Its answers are base-3 coding, fourfold fragment overlap and an explicit index; this codec uses scrambling with retries, an MDS outer code, and an address barcode. The reason for the divergence is measurable rather than aesthetic — in the 2026 benchmark the Goldman scheme reaches 0.34 bit/nt and does not decode the low-fidelity scenario at all.
-
Koch, J. et al. (2020). A DNA-of-things storage architecture to create materials with embedded memory. Nat Biotechnol 38:39–43. — About the medium rather than the code: DNA in silica nanoparticles embedded in printed objects. Nothing here touches it. The connection is that the same group at ETH Zurich produced DT4DDS, which everything above is measured against.
The field evaluates soft-decision DNA codecs on ETH's DT4DDS channel simulator. DT4DDS
writes FASTQ, and every FASTQ writer in it emits the quality string 'F' * len(seq) —
a constant Phred score of 37 for every base of every read, uncorrelated with the errors
it just injected, and numerically wrong even as an average (the simulator's own iSeq-100
substitution rates correspond to Q30 and Q26).
The consequence is worth stating plainly: on DT4DDS, soft decoding driven by quality scores is arithmetically identical to hard decoding. Any measured gain must be coming from somewhere else — an analytic channel model, or the multiplicity of reads — and the distinction matters, because only one of those transfers to a real sequencer.
Beyond that, the interesting question is not "is soft better than hard" (for a symmetric channel, summing per-read log-likelihood ratios and taking the sign is identically majority voting) but where the gain actually comes from. This codec is built to decompose it: asymmetry in the substitution matrix, survival of per-position reliability into the inner decoder, and the choice of nucleotide-to-bit mapping are separately measurable, against a baseline that collapses reads to a consensus but keeps a confidence value.
Prior art for the soft-fusion architecture itself is Banal, DNA storage approaching the information-theoretic ceiling (arXiv:2604.20810, 2026), which combines profile-HMM alignment, log-product fusion across reads, and ordered-statistics decoding. That idea is theirs and is cited as such. Its reference implementation is licensed PolyForm Noncommercial; no code from it is used here.
| Design sequence | 160 nt = 16 nt address + 144 nt codeword (primers off by default) |
| Inner code | LDPC, n = 288, m = 48, k = 240, column weight 3, rate 5/6 |
| Girth / minimum distance | 6 / at least 6 |
| Frame | 208 payload bits (13 GF(2^16) symbols) + 32-bit CRC-32C |
| Outer code | Cauchy MDS over GF(2^16), 13 symbols per strand, erasure-only |
| Density | 1.30 bit/nt at strand level, times the outer rate for the net figure |
Published DNA-storage densities are not comparable unless the accounting is stated, so:
1.30 bit/nt counts the 16-nt address against us and excludes PCR primers, matching
the operational definition used by the dt4dds-benchmark harness
(8 × input_bytes / total_nucleotides_in_the_design_file). With 20 nt primers at each
end the same code is 1.02 bit/nt. Net density is the strand figure times the outer code
rate; at the two standardised rate classes the benchmark uses, this code reaches
1.00 bit/nt and 0.50 bit/nt. It cannot reach the third class, 1.50 bit/nt — that
would need an outer rate above 1.
A rate calculation alone suggests 44. It is not constructible. Girth 6 at column weight 3
requires all 288 × 3 = 864 check-pairs to be distinct; C(44,2) = 946 exist, a 91%
packing that greedy placement never reaches (measured: 0 of 12 seeds, best 278 of 288
columns placed). Requiring no weight-4 codewords on top of that makes 44 infeasible
outright. At 48 checks the same demand is 864 of 1128 — 77% — and it succeeds on every
seed tried.
Moving to 48 costs nothing in density: the four bits it takes come out of frame padding that was doing no work, so the payload stays at 208 bits. It buys a minimum distance of 6 instead of 4, and a byte-aligned CRC input.
| M0 | Workspace, lints, CI | done |
| M1 | GF(2) linear algebra, parity-check construction | done |
| M2 | Systematic form, encoder | done |
| M3 | Belief propagation, verified against exact marginals | done |
| M4 | Exhaustive maximum-likelihood oracle on a small code | done |
| M5 | Ordered-statistics decoding | done |
| M6 | Channel simulator | done — sweeps and the waterfall are not |
| M7 | Framing, CRC, scramble, mapping, screening, address | done |
| M8 | Outer Cauchy MDS code | done |
| M9 | Command line, end-to-end round trip | done |
| M10 | DT4DDS channel integration | done |
| M11 | Error budget | generated; criterion benchmarks not started |
| — | Browser demo | done, see web/ |
119 tests pass. clippy --all-targets -- -D warnings is clean, cargo doc is
warning-free, and dnacodec-core compiles for thumbv7em-none-eabihf — encoder and
decoder — with an empty dependency table.
Numbers from the built-in channel, printed by cargo test --test roundtrip -- --nocapture:
- 1.300 bit/nt at strand level and 1.000 bit/nt net at 30% outer redundancy, computed from the emitted strands rather than asserted. That net figure is one of the three rate classes the reference benchmark standardises on.
- Keeping the evidence soft recovered more strands than voting first on 6 of 6 seeds (724 against 689, at 2% substitution and coverage 3).
- At a 0.4% combined insertion and deletion rate the length gate discards 45% of reads. That is the whole of this codec's indel handling, and the number is reported rather than buried: it means any error tolerance quoted against the injected rate is optimistic, because the discarded reads are the noisy ones.
None of these are results. The channel that produced them draws read errors independently, whereas real synthesis damages molecules and every read of a damaged molecule inherits that damage — which is precisely the independence that summing log-likelihood ratios assumes. The simulator is therefore biased in favour of the thing the codec is trying to demonstrate, and says so in its own module documentation. Real figures need DT4DDS, which is M10.
cargo build --release
# what the code actually is, computed rather than quoted
./target/release/dnacodec inspect
# store a file
./target/release/dnacodec encode -i file.bin -o sequences.txt -m file.json
# recover it from reads (one sequence per line, any order, duplicates welcome)
./target/release/dnacodec decode -i reads.txt -m file.json -o recovered.bin
# measure what the design asserts, and regenerate docs/ERROR_BUDGET.md
./target/release/dnacodec sweep --scale 12 --write-docsdecode refuses to write anything whose SHA-256 does not match the manifest. The
per-strand checksum sorts strands into trustworthy and not; it is not entitled to
pronounce on the file, because ordered-statistics decoding weighs tens of thousands of
candidate codewords against a 32-bit check and roughly one strand in 140 000 passes
wrongly — and a wrongly passing strand corrupts its whole interleave block.
Every subcommand takes --json.
Full detail and reproduction commands in docs/DT4DDS.md. A 4 kB file at
0.995 bit/nt net, sequencing depth 15, five seeds per point, dt4dds 1.1.0:
| physical redundancy | decoded | storage density |
|---|---|---|
| 4 | 5 of 5 | 28 EB/g |
| 3 | 5 of 5 | 38 EB/g |
| 2 | 4 of 5 | 57 EB/g |
| 1 | 0 of 5 | — |
Low fidelity — electrochemical synthesis with an error-prone polymerase — fails at every redundancy tried, up to twenty copies per sequence. 72% of its reads carry a frameshift and this codec does not correct deletions. That is a capability gap, not a tuning one.
The comparison to published figures is not yet like-for-like: different file size, five seeds instead of a fitted 95% threshold over ten, depth 15 instead of 30, and no clustering step. All four caveats are spelled out in the document rather than left for a reader to discover.
docs/ERROR_BUDGET.md is generated, never hand-edited. Three findings worth pulling out,
including the two that came out against expectations:
The second decoding stage earns its place. At 1.2% channel error, belief propagation alone left 25.4% of strands unrecovered; ordered statistics at order 0, 1 and 2 brought that to 20.9%, 18.1% and 17.6%. Order 0 does no searching at all — believe the reliable positions, re-derive the rest — and it alone saves 459 strands per 10 000.
The normalisation factor is not where the literature puts it, and the default was left alone anyway. 0.75 measures 0.2557; the best value measured is 0.85 at 0.2514. But the curve is not smooth and reproducibly so — 0.80 is worse than both its neighbours, and the effect survives a twelve-fold increase in trials with identical channel realisations across every value. Two minima that close together with a bump between them is not a curve whose argument-minimum means much, so chasing it would be fitting to the seed. What the measurement does establish is that anything from 0.65 to 0.85 lands within a third of a percentage point, while 1.00 — plain min-sum, no correction — is nearly twice as bad. The correction matters a great deal and its exact value does not. The bump is unexplained.
The minimum-distance claim could not be confirmed by simulation, and the document says so. The constructor forbids weight-4 codewords and reports zero of them, which for a column-weight-3 girth-6 code means a minimum distance of at least 6. Building the same code with that constraint off admits 34 of them, so the two should differ in error-floor slope. Measured slopes: 2.75 against 2.65 — a gap of 0.10, which is not evidence of anything. That is the expected outcome: minimum distance governs the floor, and these points are in the waterfall. The claim rests on the constructor's exhaustive count, which is exact; the simulation neither confirms nor refutes it, and reporting the 0.10 gap as confirmation would have been exactly the sort of quietly favourable accounting this project is meant to avoid.
cargo test # unit, gate and round-trip tests
cargo clippy --all-targets -- -D warnings
cargo fmt --all --check
cargo build -p dnacodec-core --no-default-features \
--target thumbv7em-none-eabihf # the "no dependencies" claim, enforcedThe toolchain is pinned to 1.85.0 — the minimum supported version — so that a dependency on anything newer fails immediately rather than in someone else's CI.
dnacodec-core has an empty [dependencies] table and builds without std. That is
enforced by a CI job which compiles it for thumbv7em-none-eabihf, not by discipline.
Note that #![forbid(unsafe_code)] is a crate-local lint: it means this project's own
source contains no unsafe, not that its (development-only) dependency tree does.
MIT OR Apache-2.0.