use crate::scalar::Scalar;
use crate::spec::{BinomialLink, Family, GammaLink, InverseGaussianLink};
pub(crate) const ETA_MAX: f64 = 700.0;
pub(crate) const MU_FLOOR: f64 = 1e-10;
pub(crate) const PROB_EPS: f64 = 1e-12;
pub(crate) const FRAC_1_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
pub(crate) fn clamp_eta<T: Scalar>(family: Family, eta: T) -> T {
match family {
Family::Gamma {
link: GammaLink::Inverse,
..
}
| Family::InverseGaussian {
link: InverseGaussianLink::InverseSquared,
} => eta.clamp_f64(MU_FLOOR, ETA_MAX),
Family::Poisson { .. }
| Family::Gamma {
link: GammaLink::Log,
..
}
| Family::NegativeBinomial { .. }
| Family::InverseGaussian {
link: InverseGaussianLink::Log,
} => eta.clamp_f64(-ETA_MAX, ETA_MAX),
Family::Binomial {
link: BinomialLink::Logit | BinomialLink::Probit,
}
| Family::Gaussian => eta,
Family::Binomial {
link: BinomialLink::Cloglog,
} => eta.clamp_f64(-ETA_MAX, ETA_MAX.ln()),
}
}
pub(crate) fn eta_infeasible<T: Scalar>(family: Family, eta: T) -> bool {
matches!(
family,
Family::Gamma {
link: GammaLink::Inverse,
..
} | Family::InverseGaussian {
link: InverseGaussianLink::InverseSquared,
}
) && eta.value() <= 0.0
}
pub(crate) fn clamp_mu<T: Scalar>(family: Family, mu: T) -> T {
match family {
Family::Binomial { .. } => mu.clamp_f64(PROB_EPS, 1.0 - PROB_EPS),
Family::Poisson { .. }
| Family::Gamma { .. }
| Family::NegativeBinomial { .. }
| Family::InverseGaussian { .. } => mu.max_f64(MU_FLOOR),
Family::Gaussian => mu,
}
}
pub(crate) fn link_inv<T: Scalar>(family: Family, eta: T) -> T {
let eta = clamp_eta(family, eta);
let mu = match family {
Family::Gaussian => eta,
Family::Binomial {
link: BinomialLink::Logit,
} => eta.sigmoid(),
Family::Binomial {
link: BinomialLink::Probit,
} => eta.probit_cdf(),
Family::Binomial {
link: BinomialLink::Cloglog,
} => -((-Scalar::exp(eta)).exp_m1()),
Family::Poisson { .. }
| Family::Gamma {
link: GammaLink::Log,
..
}
| Family::NegativeBinomial { .. }
| Family::InverseGaussian {
link: InverseGaussianLink::Log,
} => Scalar::exp(eta),
Family::Gamma {
link: GammaLink::Inverse,
..
} => T::ONE / eta,
Family::InverseGaussian {
link: InverseGaussianLink::InverseSquared,
} => T::ONE / eta.sqrt(),
};
clamp_mu(family, mu)
}
pub(crate) fn mu_eta<T: Scalar>(family: Family, eta: T) -> T {
let eta = clamp_eta(family, eta);
match family {
Family::Gaussian => T::ONE,
Family::Binomial {
link: BinomialLink::Logit,
} => {
let mu = eta.sigmoid();
mu * (T::ONE - mu)
}
Family::Binomial {
link: BinomialLink::Probit,
} => T::from_f64(FRAC_1_SQRT_2PI) * (T::from_f64(-0.5) * eta * eta).exp(),
Family::Binomial {
link: BinomialLink::Cloglog,
} => (eta - Scalar::exp(eta)).exp(),
Family::Poisson { .. }
| Family::Gamma {
link: GammaLink::Log,
..
}
| Family::NegativeBinomial { .. }
| Family::InverseGaussian {
link: InverseGaussianLink::Log,
} => Scalar::exp(eta),
Family::Gamma {
link: GammaLink::Inverse,
..
} => {
let mu = T::ONE / eta;
-mu * mu
}
Family::InverseGaussian {
link: InverseGaussianLink::InverseSquared,
} => {
let mu = T::ONE / eta.sqrt();
T::from_f64(-0.5) * mu * mu * mu
}
}
}
pub(crate) fn variance<T: Scalar>(family: Family, nb_theta: f64, mu: T) -> T {
match family {
Family::Gaussian => T::ONE,
Family::Binomial { .. } => mu * (T::ONE - mu),
Family::Poisson { .. } => mu,
Family::Gamma { .. } => mu * mu,
Family::NegativeBinomial { .. } => mu + mu * mu / T::from_f64(nb_theta),
Family::InverseGaussian { .. } => mu * mu * mu,
}
}
pub(crate) fn dev_resid<T: Scalar>(family: Family, nb_theta: f64, y: f64, mu: T) -> T {
match family {
Family::Gaussian => {
let r = T::from_f64(y) - mu;
r * r
}
Family::Binomial { .. } => {
let a = if y > 0.0 {
T::from_f64(y) * (T::from_f64(y) / mu).ln()
} else {
T::ZERO
};
let b = if y < 1.0 {
T::from_f64(1.0 - y) * (T::from_f64(1.0 - y) / (T::ONE - mu)).ln()
} else {
T::ZERO
};
T::from_f64(2.0) * (a + b)
}
Family::Poisson { .. } => {
let t = if y > 0.0 {
T::from_f64(y) * (T::from_f64(y.ln()) - mu.ln())
} else {
T::ZERO
};
T::from_f64(2.0) * (t - (T::from_f64(y) - mu))
}
Family::Gamma { .. } => {
T::from_f64(2.0) * (-(T::from_f64(y) / mu).ln() + (T::from_f64(y) - mu) / mu)
}
Family::NegativeBinomial { .. } => {
let t = if y > 0.0 {
T::from_f64(y) * (T::from_f64(y.ln()) - mu.ln())
} else {
T::ZERO
};
T::from_f64(2.0)
* (t - (T::from_f64(y + nb_theta))
* (T::from_f64(y + nb_theta) / (mu + T::from_f64(nb_theta))).ln())
}
Family::InverseGaussian { .. } => {
let r = T::from_f64(y) - mu;
r * r / (mu * mu * T::from_f64(y))
}
}
}
pub(crate) fn gamma_aic<T: Scalar>(
y: &[f64],
mu: &[T],
dev: T,
n: usize,
prior_w: Option<&[f64]>,
) -> T {
let sum_w = prior_w.map_or(n as f64, |w| w[..n].iter().sum());
let disp = dev / T::from_f64(sum_w);
let a = T::ONE / disp; let ln_gamma_a = a.ln_gamma();
let mut s = T::ZERO;
for (i, (&yi, &mui)) in y.iter().zip(mu).take(n).enumerate() {
let scale = mui * disp; s += T::from_f64(prior_w.map_or(1.0, |w| w[i]))
* ((a - T::ONE) * T::from_f64(yi.ln())
- T::from_f64(yi) / scale
- a * scale.ln()
- ln_gamma_a);
}
T::from_f64(-2.0) * s + T::from_f64(2.0)
}
pub(crate) fn inv_gaussian_aic<T: Scalar>(
y: &[f64],
dev: T,
n: usize,
prior_w: Option<&[f64]>,
) -> T {
let sum_w = prior_w.map_or(n as f64, |w| w[..n].iter().sum());
let disp = dev / T::from_f64(sum_w);
let mut ln_y = 0.0;
for (i, &yi) in y.iter().take(n).enumerate() {
ln_y += prior_w.map_or(1.0, |w| w[i]) * yi.ln();
}
T::from_f64(sum_w) * ((T::from_f64(2.0 * std::f64::consts::PI) * disp).ln() + T::ONE)
+ T::from_f64(3.0 * ln_y)
+ T::from_f64(2.0)
}
pub(crate) fn saturated_loglik(
family: Family,
nb_theta: f64,
y: &[f64],
prior_w: Option<&[f64]>,
) -> f64 {
let lgamma = crate::simd_transcendental::ln_gamma;
match family {
Family::Gaussian => 0.0,
Family::Gamma { .. } | Family::InverseGaussian { .. } => f64::NAN,
Family::Binomial { .. } => {
let mut s = 0.0;
for (i, &yi) in y.iter().enumerate() {
let m = prior_w.map_or(1.0, |w| w[i]);
let succ = m * yi;
s += lgamma(m + 1.0) - lgamma(succ + 1.0) - lgamma(m - succ + 1.0);
if yi > 0.0 {
s += succ * yi.ln();
}
if yi < 1.0 {
s += (m - succ) * (1.0 - yi).ln();
}
}
s
}
Family::Poisson { .. } => {
let mut s = 0.0;
for (i, &yi) in y.iter().enumerate() {
let t = if yi > 0.0 { yi * yi.ln() } else { 0.0 };
s += prior_w.map_or(1.0, |w| w[i]) * (t - yi - lgamma(yi + 1.0));
}
s
}
Family::NegativeBinomial { .. } => {
let profile = crate::fit::nb_profile_loglik(y, y, nb_theta, prior_w);
let counts: f64 = y
.iter()
.enumerate()
.map(|(i, &yi)| prior_w.map_or(1.0, |w| w[i]) * lgamma(yi + 1.0))
.sum();
profile - counts
}
}
}
pub(crate) fn glmm_sigma_sq(
family: Family,
y: &[f64],
mu: &[f64],
u: &[f64],
prior_w: Option<&[f64]>,
) -> f64 {
match family {
Family::Gamma { .. } => {
let mut wrss = 0.0;
for (i, (&yi, &mui)) in y.iter().zip(mu).enumerate() {
let r = (yi - mui) / mui;
wrss += prior_w.map_or(1.0, |w| w[i]) * r * r;
}
let usq: f64 = u.iter().map(|&v| v * v).sum();
(wrss + usq) / y.len() as f64
}
_ => 1.0,
}
}
pub(crate) fn pearson_dispersion(
y: &[f64],
mu: &[f64],
family: Family,
nb_theta: f64,
n: usize,
p: usize,
prior_w: Option<&[f64]>,
) -> f64 {
let mut s = 0.0;
for i in 0..n {
let r = (y[i] - mu[i]) / variance(family, nb_theta, mu[i]).sqrt();
let pw = prior_w.map_or(1.0, |w| w[i]);
s += pw * r * r;
}
s / (n - p) as f64
}
pub(crate) fn is_canonical(family: Family) -> bool {
matches!(
family,
Family::Binomial {
link: BinomialLink::Logit
} | Family::Poisson { .. }
)
}
pub(crate) fn irls_weight_and_resid<T: Scalar>(
family: Family,
nb_theta: f64,
y: f64,
eta: T,
) -> (T, T, T) {
let mu = link_inv(family, eta);
let v = variance(family, nb_theta, mu);
if is_canonical(family) {
(mu, v, (T::from_f64(y) - mu) / v)
} else {
let dm = mu_eta(family, eta);
(mu, dm * dm / v, (T::from_f64(y) - mu) / dm)
}
}
pub(crate) fn observed_weight<T: Scalar>(
family: Family,
nb_theta: f64,
y: f64,
prior_w: f64,
eta: T,
mu: T,
w: T,
) -> T {
let dr = match family {
Family::Gaussian
| Family::Poisson { .. }
| Family::Binomial {
link: BinomialLink::Logit,
}
| Family::Gamma {
link: GammaLink::Inverse,
}
| Family::InverseGaussian {
link: InverseGaussianLink::InverseSquared,
} => return w,
Family::Binomial {
link: BinomialLink::Probit,
} => {
let phi = mu_eta(family, eta);
let v = mu * (T::ONE - mu);
-phi * (eta * v + phi * (T::ONE - T::from_f64(2.0) * mu)) / (v * v)
}
Family::Binomial {
link: BinomialLink::Cloglog,
} => {
let r = Scalar::exp(eta) / mu;
r * (T::ONE - mu_eta(family, eta) / mu)
}
Family::Gamma {
link: GammaLink::Log,
} => -(T::ONE / mu),
Family::NegativeBinomial { .. } => {
let th = T::from_f64(nb_theta);
let d = th + mu;
-th * mu / (d * d)
}
Family::InverseGaussian {
link: InverseGaussianLink::Log,
} => T::from_f64(-2.0) / (mu * mu),
};
w - T::from_f64(prior_w) * (T::from_f64(y) - mu) * dr
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BinomialLink, Family, GammaLink, InverseGaussianLink, NegBinomialLink, PoissonLink,
};
#[test]
fn observed_weight_matches_fd_of_score_factor() {
let fams = [
Family::Binomial {
link: BinomialLink::Probit,
},
Family::Binomial {
link: BinomialLink::Cloglog,
},
Family::Gamma {
link: GammaLink::Log,
},
Family::NegativeBinomial {
link: NegBinomialLink::Log,
},
Family::InverseGaussian {
link: InverseGaussianLink::Log,
},
Family::Gamma {
link: GammaLink::Inverse,
},
Family::InverseGaussian {
link: InverseGaussianLink::InverseSquared,
},
Family::Binomial {
link: BinomialLink::Logit,
},
Family::Poisson {
link: PoissonLink::Log,
},
];
let nb_theta = 2.5;
let r = |f: Family, eta: f64| mu_eta(f, eta) / variance(f, nb_theta, link_inv(f, eta));
for f in fams {
for eta in [0.3_f64, 0.9, 1.7] {
let h = 1e-5;
let fd = (r(f, eta + h) - r(f, eta - h)) / (2.0 * h);
let mu = link_inv(f, eta);
let w = 1.0;
let got = w - observed_weight(f, nb_theta, mu + 1.0, 1.0, eta, mu, w);
assert!(
(got - fd).abs() < 1e-7,
"{f:?} eta={eta}: dr/deta {got} vs fd {fd}"
);
}
}
}
#[test]
fn working_weight_dual_derivative_matches_fd() {
use crate::dual::Dual;
let fams = [
Family::Binomial {
link: BinomialLink::Logit,
},
Family::Binomial {
link: BinomialLink::Probit,
},
Family::Binomial {
link: BinomialLink::Cloglog,
},
Family::Poisson {
link: PoissonLink::Log,
},
Family::NegativeBinomial {
link: NegBinomialLink::Log,
},
];
let nb_theta = 2.5;
for f in fams {
for eta in [-1.3_f64, 0.2, 1.7] {
let h = 1e-5;
let wf = |e: f64| irls_weight_and_resid(f, nb_theta, 1.0, e).1;
let fd = (wf(eta + h) - wf(eta - h)) / (2.0 * h);
let e = Dual::<1> { v: eta, d: [1.0] };
let (mu, w, _) = irls_weight_and_resid(f, nb_theta, 1.0, e);
let got = w.d[0];
assert!(
(got - fd).abs() < 1e-7,
"{f:?} eta={eta}: dw/deta {got} vs fd {fd}"
);
match f {
Family::Binomial {
link: BinomialLink::Logit,
} => {
let hand = w.v * (1.0 - 2.0 * mu.v);
assert!((got - hand).abs() < 1e-12, "logit hand form");
}
Family::Poisson { .. } => {
assert!((got - mu.v).abs() < 1e-12, "poisson hand form")
}
_ => {}
}
}
}
}
#[test]
fn poisson_log_canonical_quantities() {
let f = Family::Poisson {
link: PoissonLink::Log,
};
let eta = 0.5_f64;
let mu = link_inv(f, eta);
assert!((mu - eta.exp()).abs() < 1e-12); assert!((variance(f, f64::NAN, mu) - mu).abs() < 1e-12); let (m, w, r) = irls_weight_and_resid(f, f64::NAN, 3.0, eta);
assert!((m - mu).abs() < 1e-12 && (w - mu).abs() < 1e-12);
assert!((r - (3.0 - mu) / mu).abs() < 1e-12);
}
#[test]
fn gamma_log_noncanonical_weight() {
let f = Family::Gamma {
link: GammaLink::Log,
};
let eta = 0.2_f64;
let mu = eta.exp();
let (_m, w, r) = irls_weight_and_resid(f, f64::NAN, 1.0, eta);
assert!((w - 1.0).abs() < 1e-12, "w={w}");
assert!((r - (1.0 - mu) / mu).abs() < 1e-12); }
#[test]
fn gamma_inverse_residual_sign() {
let f = Family::Gamma {
link: GammaLink::Inverse,
};
let eta = 0.5_f64; let mu = 1.0 / eta;
let (m, w, r) = irls_weight_and_resid(f, f64::NAN, 3.0, eta);
assert!((m - mu).abs() < 1e-12 && (w - mu * mu).abs() < 1e-12);
assert!((r - (-(3.0 - mu) / (mu * mu))).abs() < 1e-12, "r={r}");
}
#[test]
fn poisson_deviance_resid_zero_at_fit() {
let f = Family::Poisson {
link: PoissonLink::Log,
};
assert!(dev_resid(f, f64::NAN, 4.0, 4.0).abs() < 1e-10);
assert!(dev_resid(f, f64::NAN, 4.0, 2.0) > 0.0);
assert!((dev_resid(f, f64::NAN, 0.0, 1.0) - 2.0).abs() < 1e-10);
}
#[test]
fn nb_variance_uses_theta() {
let f = Family::NegativeBinomial {
link: NegBinomialLink::Log,
};
let mu = 3.0;
assert!((variance(f, 2.0, mu) - (mu + mu * mu / 2.0)).abs() < 1e-12);
}
#[test]
fn saturated_loglik_restores_exact_densities() {
let lg = crate::simd_transcendental::ln_gamma;
let y = [0.0, 1.0, 3.0, 7.0];
let mu = [0.5, 1.2, 2.5, 6.0];
let w = [1.0, 2.0, 1.0, 3.0];
let fp = Family::Poisson {
link: PoissonLink::Log,
};
let dev: f64 = (0..4)
.map(|i| w[i] * dev_resid(fp, f64::NAN, y[i], mu[i]))
.sum();
let direct: f64 = (0..4)
.map(|i| w[i] * (y[i] * mu[i].ln() - mu[i] - lg(y[i] + 1.0)))
.sum();
let restored = -0.5 * dev + saturated_loglik(fp, f64::NAN, &y, Some(&w));
assert!(
(restored - direct).abs() < 1e-10,
"poisson {restored} vs {direct}"
);
let th = 1.7;
let fnb = Family::NegativeBinomial {
link: NegBinomialLink::Log,
};
let dev: f64 = (0..4).map(|i| w[i] * dev_resid(fnb, th, y[i], mu[i])).sum();
let direct: f64 = (0..4)
.map(|i| {
w[i] * (lg(y[i] + th) - lg(th) - lg(y[i] + 1.0)
+ th * (th / (th + mu[i])).ln()
+ y[i] * (mu[i] / (th + mu[i])).ln())
})
.sum();
let restored = -0.5 * dev + saturated_loglik(fnb, th, &y, Some(&w));
assert!(
(restored - direct).abs() < 1e-10,
"nb {restored} vs {direct}"
);
let fb = Family::Binomial {
link: BinomialLink::Logit,
};
let yb = [0.0, 0.5, 2.0 / 3.0, 1.0];
let m = [2.0, 4.0, 3.0, 5.0];
let mub = [0.3, 0.55, 0.6, 0.8];
let dev: f64 = (0..4)
.map(|i| m[i] * dev_resid(fb, f64::NAN, yb[i], mub[i]))
.sum();
let direct: f64 = (0..4)
.map(|i| {
let s = m[i] * yb[i];
lg(m[i] + 1.0) - lg(s + 1.0) - lg(m[i] - s + 1.0)
+ s * mub[i].ln()
+ (m[i] - s) * (1.0 - mub[i]).ln()
})
.sum();
let restored = -0.5 * dev + saturated_loglik(fb, f64::NAN, &yb, Some(&m));
assert!(
(restored - direct).abs() < 1e-10,
"binomial {restored} vs {direct}"
);
}
#[test]
fn probit_mu_eta_is_normal_pdf() {
let f = Family::Binomial {
link: BinomialLink::Probit,
};
assert!((link_inv(f, 0.0) - 0.5).abs() < 1e-13);
assert!((mu_eta(f, 0.0) - (1.0 / (2.0 * std::f64::consts::PI).sqrt())).abs() < 1e-12);
}
#[test]
fn cloglog_link_and_derivative() {
let f = Family::Binomial {
link: BinomialLink::Cloglog,
};
let mu0 = link_inv(f, 0.0);
assert!((mu0 - (1.0 - (-1.0f64).exp())).abs() < 1e-15, "μ(0)={mu0}");
let d0 = mu_eta(f, 0.0);
assert!((d0 - (-1.0f64).exp()).abs() < 1e-15, "dμ/dη(0)={d0}");
assert!(link_inv(f, 2.0) > 0.999);
assert!(link_inv(f, -2.0) < 0.13);
let eta = 0.4_f64;
let mu = link_inv(f, eta);
let dm = mu_eta(f, eta);
let (m, w, r) = irls_weight_and_resid(f, f64::NAN, 1.0, eta);
assert!((m - mu).abs() < 1e-15);
assert!((w - dm * dm / (mu * (1.0 - mu))).abs() < 1e-12, "w={w}");
assert!((r - (1.0 - mu) / dm).abs() < 1e-12, "r={r}");
}
#[test]
fn cloglog_eta_is_clamped_above_at_ln_eta_max() {
let f = Family::Binomial {
link: BinomialLink::Cloglog,
};
let mu = link_inv(f, 1e6);
assert!(mu.is_finite() && mu <= 1.0 - PROB_EPS, "μ={mu}");
assert!(mu_eta(f, 1e6).is_finite());
let logit = Family::Binomial {
link: BinomialLink::Logit,
};
assert_eq!(clamp_eta(logit, 1e6), 1e6);
}
#[test]
fn inverse_gaussian_inverse_squared_quantities() {
let f = Family::InverseGaussian {
link: InverseGaussianLink::InverseSquared,
};
let eta = 0.25_f64; let mu = link_inv(f, eta);
assert!((mu - 2.0).abs() < 1e-12, "μ={mu}");
assert!((variance(f, f64::NAN, mu) - 8.0).abs() < 1e-12);
let dm = mu_eta(f, eta);
assert!((dm - (-4.0)).abs() < 1e-12, "dμ/dη={dm}");
let (m, w, r) = irls_weight_and_resid(f, f64::NAN, 3.0, eta);
assert!((m - mu).abs() < 1e-12 && (w - 2.0).abs() < 1e-12, "w={w}");
assert!((r - (3.0 - 2.0) / -4.0).abs() < 1e-12, "r={r}");
assert!(eta_infeasible(f, 0.0));
assert!(eta_infeasible(f, -1.0));
assert!(!eta_infeasible(f, 1e-3));
}
#[test]
fn inverse_gaussian_log_quantities() {
let f = Family::InverseGaussian {
link: InverseGaussianLink::Log,
};
let eta = 0.7_f64;
let mu = eta.exp();
assert!((link_inv(f, eta) - mu).abs() < 1e-12);
let (_m, w, r) = irls_weight_and_resid(f, f64::NAN, 4.0, eta);
assert!((w - 1.0 / mu).abs() < 1e-12, "w={w}");
assert!((r - (4.0 - mu) / mu).abs() < 1e-12, "r={r}");
assert!(!eta_infeasible(f, -50.0)); }
#[test]
fn inverse_gaussian_deviance_resid() {
let f = Family::InverseGaussian {
link: InverseGaussianLink::Log,
};
assert!(dev_resid(f, f64::NAN, 2.0, 2.0).abs() < 1e-14);
let d = dev_resid(f, f64::NAN, 4.0, 2.0);
assert!(
(d - (4.0 - 2.0f64).powi(2) / (4.0 * 4.0)).abs() < 1e-14,
"d={d}"
);
assert!(d > 0.0);
}
#[test]
fn inverse_gaussian_saturated_loglik_is_nan() {
let f = Family::InverseGaussian {
link: InverseGaussianLink::Log,
};
assert!(saturated_loglik(f, f64::NAN, &[1.0, 2.0], None).is_nan());
}
#[test]
fn inv_gaussian_aic_matches_r_formula() {
let y = [1.5_f64, 2.0, 0.75, 3.25];
let n = y.len();
let dev = 0.42_f64;
let got = inv_gaussian_aic(&y, dev, n, None);
let disp = dev / n as f64;
let want = n as f64 * ((2.0 * std::f64::consts::PI * disp).ln() + 1.0)
+ 3.0 * y.iter().map(|v| v.ln()).sum::<f64>()
+ 2.0;
assert!((got - want).abs() < 1e-12, "got {got} want {want}");
let w = [1.0_f64, 2.0, 0.5, 1.5];
let sw: f64 = w.iter().sum();
let gotw = inv_gaussian_aic(&y, dev, n, Some(&w));
let dispw = dev / sw;
let wantw = sw * ((2.0 * std::f64::consts::PI * dispw).ln() + 1.0)
+ 3.0 * y.iter().zip(w).map(|(v, wi)| wi * v.ln()).sum::<f64>()
+ 2.0;
assert!((gotw - wantw).abs() < 1e-12);
}
}