use super::SequenceStatistics;
use crate::errors::{QlError, QlResult};
use crate::require;
pub const MAX_CONVERGENCE_SAMPLES: usize = 100_000;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ConvergencePoint {
pub samples: usize,
pub mean: f64,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DoublingConvergenceSteps;
impl DoublingConvergenceSteps {
pub const fn initial_samples() -> usize {
1
}
pub fn next_samples(current: usize) -> QlResult<usize> {
current
.checked_mul(2)
.and_then(|value| value.checked_add(1))
.ok_or_else(|| QlError::new("convergence checkpoint overflow", file!(), line!()))
}
}
pub fn validate_convergence_length(len: usize) -> QlResult<usize> {
require!(
len <= MAX_CONVERGENCE_SAMPLES,
"convergence sample limit exceeded"
);
let mut next = DoublingConvergenceSteps::initial_samples();
let mut count = 0;
while next <= len {
count += 1;
next = DoublingConvergenceSteps::next_samples(next)?;
}
Ok(count)
}
pub fn evaluate_convergence_batch(
values: &[f64],
weights: Option<&[f64]>,
) -> QlResult<Vec<ConvergencePoint>> {
let mut statistics = ConvergenceStatistics::new();
statistics.add_batch(values, weights)?;
Ok(statistics.table)
}
#[derive(Clone, Debug, Default)]
pub struct ConvergenceStatistics {
values: Vec<f64>,
weights: Vec<f64>,
weight_sum: f64,
table: Vec<ConvergencePoint>,
}
impl ConvergenceStatistics {
pub fn new() -> Self {
Self::default()
}
pub fn samples(&self) -> usize {
self.values.len()
}
pub fn weight_sum(&self) -> f64 {
self.weight_sum
}
pub fn convergence_table(&self) -> &[ConvergencePoint] {
&self.table
}
pub fn add(&mut self, value: f64) -> QlResult<()> {
self.add_weighted(value, 1.0)
}
pub fn add_weighted(&mut self, value: f64, weight: f64) -> QlResult<()> {
self.add_batch(&[value], Some(&[weight]))
}
pub fn add_batch(&mut self, values: &[f64], weights: Option<&[f64]>) -> QlResult<()> {
let checkpoint_count = validate_addition(self.samples(), values.len())?;
if let Some(weights) = weights {
require!(
weights.len() == values.len(),
"convergence weight length mismatch"
);
}
let mut total = self.weight_sum;
for (index, &value) in values.iter().enumerate() {
let weight = weights.map_or(1.0, |weights| weights[index]);
require!(value.is_finite(), "nonfinite convergence sample");
require!(
weight.is_finite() && weight >= 0.0,
"invalid convergence weight"
);
total += weight;
require!(total.is_finite(), "convergence weight sum overflow");
}
let new_points = if checkpoint_count > self.table.len() {
self.pending_points(values, weights, checkpoint_count - self.table.len())?
} else {
Vec::new()
};
self.values.extend_from_slice(values);
self.weights
.extend((0..values.len()).map(|index| weights.map_or(1.0, |weights| weights[index])));
self.weight_sum = total;
self.table.extend(new_points);
Ok(())
}
pub fn mean(&self) -> QlResult<f64> {
let mut statistics = SequenceStatistics::new(1)?;
for (&value, &weight) in self.values.iter().zip(&self.weights) {
statistics.add_weighted(&[value], weight)?;
}
Ok(statistics.mean()?[0])
}
pub fn reset(&mut self) {
self.values.clear();
self.weights.clear();
self.table.clear();
self.weight_sum = 0.0;
}
fn pending_points(
&self,
values: &[f64],
weights: Option<&[f64]>,
count: usize,
) -> QlResult<Vec<ConvergencePoint>> {
let mut next = self.table.last().map_or(Ok(1), |point| {
DoublingConvergenceSteps::next_samples(point.samples)
})?;
let mut statistics = SequenceStatistics::new(1)?;
let mut points = Vec::with_capacity(count);
let existing = self
.values
.iter()
.copied()
.zip(self.weights.iter().copied());
let incoming = values
.iter()
.copied()
.enumerate()
.map(|(index, value)| (value, weights.map_or(1.0, |weights| weights[index])));
for (value, weight) in existing.chain(incoming) {
statistics.add_weighted(&[value], weight)?;
if statistics.samples() == next {
points.push(ConvergencePoint {
samples: next,
mean: statistics.mean()?[0],
});
if points.len() == count {
break;
}
next = DoublingConvergenceSteps::next_samples(next)?;
}
}
Ok(points)
}
}
fn validate_addition(current: usize, additional: usize) -> QlResult<usize> {
let final_count = current
.checked_add(additional)
.ok_or_else(|| QlError::new("convergence sample count overflow", file!(), line!()))?;
validate_convergence_length(final_count)
}
#[cfg(test)]
#[path = "convergence_tests.rs"]
mod tests;