use serde::{Deserialize, Serialize};
use crate::pack::Pack;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PackMetrics {
pub num_sequences: usize,
pub num_packs: usize,
pub total_tokens: usize,
pub padding_tokens: usize,
pub efficiency: f64,
pub avg_utilisation: f64,
pub utilisation_std: f64,
pub min_utilisation: f64,
pub max_utilisation: f64,
pub avg_sequences_per_pack: f64,
pub packing_time_ms: f64,
}
impl PackMetrics {
pub fn padding_ratio(&self) -> f64 {
let total = self.total_tokens + self.padding_tokens;
if total == 0 {
0.0
} else {
self.padding_tokens as f64 / total as f64
}
}
pub fn throughput(&self) -> f64 {
if self.packing_time_ms == 0.0 {
f64::INFINITY
} else {
self.num_sequences as f64 / (self.packing_time_ms / 1000.0)
}
}
}
pub struct MetricsBuilder {
num_sequences: usize,
total_tokens: usize,
packing_time_ms: f64,
}
impl MetricsBuilder {
pub fn new(num_sequences: usize, total_tokens: usize) -> Self {
Self {
num_sequences,
total_tokens,
packing_time_ms: 0.0,
}
}
pub fn with_time(mut self, time_ms: f64) -> Self {
self.packing_time_ms = time_ms;
self
}
pub fn build(self, packs: &[Pack]) -> PackMetrics {
let num_packs = packs.len();
let padding_tokens: usize = packs.iter().map(|p| p.padding_tokens()).sum();
let total_capacity: usize = packs.iter().map(|p| p.capacity).sum();
let efficiency = if total_capacity == 0 {
0.0
} else {
self.total_tokens as f64 / total_capacity as f64
};
let utilisations: Vec<f64> = packs.iter().map(|p| p.utilisation()).collect();
let avg_utilisation = if utilisations.is_empty() {
0.0
} else {
utilisations.iter().sum::<f64>() / utilisations.len() as f64
};
let utilisation_std = if utilisations.len() < 2 {
0.0
} else {
let variance = utilisations
.iter()
.map(|&u| (u - avg_utilisation).powi(2))
.sum::<f64>()
/ utilisations.len() as f64;
variance.sqrt()
};
let min_utilisation = utilisations
.iter()
.copied()
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
let max_utilisation = utilisations
.iter()
.copied()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
let total_seqs: usize = packs.iter().map(|p| p.len()).sum();
let avg_sequences_per_pack = if num_packs == 0 {
0.0
} else {
total_seqs as f64 / num_packs as f64
};
PackMetrics {
num_sequences: self.num_sequences,
num_packs,
total_tokens: self.total_tokens,
padding_tokens,
efficiency,
avg_utilisation,
utilisation_std,
min_utilisation,
max_utilisation,
avg_sequences_per_pack,
packing_time_ms: self.packing_time_ms,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pack::Pack;
use crate::sequence::Sequence;
fn make_pack(capacity: usize, lengths: &[usize]) -> Pack {
let mut pack = Pack::new(capacity);
for (i, &len) in lengths.iter().enumerate() {
pack.add(Sequence::new(i, len)).unwrap();
}
pack
}
#[test]
fn test_padding_ratio_zero_padding() {
let packs = vec![make_pack(100, &[100])];
let m = MetricsBuilder::new(1, 100).build(&packs);
assert!((m.padding_ratio() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_padding_ratio_half_padding() {
let packs = vec![make_pack(100, &[50])];
let m = MetricsBuilder::new(1, 50).build(&packs);
assert!((m.padding_ratio() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_padding_ratio_empty() {
let packs: Vec<Pack> = vec![];
let m = MetricsBuilder::new(0, 0).build(&packs);
assert!((m.padding_ratio() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_throughput_normal() {
let packs = vec![make_pack(100, &[100])];
let m = MetricsBuilder::new(1000, 100)
.with_time(500.0)
.build(&packs);
assert!((m.throughput() - 2000.0).abs() < f64::EPSILON);
}
#[test]
fn test_throughput_zero_time() {
let packs = vec![make_pack(100, &[100])];
let m = MetricsBuilder::new(10, 100).build(&packs);
assert!(m.throughput().is_infinite());
}
#[test]
fn test_builder_defaults() {
let packs = vec![make_pack(100, &[60])];
let m = MetricsBuilder::new(5, 60).build(&packs);
assert_eq!(m.num_sequences, 5);
assert_eq!(m.total_tokens, 60);
assert_eq!(m.packing_time_ms, 0.0);
}
#[test]
fn test_builder_with_time() {
let packs = vec![make_pack(100, &[100])];
let m = MetricsBuilder::new(1, 100).with_time(42.5).build(&packs);
assert!((m.packing_time_ms - 42.5).abs() < f64::EPSILON);
}
#[test]
fn test_build_num_packs() {
let packs = vec![make_pack(100, &[60, 30]), make_pack(100, &[50])];
let m = MetricsBuilder::new(3, 140).build(&packs);
assert_eq!(m.num_packs, 2);
}
#[test]
fn test_build_padding_tokens() {
let packs = vec![make_pack(100, &[60, 30]), make_pack(100, &[50])];
let m = MetricsBuilder::new(3, 140).build(&packs);
assert_eq!(m.padding_tokens, 60);
}
#[test]
fn test_build_efficiency() {
let packs = vec![make_pack(100, &[60, 30]), make_pack(100, &[50])];
let m = MetricsBuilder::new(3, 140).build(&packs);
assert!((m.efficiency - 0.7).abs() < f64::EPSILON);
}
#[test]
fn test_build_efficiency_empty() {
let packs: Vec<Pack> = vec![];
let m = MetricsBuilder::new(0, 0).build(&packs);
assert!((m.efficiency - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_build_avg_utilisation() {
let packs = vec![make_pack(100, &[60, 30]), make_pack(100, &[50])];
let m = MetricsBuilder::new(3, 140).build(&packs);
assert!((m.avg_utilisation - 0.7).abs() < f64::EPSILON);
}
#[test]
fn test_build_utilisation_std() {
let packs = vec![make_pack(100, &[60, 30]), make_pack(100, &[50])];
let m = MetricsBuilder::new(3, 140).build(&packs);
assert!((m.utilisation_std - 0.2).abs() < 1e-10);
}
#[test]
fn test_build_utilisation_std_single_pack() {
let packs = vec![make_pack(100, &[75])];
let m = MetricsBuilder::new(1, 75).build(&packs);
assert!((m.utilisation_std - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_build_min_max_utilisation() {
let packs = vec![
make_pack(100, &[90]), make_pack(100, &[50]), make_pack(100, &[70]), ];
let m = MetricsBuilder::new(3, 210).build(&packs);
assert!((m.min_utilisation - 0.5).abs() < f64::EPSILON);
assert!((m.max_utilisation - 0.9).abs() < f64::EPSILON);
}
#[test]
fn test_build_avg_sequences_per_pack() {
let packs = vec![make_pack(100, &[60, 30]), make_pack(100, &[50])];
let m = MetricsBuilder::new(3, 140).build(&packs);
assert!((m.avg_sequences_per_pack - 1.5).abs() < f64::EPSILON);
}
#[test]
fn test_padding_ratio_plus_efficiency_equals_one() {
let packs = vec![make_pack(100, &[60, 30]), make_pack(100, &[50])];
let m = MetricsBuilder::new(3, 140).build(&packs);
assert!((m.padding_ratio() + m.efficiency - 1.0).abs() < 1e-10);
}
}