rdml-qpcr 0.1.1

Read, write, and validate RDML (Real-time PCR Data Markup Language) qPCR data files
Documentation
//! Reading, writing, and validation of RDML (Real-time PCR Data Markup
//! Language) files — the [RDML consortium]'s XML interchange format for
//! qPCR and digital PCR data.
//!
//! This crate stores and transports results computed elsewhere. It
//! performs no analysis: fields like `cq`, `ampEff`, and `bgFluor` are
//! carried verbatim from whatever instrument or software produced them.
//!
//! # Document structure
//!
//! An RDML document is normalised. The root ([`Rdml`]) holds flat lists
//! of master elements — [`Experimenter`], [`Documentation`], [`Dye`],
//! [`Sample`], [`Target`], [`ThermalCyclingConditions`], and
//! [`Experiment`] — each identified by a unique [`Id`]. Other elements
//! refer to them by id rather than nesting. References are typed
//! ([`SampleRef`], [`TargetRef`], [`DyeRef`], …) so a reference cannot be
//! confused with a definition or with a reference of another kind.
//!
//! Measurements sit in one containment chain:
//!
//! ```text
//! rdml ─ experiment ─ run ─ react ─ data ─┬─ adp   (amplification points, per cycle)
//!                                         └─ mdp   (melting points, per temperature)
//! ```
//!
//! A [`React`] is one physical reaction (well, capillary, or
//! through-hole), identified by an integer position (row-first /
//! column-second on plates) and referencing the [`Sample`] it contains.
//! Each [`Data`] entry holds one target's results; multiplex reactions
//! have several. Digital PCR stores partition counts ([`Partitions`]) in
//! the document and optional per-partition values in TSV tables
//! ([`PartitionTable`]) inside the archive.
//!
//! Ids are short human-readable names chosen by the user; qPCR software
//! displays them, and the format notes tell producers not to use
//! generated strings. This crate provides no id generator.
//!
//! # Reading
//!
//! [`RdmlFile::open`] reads all format versions (1.0–1.4) and both
//! physical forms — the `.rdml`/`.rdm` zip container and bare
//! uncompressed XML — detected by content:
//!
//! ```no_run
//! let file = rdml_qpcr::RdmlFile::open("run42.rdml")?;
//! for ctx in file.document.data_entries() {
//!     let well = ctx.run.pcr_format.well_name(ctx.react.id);
//!     match ctx.data.cq {
//!         Some(cq) => println!("{well} {}: Cq {cq:.2}", ctx.data.tar),
//!         None => println!("{well} {}: no Cq", ctx.data.tar),
//!     }
//! }
//! # Ok::<(), rdml_qpcr::Error>(())
//! ```
//!
//! Old versions are migrated to the current model on read. Mappings that
//! are not plain renames are reported in [`RdmlFile::read_notes`];
//! nothing is dropped silently. The migration rules are documented in
//! [`version`].
//!
//! # Writing
//!
//! ```
//! use rdml_qpcr::*;
//! use std::num::NonZeroU32;
//!
//! 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());
//! 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);
//! 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);
//!
//! // Zip container, RDML 1.3 (the latest recommendation):
//! let file = RdmlFile::new(doc);
//! let (bytes, report) = file.to_bytes(RdmlVersion::V1_3)?;
//! assert!(report.is_lossless());
//!
//! // Or bare XML of any writable version:
//! let (xml, _) = file.document.to_xml(RdmlVersion::V1_4)?;
//! assert!(xml.contains("<cq>21.4</cq>"));
//! # Ok::<(), rdml_qpcr::Error>(())
//! ```
//!
//! Writing validates first and fails on broken references or duplicate
//! ids; the `*_unchecked` variants skip that check. Every write returns
//! a [`WriteReport`] listing anything the target version could not
//! express. Archive members this crate does not understand (vendor
//! sidecar files) are preserved byte-for-byte across read–modify–write;
//! [`RdmlFile::insert_member`] adds new ones.
//!
//! # Format rules outside the schema
//!
//! Several normative RDML rules appear in the consortium's format notes
//! rather than the XSD. They are encoded in the types and documented on
//! the affected items:
//!
//! - `-1.0` means "not available" on [`Data::cq`], [`Data::n0`],
//!   [`Data::corr_p`], [`Data::corr_cq`], and [`Data::n_copy`], and only
//!   there. The fields are `Option<f64>`; the sentinel reads as `None`
//!   and is never written.
//! - `excl` and `note` signal by presence. They are
//!   `Option<`[`Reasons`]`>`: `None` writes nothing, so
//!   `<excl>false</excl>` cannot be produced. [`Reasons`] handles the
//!   `;`-joined multi-reason convention.
//! - Amplification fluorescence must not be baseline-corrected; see
//!   [`AmpPoint::fluor`].
//! - `pcrFormat` display conventions (`rows = -1`, `columns = 1`); see
//!   [`PcrFormat`].
//! - Files are UTF-8 with `\n` newlines and `.` decimal separators. The
//!   reader accepts any namespace prefix and child order and skips
//!   unknown elements with a note; the writer emits schema order.
//!
//! # Versions
//!
//! RDML 1.0–1.3 are consortium recommendations; 1.4 is a candidate
//! recommendation and may still change. This crate reads all five
//! versions into one model (shaped like 1.4) and writes 1.1–1.4, 1.3 by
//! default. 1.0 is read-only. See [`version`] for the migration rules
//! and loss reporting.
//!
//! # Validation
//!
//! XSD validators do not check RDML's `xs:key`/`xs:keyref`/`xs:unique`
//! constraints. [`Rdml::validate`] does: reference resolution, id
//! uniqueness, and the per-element uniqueness rules (`run`/`react` ids,
//! `step` numbers, `adp` cycles, `mdp` temperatures). It returns all
//! findings with document paths, not just the first. See [`validate`].
//!
//! # Serde
//!
//! The model derives [`serde::Serialize`] and [`serde::Deserialize`]
//! with schema-vocabulary field names and no XML artifacts, so documents
//! can be converted to JSON, CBOR, or any other serde format.
//!
//! # Cargo features
//!
//! - `time` (off by default): conversions between [`DateTime`] and the
//!   [`time`](https://docs.rs/time) crate's `OffsetDateTime` and
//!   `PrimitiveDateTime`.
//!
//! Serde support is not feature-gated.
//!
//! [RDML consortium]: https://rdml.org/
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]

// Compile the README's examples as doctests.
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;

mod container;
mod enums;
mod error;
mod model;
pub mod partition_table;
mod types;
pub mod validate;
pub mod version;
pub mod xml;

pub use container::{DOCUMENT_MEMBER, PARTITIONS_FOLDER, RdmlFile};
pub use enums::{
    CqDetectionMethod, DyeChemistry, LabelFormat, Measure, Nucleotide, PrimingMethod, QuantityUnit,
    SampleType, TargetType,
};
pub use error::{Error, Result};
pub use model::*;
pub use partition_table::{PartitionColumn, PartitionPoint, PartitionScore, PartitionTable};
pub use types::{
    DateTime, DocumentationRef, DyeRef, ExperimenterRef, Id, Reasons, SampleRef, Sequence,
    TargetRef, TccRef,
};
pub use validate::{Finding, Severity, ValidationReport};
pub use version::{RdmlVersion, ReadNote, WriteLoss, WriteReport};