rdml-qpcr 0.1.1

Read, write, and validate RDML (Real-time PCR Data Markup Language) qPCR data files
Documentation
//! Digital-PCR partition data (schema `partitionsType`), available since
//! RDML 1.3.
//!
//! Digital PCR splits a reaction into thousands of partitions. To keep the
//! XML manageable, only the *counts* live in the document; the raw
//! per-partition fluorescence values live in TSV files under a
//! `partitions/` folder inside the `.rdml` archive, referenced by
//! [`Partitions::end_pt_table`] and modelled by
//! [`PartitionTable`](crate::partition_table::PartitionTable).

use serde::{Deserialize, Serialize};

use crate::types::{Reasons, TargetRef};

/// The partition summary of one digital-PCR reaction (element
/// `react/partitions`). *(RDML ≥ 1.3)*
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Partitions {
    /// Average volume of one partition in nanoliters.
    pub volume: f64,
    /// File name of the per-partition endpoint-fluorescence table inside
    /// the archive's `partitions/` folder (name only, no folder prefix).
    /// See [`RdmlFile::partition_table`](crate::RdmlFile::partition_table).
    #[serde(
        rename = "endPtTable",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub end_pt_table: Option<String>,
    /// Per-target partition counts. The schema requires at least one
    /// entry; [`validate`](crate::Rdml::validate) reports an empty list.
    pub data: Vec<PartitionData>,
}

impl Partitions {
    /// Creates a partition summary with the given average partition
    /// volume (nanoliters) and an empty data list — add at least one
    /// [`PartitionData`] before writing.
    #[must_use]
    pub fn new(volume: f64) -> Self {
        Self {
            volume,
            end_pt_table: None,
            data: Vec::new(),
        }
    }
}

/// Partition counts for one target (schema `partitionDataType`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PartitionData {
    /// The target these counts belong to.
    pub tar: TargetRef,
    /// If `Some`, this entry is excluded from evaluation, with the
    /// reasons why. Same presence semantics as
    /// [`Data::excl`](crate::Data::excl): `None` writes nothing;
    /// `<excluded>false</excluded>` is unrepresentable.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub excluded: Option<Reasons>,
    /// Notes that do not exclude the entry.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub note: Option<Reasons>,
    /// Number of positive partitions.
    pub pos: i32,
    /// Number of negative partitions.
    pub neg: i32,
    /// Number of undefined / not-yet-scored partitions.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub undef: Option<i32>,
    /// Number of excluded partitions.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub excl: Option<i32>,
    /// Concentration in copies per microliter of reaction mix.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub conc: Option<f64>,
}

impl PartitionData {
    /// Creates a count entry for `target` with `pos` positive and `neg`
    /// negative partitions.
    #[must_use]
    pub fn new(tar: TargetRef, pos: i32, neg: i32) -> Self {
        Self {
            tar,
            excluded: None,
            note: None,
            pos,
            neg,
            undef: None,
            excl: None,
            conc: None,
        }
    }
}