libitofin 0.52.1

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
use super::*;

fn close(actual: f64, expected: f64) {
    assert!(
        (actual - expected).abs() <= 2e-15 * expected.abs().max(1.0),
        "actual {actual}, expected {expected}"
    );
}

fn unchanged(statistics: &ConvergenceStatistics, previous: &ConvergenceStatistics) {
    assert_eq!(statistics.values, previous.values);
    assert_eq!(statistics.weights, previous.weights);
    assert_eq!(statistics.weight_sum(), previous.weight_sum());
    assert_eq!(statistics.convergence_table(), previous.convergence_table());
}

#[test]
fn doubling_steps_are_checked() {
    assert_eq!(DoublingConvergenceSteps::initial_samples(), 1);
    let mut current = 1;
    for expected in [3, 7, 15, 31, 63] {
        current = DoublingConvergenceSteps::next_samples(current).unwrap();
        assert_eq!(current, expected);
    }
    assert_eq!(DoublingConvergenceSteps::next_samples(0).unwrap(), 1);
    assert_eq!(
        DoublingConvergenceSteps::next_samples(usize::MAX / 2).unwrap(),
        usize::MAX
    );
    assert!(DoublingConvergenceSteps::next_samples(usize::MAX / 2 + 1).is_err());
    assert!(DoublingConvergenceSteps::next_samples(usize::MAX).is_err());
}

#[test]
fn input_length_preflight_returns_completed_checkpoints() {
    for (length, count) in [(0, 0), (1, 1), (2, 1), (3, 2), (6, 2), (7, 3), (15, 4)] {
        assert_eq!(validate_convergence_length(length).unwrap(), count);
    }
    assert_eq!(
        validate_convergence_length(MAX_CONVERGENCE_SAMPLES).unwrap(),
        16
    );
    assert!(validate_convergence_length(MAX_CONVERGENCE_SAMPLES + 1).is_err());
    assert!(validate_convergence_length(usize::MAX).is_err());
}

#[test]
fn addition_count_checks_overflow_and_cap_before_allocation() {
    assert_eq!(validate_addition(2, 5).unwrap(), 3);
    assert!(validate_addition(usize::MAX, 1).is_err());
    assert!(validate_addition(usize::MAX / 2 + 1, usize::MAX / 2 + 1).is_err());
    assert!(validate_addition(MAX_CONVERGENCE_SAMPLES, 1).is_err());
    assert_eq!(validate_addition(MAX_CONVERGENCE_SAMPLES, 0).unwrap(), 16);
}

#[test]
fn cumulative_checkpoints_exclude_incomplete_tail() {
    let values: Vec<_> = (1..=16).map(f64::from).collect();
    let mut statistics = ConvergenceStatistics::new();
    statistics.add_batch(&values, None).unwrap();
    assert_eq!(statistics.samples(), 16);
    assert_eq!(statistics.weight_sum(), 16.0);
    close(statistics.mean().unwrap(), 8.5);
    assert_eq!(
        statistics
            .convergence_table()
            .iter()
            .map(|point| point.samples)
            .collect::<Vec<_>>(),
        [1, 3, 7, 15]
    );
    for (point, expected) in statistics
        .convergence_table()
        .iter()
        .zip([1.0, 2.0, 4.0, 8.0])
    {
        close(point.mean, expected);
    }
    assert_eq!(
        evaluate_convergence_batch(&values, None).unwrap(),
        statistics.table
    );
}

#[test]
fn weighted_prefix_means_count_zero_weight_samples() {
    let values = [2.0, 1000.0, 8.0, 10.0, -1000.0, 6.0, 4.0, 100.0];
    let weights = [2.0, 0.0, 1.0, 3.0, 0.0, 2.0, 2.0, 5.0];
    let mut statistics = ConvergenceStatistics::new();
    statistics.add_batch(&values, Some(&weights)).unwrap();
    assert_eq!(statistics.samples(), 8);
    assert_eq!(statistics.weight_sum(), 15.0);
    for (point, (count, mean)) in statistics.table.iter().zip([(1, 2.0), (3, 4.0), (7, 6.2)]) {
        assert_eq!(point.samples, count);
        close(point.mean, mean);
    }
    close(statistics.mean().unwrap(), 562.0 / 15.0);
}

#[test]
fn streaming_and_differently_partitioned_batches_are_identical() {
    let values: Vec<_> = (0..100).map(|value| f64::from(value) - 15.0).collect();
    let weights: Vec<_> = (0..100).map(|index| f64::from(index % 4)).collect();
    let mut weights = weights;
    weights[0] = 1.0;
    let expected = evaluate_convergence_batch(&values, Some(&weights)).unwrap();
    let mut streamed = ConvergenceStatistics::new();
    for (&value, &weight) in values.iter().zip(&weights) {
        streamed.add_weighted(value, weight).unwrap();
    }
    assert_eq!(streamed.convergence_table(), expected);
    let mut batched = ConvergenceStatistics::new();
    for (values, weights) in values.chunks(9).zip(weights.chunks(9)) {
        batched.add_batch(values, Some(weights)).unwrap();
    }
    assert_eq!(batched.convergence_table(), expected);
    assert_eq!(
        batched.mean().unwrap().to_bits(),
        streamed.mean().unwrap().to_bits()
    );
}

#[test]
fn reset_and_clone_preserve_independent_storage() {
    let mut statistics = ConvergenceStatistics::default();
    assert_eq!(statistics.samples(), 0);
    assert_eq!(statistics.weight_sum(), 0.0);
    assert!(statistics.mean().is_err());
    assert!(statistics.convergence_table().is_empty());
    statistics.add_batch(&[2.0, 4.0, 6.0], None).unwrap();
    let original = statistics.clone();
    statistics.reset();
    assert_eq!(statistics.samples(), 0);
    assert_eq!(statistics.weight_sum(), 0.0);
    assert!(statistics.mean().is_err());
    assert!(statistics.convergence_table().is_empty());
    statistics.add(9.0).unwrap();
    assert_eq!(
        statistics.table,
        [ConvergencePoint {
            samples: 1,
            mean: 9.0
        }]
    );
    assert_eq!(original.samples(), 3);
    close(original.mean().unwrap(), 4.0);
}

#[test]
fn invalid_sample_or_weight_additions_are_atomic() {
    let mut statistics = ConvergenceStatistics::new();
    statistics.add(2.0).unwrap();
    let previous = statistics.clone();
    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
        assert!(statistics.add(value).is_err());
        assert!(statistics.add_weighted(value, 0.0).is_err());
        unchanged(&statistics, &previous);
    }
    for weight in [-1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
        assert!(statistics.add_weighted(3.0, weight).is_err());
        unchanged(&statistics, &previous);
    }
}

#[test]
fn invalid_batch_rolls_back_earlier_samples_and_checkpoints() {
    let mut statistics = ConvergenceStatistics::new();
    statistics.add_batch(&[1.0, 2.0], None).unwrap();
    let previous = statistics.clone();
    assert!(statistics.add_batch(&[3.0, 4.0, f64::NAN], None).is_err());
    unchanged(&statistics, &previous);
    assert!(
        statistics
            .add_batch(&[3.0, 4.0, 5.0], Some(&[1.0, 1.0, -1.0]))
            .is_err()
    );
    unchanged(&statistics, &previous);
    assert!(statistics.add_batch(&[3.0], Some(&[])).is_err());
    unchanged(&statistics, &previous);
    assert!(statistics.add_batch(&[], Some(&[1.0])).is_err());
    unchanged(&statistics, &previous);
    statistics.add_batch(&[], Some(&[])).unwrap();
    unchanged(&statistics, &previous);
}

#[test]
fn zero_total_first_checkpoint_rejects_whole_batch() {
    let mut statistics = ConvergenceStatistics::new();
    let previous = statistics.clone();
    assert!(statistics.add_weighted(2.0, 0.0).is_err());
    unchanged(&statistics, &previous);
    assert!(
        statistics
            .add_batch(&[2.0, 4.0, 6.0], Some(&[0.0, 1.0, 1.0]))
            .is_err()
    );
    unchanged(&statistics, &previous);
    assert!(evaluate_convergence_batch(&[2.0, 4.0], Some(&[0.0, 1.0])).is_err());
    statistics.add_weighted(2.0, 1.0).unwrap();
    statistics.add_weighted(100.0, -0.0).unwrap();
    statistics.add_weighted(200.0, 0.0).unwrap();
    close(statistics.table[1].mean, 2.0);
}

#[test]
fn empty_batch_is_allowed_and_has_no_checkpoint() {
    assert!(evaluate_convergence_batch(&[], None).unwrap().is_empty());
    assert!(
        evaluate_convergence_batch(&[], Some(&[]))
            .unwrap()
            .is_empty()
    );
    assert!(evaluate_convergence_batch(&[], Some(&[1.0])).is_err());
}

#[test]
fn overflowing_weight_sum_is_atomic() {
    let mut statistics = ConvergenceStatistics::new();
    statistics.add_weighted(3.0, f64::MAX).unwrap();
    let previous = statistics.clone();
    assert!(statistics.add_weighted(4.0, f64::MAX).is_err());
    unchanged(&statistics, &previous);
    assert!(
        statistics
            .add_batch(&[4.0, 5.0], Some(&[0.0, f64::MAX]))
            .is_err()
    );
    unchanged(&statistics, &previous);
    close(statistics.mean().unwrap(), 3.0);
}

#[test]
fn sample_cap_allows_maximum_without_quadratic_streaming_work() {
    let mut statistics = ConvergenceStatistics::new();
    for _ in 0..MAX_CONVERGENCE_SAMPLES {
        statistics.add(2.0).unwrap();
    }
    assert_eq!(statistics.samples(), MAX_CONVERGENCE_SAMPLES);
    assert_eq!(statistics.table.len(), 16);
    assert_eq!(statistics.table.last().unwrap().samples, 65_535);
    close(statistics.mean().unwrap(), 2.0);
    let previous = statistics.clone();
    assert!(statistics.add(3.0).is_err());
    unchanged(&statistics, &previous);
    assert!(statistics.add_batch(&[3.0, 4.0], None).is_err());
    unchanged(&statistics, &previous);
    assert!(evaluate_convergence_batch(&vec![1.0; MAX_CONVERGENCE_SAMPLES + 1], None).is_err());
}

#[test]
fn compensated_cancellation_mean_is_stable_for_all_permutations() {
    for values in [
        [1e16, -1e16, 1.0],
        [1e16, 1.0, -1e16],
        [-1e16, 1e16, 1.0],
        [-1e16, 1.0, 1e16],
        [1.0, 1e16, -1e16],
        [1.0, -1e16, 1e16],
    ] {
        let table = evaluate_convergence_batch(&values, None).unwrap();
        close(table[1].mean, 1.0 / 3.0);
    }
}

#[test]
fn extreme_weight_ratios_and_large_offsets_reuse_stable_sequence_mean() {
    let table = evaluate_convergence_batch(&[1e16, 1.0, 1.0], Some(&[1e-16, 1.0, 1.0])).unwrap();
    close(table[1].mean, 1.5);
    let table =
        evaluate_convergence_batch(&[1e300, 0.0, 0.0], Some(&[1e-300, 1e300, 0.0])).unwrap();
    assert!((table[1].mean / 1e-300 - 1.0).abs() < 2e-15);
    let table = evaluate_convergence_batch(&[1e12, 1e12 + 1.0, 1e12 + 2.0], None).unwrap();
    assert_eq!(table[1].mean, 1e12 + 1.0);
    let table = evaluate_convergence_batch(&[f64::MAX, -f64::MAX, 0.0], None).unwrap();
    assert_eq!(table[1].mean, 0.0);
}