use crate::param::dmatrix_gamma::GammaMatrix;
use crate::param::traits::{Inference, TwoStatParam};
use nalgebra::DMatrix;
#[test]
fn log_sd_is_sqrt_trigamma_of_the_shape() {
let cases = [
(1.0_f32, (std::f32::consts::PI.powi(2) / 6.0).sqrt()),
(2.0, (std::f32::consts::PI.powi(2) / 6.0 - 1.0).sqrt()),
(0.5, (std::f32::consts::PI.powi(2) / 2.0).sqrt()),
];
for (a, want) in cases {
let mut p = GammaMatrix::new((1, 1), 0.0, 0.0);
p.update_stat(
&DMatrix::from_element(1, 1, a),
&DMatrix::from_element(1, 1, 3.0),
);
p.calibrate();
let got = p.posterior_log_sd()[(0, 0)];
assert!(
(got - want).abs() < 1e-4,
"a={a}: log_sd {got} != sqrt(trigamma(a)) {want}"
);
}
}
#[test]
fn an_unobserved_feature_has_the_largest_log_sd_not_zero() {
let mut p = GammaMatrix::new((2, 1), 1.0, 1.0);
let a_obs = DMatrix::from_row_slice(2, 1, &[0.0, 500.0]);
let b_obs = DMatrix::from_row_slice(2, 1, &[0.0, 500.0]);
p.update_stat(&a_obs, &b_obs);
p.calibrate();
let sd = p.posterior_log_sd();
assert!(
sd[(0, 0)] > 1.0,
"unobserved row should be uncertain, got {}",
sd[(0, 0)]
);
assert!(
sd[(1, 0)] < 0.1,
"well-observed row should be precise, got {}",
sd[(1, 0)]
);
assert!(
sd[(0, 0)] > sd[(1, 0)] * 10.0,
"uncertainty should be ordered by evidence"
);
}
use nalgebra::DVector;
fn rows(v: &[f32]) -> DVector<f32> {
DVector::from_column_slice(v)
}
#[test]
fn row_prior_reset_fills_each_row_with_its_own_hyperparameter() {
let a0 = rows(&[1.0, 2.0, 3.0]);
let b0 = rows(&[4.0, 5.0, 6.0]);
let mut p = GammaMatrix::with_row_prior((3, 2), &a0, &b0);
p.update_stat(&DMatrix::zeros(3, 2), &DMatrix::zeros(3, 2));
p.calibrate();
let m = p.posterior_mean();
for d in 0..3 {
for c in 0..2 {
let want = a0[d] / b0[d];
assert!(
(m[(d, c)] - want).abs() < 1e-6,
"row {d} col {c}: mean {} != {want}",
m[(d, c)]
);
}
}
}
#[test]
fn row_prior_with_equal_rows_matches_scalar_path_exactly() {
let a = DMatrix::from_row_slice(3, 2, &[1.0, 5.0, 0.0, 2.0, 7.5, 3.0]);
let b = DMatrix::from_row_slice(3, 2, &[2.0, 4.0, 1.0, 1.5, 6.0, 2.5]);
let mut scalar = GammaMatrix::new((3, 2), 0.5, 2.0);
let mut row = GammaMatrix::with_row_prior((3, 2), &rows(&[0.5; 3]), &rows(&[2.0; 3]));
scalar.update_stat(&a, &b);
row.update_stat(&a, &b);
scalar.calibrate();
row.calibrate();
assert_eq!(scalar.posterior_mean(), row.posterior_mean());
assert_eq!(scalar.posterior_sd(), row.posterior_sd());
assert_eq!(scalar.posterior_log_mean(), row.posterior_log_mean());
assert_eq!(scalar.posterior_log_sd(), row.posterior_log_sd());
}
#[test]
fn row_prior_update_stat_equals_hand_folded_prior() {
let a = DMatrix::from_row_slice(2, 2, &[3.0, 0.0, 10.0, 4.0]);
let b = DMatrix::from_row_slice(2, 2, &[2.0, 1.0, 8.0, 3.0]);
let a0 = rows(&[0.7, 9.0]);
let b0 = rows(&[0.7, 9.0]);
let mut folded = GammaMatrix::new((2, 2), 0.0, 0.0);
let mut a_f = a.clone();
let mut b_f = b.clone();
for d in 0..2 {
a_f.row_mut(d).add_scalar_mut(a0[d]);
b_f.row_mut(d).add_scalar_mut(b0[d]);
}
folded.update_stat(&a_f, &b_f);
folded.calibrate();
let mut row = GammaMatrix::with_row_prior((2, 2), &a0, &b0);
row.update_stat(&a, &b);
row.calibrate();
for (x, y) in folded
.posterior_mean()
.iter()
.zip(row.posterior_mean().iter())
{
assert!((x - y).abs() < 1e-6, "mean {x} != {y}");
}
for (x, y) in folded
.posterior_log_mean()
.iter()
.zip(row.posterior_log_mean().iter())
{
assert!((x - y).abs() < 1e-6, "log mean {x} != {y}");
}
}
#[test]
fn evidence_mean_subtracts_the_row_prior() {
let a = DMatrix::from_row_slice(2, 2, &[3.0, 0.0, 10.0, 4.0]);
let b = DMatrix::from_row_slice(2, 2, &[2.0, 1.0, 8.0, 3.0]);
let mut p = GammaMatrix::with_row_prior((2, 2), &rows(&[1.0, 2.0]), &rows(&[3.0, 4.0]));
p.update_stat(&a, &b);
for d in 0..2 {
for c in 0..2 {
let want = if a[(d, c)] > 0.0 {
a[(d, c)] / b[(d, c)]
} else {
0.0
};
assert!((p.evidence_mean(d, c) - want).abs() < 1e-6);
assert_eq!(p.has_data_support(d, c), a[(d, c)] > 0.0);
}
}
}
#[test]
fn vconcat_concatenates_row_priors() {
let mut top = GammaMatrix::with_row_prior((2, 2), &rows(&[1.0, 2.0]), &rows(&[3.0, 4.0]));
let mut bottom = GammaMatrix::with_row_prior((1, 2), &rows(&[5.0]), &rows(&[6.0]));
top.update_stat(
&DMatrix::from_element(2, 2, 10.0),
&DMatrix::from_element(2, 2, 2.0),
);
bottom.update_stat(
&DMatrix::from_element(1, 2, 7.0),
&DMatrix::from_element(1, 2, 1.0),
);
let want_bottom = bottom.evidence_mean(0, 1);
let want_top = top.evidence_mean(1, 0);
let stacked = GammaMatrix::vconcat(vec![top, bottom], true);
let (a0, b0) = stacked.row_prior().expect("row prior carried through");
assert_eq!(a0, &rows(&[1.0, 2.0, 5.0]));
assert_eq!(b0, &rows(&[3.0, 4.0, 6.0]));
assert!((stacked.evidence_mean(2, 1) - want_bottom).abs() < 1e-6);
assert!((stacked.evidence_mean(1, 0) - want_top).abs() < 1e-6);
}
#[test]
#[should_panic]
fn vconcat_scalar_blocks_with_different_hyperparameters_panics() {
let a = GammaMatrix::new((1, 1), 1.0, 1.0);
let b = GammaMatrix::new((1, 1), 2.0, 1.0);
let _ = GammaMatrix::vconcat(vec![a, b], true);
}
#[test]
#[should_panic]
fn vconcat_mixed_scalar_and_row_prior_panics() {
let a = GammaMatrix::new((1, 1), 1.0, 1.0);
let b = GammaMatrix::with_row_prior((1, 1), &rows(&[1.0]), &rows(&[1.0]));
let _ = GammaMatrix::vconcat(vec![a, b], true);
}
#[test]
fn set_row_prior_takes_effect_at_next_reset_only() {
let a = DMatrix::from_element(2, 1, 4.0);
let b = DMatrix::from_element(2, 1, 2.0);
let mut p = GammaMatrix::with_row_prior((2, 1), &rows(&[1.0, 1.0]), &rows(&[1.0, 1.0]));
p.update_stat(&a, &b);
p.calibrate();
let before = p.posterior_mean().clone();
p.set_row_prior(&rows(&[10.0, 10.0]), &rows(&[10.0, 10.0]));
p.calibrate();
assert_eq!(p.posterior_mean(), &before);
p.update_stat(&a, &b);
p.calibrate();
let want = (10.0 + 4.0) / (10.0 + 2.0);
for m in p.posterior_mean().iter() {
assert!((m - want).abs() < 1e-6, "mean {m} != {want}");
}
}
#[test]
#[should_panic]
fn set_row_prior_wrong_length_panics() {
let mut p = GammaMatrix::new((2, 1), 1.0, 1.0);
p.set_row_prior(&rows(&[1.0]), &rows(&[1.0]));
}