use crate::errors::QlResult;
use crate::require;
use crate::types::Real;
use super::sequence_shape::{SequenceStatistic, validate_sequence_shape};
pub const MAX_DISCREPANCY_ROWS: usize = 4096;
pub const MAX_DISCREPANCY_WORK: usize = 100_000_000;
pub fn validate_discrepancy_shape(rows: usize, dimension: usize) -> QlResult<usize> {
require!(dimension >= 2, "discrepancy dimension must be at least two");
validate_sequence_shape(rows, dimension, SequenceStatistic::Mean)?;
require!(
rows <= MAX_DISCREPANCY_ROWS,
"discrepancy row limit exceeded"
);
require!(
rows.checked_mul(rows)
.and_then(|pairs| pairs.checked_mul(dimension))
.is_some_and(|work| work <= MAX_DISCREPANCY_WORK),
"discrepancy pair work limit exceeded"
);
Ok(rows * dimension)
}
pub fn evaluate_discrepancy_batch(
values: &[Real],
rows: usize,
dimension: usize,
weights: Option<&[Real]>,
) -> QlResult<Real> {
let components = validate_discrepancy_shape(rows, dimension)?;
require!(
values.len() == components,
"discrepancy sample shape mismatch"
);
if let Some(weights) = weights {
require!(weights.len() == rows, "discrepancy weight length mismatch");
require!(
weights.iter().all(|&weight| weight == 1.0),
"discrepancy requires unit weights"
);
}
validate_coordinates(values)?;
let mut statistics = DiscrepancyStatistics::new(dimension)?;
for point in values.chunks_exact(dimension) {
statistics.add(point)?;
}
statistics.discrepancy()
}
#[derive(Clone, Debug)]
pub struct DiscrepancyStatistics {
dimension: usize,
values: Vec<Real>,
pair_sum: CompensatedSum,
coordinate_sum: CompensatedSum,
}
impl DiscrepancyStatistics {
pub fn new(dimension: usize) -> QlResult<Self> {
validate_discrepancy_shape(1, dimension)?;
Ok(Self {
dimension,
values: Vec::new(),
pair_sum: CompensatedSum::default(),
coordinate_sum: CompensatedSum::default(),
})
}
pub fn dimension(&self) -> usize {
self.dimension
}
pub fn samples(&self) -> usize {
self.values.len() / self.dimension
}
pub fn add(&mut self, sample: &[Real]) -> QlResult<()> {
self.add_weighted(sample, 1.0)
}
pub fn add_weighted(&mut self, sample: &[Real], weight: Real) -> QlResult<()> {
require!(weight == 1.0, "discrepancy requires unit weights");
require!(
sample.len() == self.dimension,
"discrepancy sample dimension mismatch"
);
validate_discrepancy_shape(self.samples() + 1, self.dimension)?;
validate_coordinates(sample)?;
let mut pair_sum = self.pair_sum;
for point in self.values.chunks_exact(self.dimension) {
let product = point
.iter()
.zip(sample)
.map(|(&left, &right)| 1.0 - left.max(right))
.product::<Real>();
pair_sum.add(2.0 * product);
}
pair_sum.add(sample.iter().map(|&value| 1.0 - value).product());
let coordinate_product = sample.iter().map(|&value| 1.0 - value * value).product();
self.values.extend_from_slice(sample);
self.pair_sum = pair_sum;
self.coordinate_sum.add(coordinate_product);
Ok(())
}
pub fn reset(&mut self, dimension: usize) -> QlResult<()> {
let dimension = if dimension == 0 {
self.dimension
} else {
dimension
};
validate_discrepancy_shape(1, dimension)?;
self.values.clear();
self.pair_sum = CompensatedSum::default();
self.coordinate_sum = CompensatedSum::default();
self.dimension = dimension;
Ok(())
}
pub fn discrepancy(&self) -> QlResult<Real> {
require!(
self.samples() > 0,
"discrepancy requires at least one point"
);
let count = self.samples() as Real;
let exponent = self.dimension as i32;
let pair_term = self.pair_sum.total() / count / count;
let coordinate_term = 2.0_f64.powi(1 - exponent) * self.coordinate_sum.total() / count;
let constant_term = 3.0_f64.powi(-exponent);
finish_discrepancy(pair_term, coordinate_term, constant_term, self.dimension)
}
}
fn validate_coordinates(values: &[Real]) -> QlResult<()> {
require!(
values
.iter()
.all(|value| value.is_finite() && (0.0..=1.0).contains(value)),
"discrepancy coordinates must be finite and in [0,1]"
);
Ok(())
}
fn finish_discrepancy(
pair: Real,
coordinate: Real,
constant: Real,
dimension: usize,
) -> QlResult<Real> {
let squared = (pair - coordinate) + constant;
let tolerance = 8.0
* Real::EPSILON
* (dimension + 2) as Real
* (pair.abs() + coordinate.abs() + constant.abs());
require!(
squared.is_finite() && squared >= -tolerance,
"invalid discrepancy squared result"
);
Ok(squared.max(0.0).sqrt())
}
#[derive(Clone, Copy, Debug, Default)]
struct CompensatedSum {
sum: Real,
correction: Real,
}
impl CompensatedSum {
fn add(&mut self, value: Real) {
let next = self.sum + value;
self.correction += if self.sum.abs() >= value.abs() {
(self.sum - next) + value
} else {
(value - next) + self.sum
};
self.sum = next;
}
fn total(self) -> Real {
self.sum + self.correction
}
}
#[cfg(test)]
#[path = "discrepancy_tests.rs"]
mod tests;