#![deny(unsafe_code)]
#[cfg(feature = "clip")]
pub mod clip;
pub mod consensus;
pub mod correct;
pub mod duplex;
pub mod group;
pub mod rejection;
pub mod shared;
pub mod simplex;
pub mod writer;
use serde::{Deserialize, Serialize};
pub const FLOAT_PRECISION: usize = 6;
#[must_use]
pub fn format_float(value: f64) -> String {
format!("{value:.FLOAT_PRECISION$}")
}
#[must_use]
#[expect(clippy::cast_precision_loss, reason = "metric counts never exceed 2^53")]
pub fn frac(numerator: usize, denominator: usize) -> f64 {
if denominator > 0 { numerator as f64 / denominator as f64 } else { 0.0 }
}
#[must_use]
#[expect(clippy::cast_precision_loss, reason = "metric counts never exceed 2^53")]
pub fn frac_u64(numerator: u64, denominator: u64) -> f64 {
if denominator > 0 { numerator as f64 / denominator as f64 } else { 0.0 }
}
pub trait Metric: Serialize + for<'de> Deserialize<'de> + Clone + Default {
fn metric_name() -> &'static str;
}
pub trait ProcessingMetrics {
fn total_input(&self) -> u64;
fn total_output(&self) -> u64;
fn total_filtered(&self) -> u64;
fn efficiency(&self) -> f64 {
frac_u64(self.total_output(), self.total_input()) * 100.0
}
}
#[cfg(feature = "clip")]
pub use clip::{ClipCounts, ClippingMetrics, ClippingMetricsCollection, ReadType};
pub use consensus::{ConsensusKvMetric, ConsensusMetrics};
pub use correct::UmiCorrectionMetrics;
pub use duplex::{
DuplexFamilySizeMetric, DuplexMetricsCollector, DuplexUmiMetric, DuplexYieldMetric,
FamilySizeMetric,
};
pub use group::{FamilySizeMetrics, PositionGroupSizeMetrics, UmiGroupingMetrics};
pub use rejection::{RejectionReason, format_count};
pub use shared::UmiMetric;
pub use simplex::{SimplexFamilySizeMetric, SimplexMetricsCollector, SimplexYieldMetric};
pub use writer::{read_metrics, read_metrics_auto, write_metrics};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frac_normal() {
assert!((frac(3, 4) - 0.75).abs() < f64::EPSILON);
}
#[test]
fn test_frac_zero_denominator() {
assert!((frac(5, 0)).abs() < f64::EPSILON);
}
#[test]
fn test_frac_zero_numerator() {
assert!((frac(0, 10)).abs() < f64::EPSILON);
}
#[test]
fn test_frac_u64_normal() {
assert!((frac_u64(3, 4) - 0.75).abs() < f64::EPSILON);
}
#[test]
fn test_frac_u64_zero_denominator() {
assert!((frac_u64(5, 0)).abs() < f64::EPSILON);
}
#[test]
fn test_frac_u64_zero_numerator() {
assert!((frac_u64(0, 10)).abs() < f64::EPSILON);
}
#[test]
fn test_processing_metrics_consensus() {
let metrics = ConsensusMetrics {
total_input_reads: 1000,
consensus_reads: 800,
filtered_reads: 200,
..Default::default()
};
assert_eq!(metrics.total_input(), 1000);
assert_eq!(metrics.total_output(), 800);
assert_eq!(metrics.total_filtered(), 200);
assert!((metrics.efficiency() - 80.0).abs() < f64::EPSILON);
}
#[test]
fn test_processing_metrics_grouping() {
let metrics = UmiGroupingMetrics {
total_records: 1000,
accepted_records: 900,
discarded_non_pf: 50,
discarded_poor_alignment: 30,
discarded_ns_in_umi: 20,
..Default::default()
};
assert_eq!(metrics.total_input(), 1000);
assert_eq!(metrics.total_output(), 900);
assert_eq!(metrics.total_filtered(), 100);
assert!((metrics.efficiency() - 90.0).abs() < f64::EPSILON);
}
#[test]
fn test_processing_metrics_zero_input() {
let metrics = ConsensusMetrics::default();
assert_eq!(metrics.total_input(), 0);
assert_eq!(metrics.total_output(), 0);
assert_eq!(metrics.total_filtered(), 0);
assert!((metrics.efficiency()).abs() < f64::EPSILON);
}
#[test]
fn test_processing_metrics_generic_usage() {
fn log_efficiency(m: &impl ProcessingMetrics) -> f64 {
m.efficiency()
}
let consensus =
ConsensusMetrics { total_input_reads: 100, consensus_reads: 50, ..Default::default() };
let grouping =
UmiGroupingMetrics { total_records: 100, accepted_records: 75, ..Default::default() };
assert!((log_efficiency(&consensus) - 50.0).abs() < f64::EPSILON);
assert!((log_efficiency(&grouping) - 75.0).abs() < f64::EPSILON);
}
}