gsdesign 0.1.0

Group sequential design
Documentation
#[allow(dead_code)]
mod common;

use gsdesign::{DensityGrid, IntegrationError, gridpts, h1, hupdate};

use common::assert_close_slice;

#[test]
fn gridpts_supports_infinite_bounds() {
    let grid = gridpts(3, 0.0, f64::NEG_INFINITY, f64::INFINITY).expect("gridpts infinite");
    assert_eq!(grid.points.len(), grid.weights.len());
    for (lhs, rhs) in grid.points.iter().zip(grid.points.iter().rev()) {
        assert!((*lhs + *rhs).abs() < 1e-12);
    }
    assert!(grid.weights.iter().all(|w| *w > 0.0));
}

#[test]
fn gridpts_single_point_when_bounds_extreme() {
    let grid = gridpts(3, 0.0, 100.0, 101.0).expect("gridpts extreme bounds");
    assert_eq!(grid.points.len(), 1);
    assert_eq!(grid.weights.len(), 1);
    assert_close_slice(&grid.points, &[100.0]);
    assert_close_slice(&grid.weights, &[1.0]);
}

#[test]
fn gridpts_requires_r_at_least_two() {
    let err = gridpts(1, 0.0, -1.0, 1.0).expect_err("gridpts should fail");
    assert!(matches!(err, IntegrationError::InvalidStencil { .. }));
}

#[test]
fn gridpts_requires_increasing_bounds() {
    let err = gridpts(2, 0.0, 1.0, 1.0).expect_err("gridpts should fail");
    assert!(matches!(err, IntegrationError::NonIncreasingBounds { .. }));
}

#[test]
fn h1_requires_positive_info() {
    let err = h1(3, 0.0, 0.0, -1.0, 1.0).expect_err("h1 should fail");
    assert!(matches!(
        err,
        IntegrationError::NonPositiveInformation { .. }
    ));
}

#[test]
fn h1_infinite_bounds_integrates_to_one() {
    let grid = h1(3, 0.0, 1.0, f64::NEG_INFINITY, f64::INFINITY).expect("h1 infinite");
    let total: f64 = grid.values.iter().sum();
    assert!((total - 1.0).abs() < 1e-3);
}

#[test]
fn hupdate_requires_increasing_info() {
    let gm1 = h1(3, 0.0, 1.0, -2.0, 2.0).expect("h1 previous");
    let err = hupdate(3, 0.0, 1.0, -2.0, 2.0, 0.0, 1.0, &gm1).expect_err("hupdate should fail");
    assert!(matches!(
        err,
        IntegrationError::NonIncreasingInformation { info_prev, info }
        if (info_prev - 1.0).abs() < 1e-12 && (info - 1.0).abs() < 1e-12
    ));
}

#[test]
fn hupdate_requires_positive_previous_info() {
    let gm1 = DensityGrid {
        points: vec![0.0],
        weights: vec![1.0],
        values: vec![1.0],
    };
    let err = hupdate(3, 0.0, 2.0, -2.0, 2.0, 0.0, 0.0, &gm1).expect_err("hupdate should fail");
    assert!(matches!(
        err,
        IntegrationError::NonPositivePreviousInformation { info_prev }
        if info_prev == 0.0
    ));
}

#[test]
fn hupdate_rejects_shape_mismatch() {
    let gm1 = DensityGrid {
        points: vec![0.0, 1.0],
        weights: vec![1.0, 1.0],
        values: vec![1.0],
    };
    let err = hupdate(3, 0.0, 2.0, -2.0, 2.0, 0.0, 1.0, &gm1).expect_err("hupdate should fail");
    assert!(matches!(
        err,
        IntegrationError::ShapeMismatch {
            points,
            weights,
            values: Some(values),
        }
        if points == 2 && weights == 2 && values == 1
    ));
}

#[test]
fn hupdate_infinite_bounds_preserves_mass() {
    let gm1 = h1(3, 0.0, 1.0, f64::NEG_INFINITY, f64::INFINITY).expect("h1 infinite");
    let grid = hupdate(
        3,
        0.0,
        2.0,
        f64::NEG_INFINITY,
        f64::INFINITY,
        0.0,
        1.0,
        &gm1,
    )
    .expect("hupdate infinite");
    let total: f64 = grid.values.iter().sum();
    assert!((total - 1.0).abs() < 1e-3);
}