rdml-qpcr 0.1.1

Read, write, and validate RDML (Real-time PCR Data Markup Language) qPCR data files
Documentation
# 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

| RDML | status | read | write |
|---|---|---|---|
| 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.