# rdml-qpcr
Rust implementation of [RDML](https://rdml.org/) (Real-time PCR Data
Markup Language), the RDML consortium's XML interchange format for qPCR
and digital PCR data.
- Complete model of RDML 1.3 (REC) and 1.4 (CR): every element,
attribute, and enumeration, including melting curves, digital-PCR
partitions with their TSV sidecar tables, all protocol step kinds,
commercial assays, and oligo sequences.
- Reads all published versions (1.0–1.4) into one model; writes 1.1–1.4.
Version migrations are explicit and reported, in both directions.
- Reads `.rdml`/`.rdm` zip containers and bare `.xml` (detected by
content). Unknown archive members — vendor sidecar files — are
preserved byte-for-byte on round-trip.
- `validate()` checks the schema's identity constraints (reference
resolution, id and per-element uniqueness), which XSD tooling does not,
and reports all findings with document paths. Writing validates first.
- The whole model derives serde `Serialize`/`Deserialize` with clean
field names, for conversion to JSON, CBOR, etc.
- The format's out-of-schema rules are encoded in the types: the `-1.0`
"not available" sentinel maps to `Option`, `excl`/`note` presence
semantics make `<excl>false</excl>` unrepresentable, and `;`-joined
reason lists are a dedicated type.
This crate stores results computed elsewhere; it does no Cq calling,
baselining, or curve fitting.
## Writing a file
```rust,no_run
use rdml_qpcr::*;
use std::num::NonZeroU32;
fn main() -> rdml_qpcr::Result<()> {
let mut doc = Rdml::new();
doc.dyes.push(Dye::new(Id::new("SYBR")?));
doc.samples.push(Sample::new(Id::new("liver 1")?));
doc.targets.push(Target::new(
Id::new("GAPDH")?,
TargetType::Reference,
DyeRef::new("SYBR")?,
));
let mut run = Run::new(Id::new("plate 1")?, PcrFormat::plate96());
run.run_date = Some(DateTime::new("2026-08-14T09:30:00Z")?);
let mut react = React::new(NonZeroU32::new(1).unwrap(), SampleRef::new("liver 1")?);
let mut data = Data::new(TargetRef::new("GAPDH")?);
data.cq = Some(21.4); // None = not available; -1.0 is never written
data.adps = (1..=40)
.map(|cyc| AmpPoint::new(cyc as f64, 0.02 * cyc as f64))
.collect();
react.data.push(data);
run.reacts.push(react);
let mut experiment = Experiment::new(Id::new("pilot")?);
experiment.runs.push(run);
doc.experiments.push(experiment);
// Validates first; fails on dangling references or duplicate ids.
let report = RdmlFile::new(doc).save("pilot.rdml")?;
assert!(report.is_lossless());
Ok(())
}
```
## Reading a file
```rust,no_run
fn main() -> rdml_qpcr::Result<()> {
// Any version 1.0–1.4, zip container or bare XML.
let file = rdml_qpcr::RdmlFile::open("pilot.rdml")?;
for note in file.read_notes() {
eprintln!("note: {note}"); // legacy migrations, skipped unknowns
}
for ctx in file.document.data_entries() {
let well = ctx.run.pcr_format.well_name(ctx.react.id);
let sample = &ctx.react.sample;
match ctx.data.cq {
Some(cq) => println!("{well} {sample} {} Cq {cq:.2}", ctx.data.tar),
None => println!("{well} {sample} {} —", ctx.data.tar),
}
}
Ok(())
}
```
## Versions
| 1.0 | REC | yes (migrated, with notes) | no |
| 1.1 | REC | yes | yes |
| 1.2 | REC | yes | yes |
| 1.3 | REC | yes | yes (default) |
| 1.4 | CR (may still change) | yes | yes |
Writing an older version than the document uses returns a `WriteReport`
listing every element the target version cannot express; nothing is
dropped silently. RDML 1.0 is read-only: its enumerated plate formats and
well-label reaction ids are a different document shape, which reading
migrates (positions mapped from labels, dye records synthesized from
free-text names) with a note for each non-trivial step.
## Cargo features
- `time` (off by default): conversions between `rdml_qpcr::DateTime` and
the [`time`](https://docs.rs/time) crate's types.
Serde support is not feature-gated.
MSRV: Rust 1.88.
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE)
or [MIT license](LICENSE-MIT) at your option.
The vendored RDML schema files under `schema/` are © the RDML
consortium, MIT-licensed (see `schema/LICENSE-RDML-consortium`).
Conformance test files under `tests/corpus/` come from the consortium's
[RDMLpython](https://github.com/RDML-consortium/RDMLpython) and the
[rdml R package](https://github.com/ramiromagno/rdml), both MIT; see
`tests/corpus/README.md` for provenance.
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms
or conditions.