#![allow(clippy::needless_range_loop)]
use crate::errors::EdgeErrors;
const DEFAULT_MAX_ITER: usize = 50;
const DEFAULT_MAX_BACKTRACK: usize = 10;
const DEFAULT_EPS: f64 = 1e-6;
const GRADIENT_TOLERANCE: f64 = 0.01;
const STEP_CUTOFF: f64 = 40.0;
const EIGENVALUE_CUTOFF: f64 = 1e-8;
const JACOBI_SWEEPS: usize = 30;
pub const CONV_SUCCESS: i32 = 1;
pub const CONV_CRITICAL_POINT: i32 = -10;
pub const CONV_MAX_ITER: i32 = -20;
pub const CONV_SINGULAR: i32 = -25;
pub const CONV_NOT_FINITE: i32 = -30;
pub const CONV_STALLED: i32 = -40;
pub const CONV_AT_BOUND: i32 = -60;
#[derive(Clone, Copy, Debug)]
pub struct PmlData<'a> {
pub design: &'a [f64],
pub offset: &'a [f64],
pub counts: &'a [f64],
pub cell_index: &'a [usize],
pub subject_start: &'a [usize],
pub subject_total: &'a [f64],
}
impl PmlData<'_> {
pub fn n_cells(&self) -> usize {
self.offset.len()
}
pub fn n_subjects(&self) -> usize {
self.subject_start.len().saturating_sub(1)
}
pub fn n_beta(&self) -> usize {
let n = self.n_cells();
self.design.len().checked_div(n).unwrap_or(0)
}
fn validate(&self) -> Result<(), EdgeErrors> {
let n_cells = self.n_cells();
if n_cells == 0 {
return Err(EdgeErrors::MustBePositive("n_cells".to_string()));
}
let k = self.n_subjects();
if k == 0 {
return Err(EdgeErrors::MustBePositive("n_subjects".to_string()));
}
let n_beta = self.n_beta();
if n_beta == 0 || n_beta * n_cells != self.design.len() {
return Err(EdgeErrors::LengthMismatch {
name: "design",
expected: n_cells * n_beta.max(1),
got: self.design.len(),
});
}
if self.counts.len() != self.cell_index.len() {
return Err(EdgeErrors::LengthMismatch {
name: "cell_index",
expected: self.counts.len(),
got: self.cell_index.len(),
});
}
if self.subject_total.len() != k {
return Err(EdgeErrors::LengthMismatch {
name: "subject_total",
expected: k,
got: self.subject_total.len(),
});
}
if self.subject_start[0] != 0 || self.subject_start[k] != n_cells {
return Err(EdgeErrors::InvalidArgument(format!(
"subject_start must run from 0 to {n_cells}; got {} to {}.",
self.subject_start[0], self.subject_start[k]
)));
}
for s in 0..k {
if self.subject_start[s] >= self.subject_start[s + 1] {
return Err(EdgeErrors::SubjectsNotContiguous { subject: s });
}
}
let mut previous: Option<usize> = None;
for &c in self.cell_index {
if c >= n_cells {
return Err(EdgeErrors::InvalidArgument(format!(
"cell_index {c} is out of range for {n_cells} cells."
)));
}
if previous.is_some_and(|p| c <= p) {
return Err(EdgeErrors::InvalidArgument(
"cell_index must be strictly increasing.".to_string(),
));
}
previous = Some(c);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
pub struct PmlVariance {
pub subject: f64,
pub cell: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct PmlParams {
pub reml: bool,
pub eps: f64,
pub ord: u32,
pub max_iter: usize,
pub max_backtrack: usize,
}
impl Default for PmlParams {
fn default() -> Self {
Self {
reml: false,
eps: DEFAULT_EPS,
ord: 1,
max_iter: DEFAULT_MAX_ITER,
max_backtrack: DEFAULT_MAX_BACKTRACK,
}
}
}
#[derive(Clone, Debug)]
pub struct PmlResult {
pub beta: Vec<f64>,
pub log_w: Vec<f64>,
pub information: Vec<f64>,
pub log_likelihood: f64,
pub log_likelihood_prev: f64,
pub log_det: f64,
pub second_order: f64,
pub convergence: i32,
pub iterations: usize,
pub backtracks: usize,
}
#[derive(Clone, Debug)]
pub struct PmlObjective {
pub objective: f64,
pub gradient: Vec<f64>,
}
#[derive(Clone, Copy, Debug)]
enum Penalty {
Gamma {
alpha: f64,
lambda: f64,
},
LogNormal {
alpha: f64,
},
}
impl Penalty {
fn accumulate(&self, log_likelihood: f64, log_w: &[f64], w: &[f64]) -> f64 {
match *self {
Penalty::Gamma { alpha, lambda } => {
let mut sum_log_w = 0.0;
for &l in log_w {
sum_log_w += l;
}
let mut sum_w = 0.0;
for &x in w {
sum_w += x;
}
log_likelihood + (alpha * sum_log_w - lambda * sum_w)
}
Penalty::LogNormal { alpha } => {
let mut quad = 0.0;
for &l in log_w {
quad += l * l;
}
log_likelihood - quad / alpha / 2.0 - (log_w.len() as f64) / 2.0 * alpha.ln()
}
}
}
fn gradient(&self, log_w_s: f64, w_s: f64) -> f64 {
match *self {
Penalty::Gamma { alpha, lambda } => alpha - lambda * w_s,
Penalty::LogNormal { alpha } => -log_w_s / alpha,
}
}
fn curvature(&self, w_s: f64) -> f64 {
match *self {
Penalty::Gamma { lambda, .. } => lambda * w_s,
Penalty::LogNormal { alpha } => 1.0 / alpha,
}
}
}
pub fn opt_pml(
data: &PmlData<'_>,
beta_init: &[f64],
variance: &PmlVariance,
params: Option<PmlParams>,
) -> Result<PmlResult, EdgeErrors> {
let exps = variance.subject.exp();
if !(exps.is_finite() && exps > 1.0) {
return Err(EdgeErrors::InvalidArgument(format!(
"PmlVariance::subject must be finite and strictly positive for opt_pml; got {}.",
variance.subject
)));
}
let penalty = Penalty::Gamma {
alpha: 1.0 / (exps - 1.0),
lambda: 1.0 / (exps.sqrt() * (exps - 1.0)),
};
optimise(
data,
beta_init,
penalty,
variance.cell,
params.unwrap_or_default(),
)
}
pub fn opt_pml_nbm(
data: &PmlData<'_>,
beta_init: &[f64],
variance: &PmlVariance,
params: Option<PmlParams>,
) -> Result<PmlResult, EdgeErrors> {
if !(variance.subject.is_finite() && variance.subject > 0.0) {
return Err(EdgeErrors::InvalidArgument(format!(
"PmlVariance::subject must be finite and strictly positive for opt_pml_nbm; got {}.",
variance.subject
)));
}
let penalty = Penalty::LogNormal {
alpha: variance.subject,
};
optimise(
data,
beta_init,
penalty,
variance.cell,
params.unwrap_or_default(),
)
}
pub fn pml_log_likelihood_gradient(
data: &PmlData<'_>,
beta: &[f64],
log_w: &[f64],
variance: &PmlVariance,
) -> Result<PmlObjective, EdgeErrors> {
data.validate()?;
let n_beta = data.n_beta();
let k = data.n_subjects();
if beta.len() != n_beta {
return Err(EdgeErrors::LengthMismatch {
name: "beta",
expected: n_beta,
got: beta.len(),
});
}
if log_w.len() != k {
return Err(EdgeErrors::LengthMismatch {
name: "log_w",
expected: k,
got: log_w.len(),
});
}
let alpha = variance.subject;
let gamma = variance.cell;
let penalty = Penalty::Gamma {
alpha,
lambda: alpha,
};
let mut work = Workspace::new(data);
work.seed(data, gamma);
let log_likelihood = work.evaluate(data, beta, log_w, gamma, penalty);
work.gradient_only(data, gamma, penalty, log_w);
let mut gradient = Vec::with_capacity(n_beta + k);
gradient.extend(work.db.iter().map(|v| -v));
gradient.extend(work.dw.iter().map(|v| -v));
Ok(PmlObjective {
objective: -log_likelihood,
gradient,
})
}
pub fn check_convergence(
result: &PmlResult,
params: &PmlParams,
variance_at_bound: bool,
initial: i32,
) -> i32 {
let mut code = initial;
if code == CONV_SUCCESS {
code = if variance_at_bound {
CONV_AT_BOUND
} else if result.log_likelihood.is_nan() {
CONV_NOT_FINITE
} else if result.iterations == params.max_iter {
CONV_MAX_ITER
} else if result.backtracks == params.max_backtrack + 1 {
CONV_CRITICAL_POINT
} else if result.backtracks == params.max_backtrack + 2 {
CONV_STALLED
} else {
CONV_SUCCESS
};
}
let n_beta = result.beta.len();
if n_beta > 1 && min_eigenvalue(&result.information, n_beta) < EIGENVALUE_CUTOFF {
code = CONV_SINGULAR;
}
code
}
struct Workspace {
extb: Vec<f64>,
extbphil: Vec<f64>,
phi: Vec<f64>,
gstar: Vec<f64>,
deriv: Vec<f64>,
x_phi: Vec<f64>,
w: Vec<f64>,
yx: Vec<f64>,
db: Vec<f64>,
dw: Vec<f64>,
vw: Vec<f64>,
vwb: Vec<f64>,
vb: Vec<f64>,
vb2: Vec<f64>,
factor: Vec<f64>,
perm: Vec<usize>,
tmp: Vec<f64>,
step_beta: Vec<f64>,
step_log_w: Vec<f64>,
dwvw: Vec<f64>,
damp_beta: Vec<f64>,
damp_log_w: Vec<f64>,
new_beta: Vec<f64>,
new_log_w: Vec<f64>,
third: Vec<f64>,
fourth: Vec<f64>,
}
impl Workspace {
fn new(data: &PmlData<'_>) -> Self {
let n_cells = data.n_cells();
let k = data.n_subjects();
let nb = data.n_beta();
Self {
extb: vec![0.0; n_cells],
extbphil: vec![0.0; n_cells],
phi: vec![0.0; n_cells],
gstar: vec![0.0; n_cells],
deriv: vec![0.0; n_cells],
x_phi: vec![0.0; n_cells * nb],
w: vec![0.0; k],
yx: vec![0.0; nb],
db: vec![0.0; nb],
dw: vec![0.0; k],
vw: vec![0.0; k],
vwb: vec![0.0; k * nb],
vb: vec![0.0; nb * nb],
vb2: vec![0.0; nb * nb],
factor: vec![0.0; nb * nb],
perm: vec![0; nb],
tmp: vec![0.0; nb],
step_beta: vec![0.0; nb],
step_log_w: vec![0.0; k],
dwvw: vec![0.0; k],
damp_beta: vec![0.0; nb],
damp_log_w: vec![0.0; k],
new_beta: vec![0.0; nb],
new_log_w: vec![0.0; k],
third: vec![0.0; k],
fourth: vec![0.0; k],
}
}
fn seed(&mut self, data: &PmlData<'_>, gamma: f64) {
let nb = data.n_beta();
self.gstar.fill(gamma);
for (i, &c) in data.cell_index.iter().enumerate() {
self.gstar[c] += data.counts[i];
}
self.yx.fill(0.0);
for (i, &c) in data.cell_index.iter().enumerate() {
let row = &data.design[c * nb..(c + 1) * nb];
let y = data.counts[i];
for j in 0..nb {
self.yx[j] += row[j] * y;
}
}
}
fn evaluate(
&mut self,
data: &PmlData<'_>,
beta: &[f64],
log_w: &[f64],
gamma: f64,
penalty: Penalty,
) -> f64 {
let nb = data.n_beta();
let n_cells = data.n_cells();
let k = data.n_subjects();
for r in 0..n_cells {
let row = &data.design[r * nb..(r + 1) * nb];
let mut acc = 0.0;
for j in 0..nb {
acc += row[j] * beta[j];
}
self.extb[r] = data.offset[r] + acc;
}
let mut log_likelihood = 0.0;
for (i, &c) in data.cell_index.iter().enumerate() {
log_likelihood += self.extb[c] * data.counts[i];
}
for s in 0..k {
log_likelihood += log_w[s] * data.subject_total[s];
}
for s in 0..k {
for e in &mut self.extb[data.subject_start[s]..data.subject_start[s + 1]] {
*e += log_w[s];
}
}
for e in &mut self.extb[..n_cells] {
*e = e.exp();
}
let mut sum_phil = 0.0;
for r in 0..n_cells {
self.extbphil[r] = (self.extb[r] + gamma).ln();
sum_phil += self.extbphil[r];
}
log_likelihood -= gamma * sum_phil;
for (i, &c) in data.cell_index.iter().enumerate() {
log_likelihood -= data.counts[i] * self.extbphil[c];
}
for s in 0..k {
self.w[s] = log_w[s].exp();
}
penalty.accumulate(log_likelihood, log_w, &self.w)
}
fn gradient_only(&mut self, data: &PmlData<'_>, gamma: f64, penalty: Penalty, log_w: &[f64]) {
let nb = data.n_beta();
let n_cells = data.n_cells();
let k = data.n_subjects();
for r in 0..n_cells {
self.phi[r] = self.gstar[r] / (1.0 + gamma / self.extb[r]);
}
self.db.copy_from_slice(&self.yx);
for r in 0..n_cells {
let row = &data.design[r * nb..(r + 1) * nb];
let p = self.phi[r];
for j in 0..nb {
self.db[j] -= row[j] * p;
}
}
for s in 0..k {
let mut acc = 0.0;
for r in data.subject_start[s]..data.subject_start[s + 1] {
acc += self.phi[r];
}
self.dw[s] = (data.subject_total[s] - acc) + penalty.gradient(log_w[s], self.w[s]);
}
}
fn curvature(&mut self, data: &PmlData<'_>, gamma: f64, penalty: Penalty) {
let nb = data.n_beta();
let n_cells = data.n_cells();
let k = data.n_subjects();
for r in 0..n_cells {
self.phi[r] /= self.extb[r] + gamma;
}
for s in 0..k {
let mut acc = 0.0;
for r in data.subject_start[s]..data.subject_start[s + 1] {
acc += self.phi[r];
}
self.vw[s] = gamma * acc + penalty.curvature(self.w[s]);
}
for r in 0..n_cells {
let row = &data.design[r * nb..(r + 1) * nb];
let p = self.phi[r];
let out = &mut self.x_phi[r * nb..(r + 1) * nb];
for j in 0..nb {
out[j] = row[j] * p;
}
}
for s in 0..k {
let dst = &mut self.vwb[s * nb..(s + 1) * nb];
dst.fill(0.0);
for r in data.subject_start[s]..data.subject_start[s + 1] {
let src = &self.x_phi[r * nb..(r + 1) * nb];
for j in 0..nb {
dst[j] += src[j];
}
}
for v in dst.iter_mut() {
*v *= gamma;
}
}
self.vb.fill(0.0);
for r in 0..n_cells {
let row = &data.design[r * nb..(r + 1) * nb];
let scaled = &self.x_phi[r * nb..(r + 1) * nb];
for i in 0..nb {
for j in i..nb {
self.vb[i * nb + j] += row[i] * scaled[j];
}
}
}
for i in 0..nb {
for j in i..nb {
let v = gamma * self.vb[i * nb + j];
self.vb[i * nb + j] = v;
self.vb[j * nb + i] = v;
}
}
for s in 0..k {
let scale = 1.0 / self.vw[s].sqrt();
for v in &mut self.vwb[s * nb..(s + 1) * nb] {
*v *= scale;
}
}
for i in 0..nb {
for j in 0..nb {
let mut acc = 0.0;
for s in 0..k {
acc += self.vwb[s * nb + i] * self.vwb[s * nb + j];
}
self.vb2[i * nb + j] = self.vb[i * nb + j] - acc;
}
}
for s in 0..k {
let scale = self.vw[s].sqrt();
for v in &mut self.vwb[s * nb..(s + 1) * nb] {
*v *= scale;
}
}
}
}
fn optimise(
data: &PmlData<'_>,
beta_init: &[f64],
penalty: Penalty,
gamma: f64,
params: PmlParams,
) -> Result<PmlResult, EdgeErrors> {
data.validate()?;
let nb = data.n_beta();
let k = data.n_subjects();
if beta_init.len() != nb {
return Err(EdgeErrors::LengthMismatch {
name: "beta_init",
expected: nb,
got: beta_init.len(),
});
}
if !(gamma.is_finite() && gamma > 0.0) {
return Err(EdgeErrors::InvalidDispersion(gamma));
}
let mut beta = beta_init.to_vec();
let mut log_w = vec![0.0; k];
let mut work = Workspace::new(data);
work.seed(data, gamma);
let mut log_likelihood = work.evaluate(data, &beta, &log_w, gamma, penalty);
let mut log_likelihood_prev = 0.0;
let mut likdif = 0.0;
let mut step = 0usize;
let mut backtracks = 0usize;
while step == 0 || (likdif > params.eps && step < params.max_iter) {
step += 1;
work.damp_beta.fill(1.0);
work.damp_log_w.fill(1.0);
work.gradient_only(data, gamma, penalty, &log_w);
work.curvature(data, gamma, penalty);
for s in 0..k {
work.dwvw[s] = work.dw[s] / work.vw[s];
}
for j in 0..nb {
let mut acc = 0.0;
for s in 0..k {
acc += work.vwb[s * nb + j] * work.dwvw[s];
}
work.step_beta[j] = work.db[j] - acc;
}
work.factor.copy_from_slice(&work.vb2);
ldlt_solve(
&mut work.factor,
&mut work.step_beta,
nb,
&mut work.perm,
&mut work.tmp,
);
for s in 0..k {
let mut acc = 0.0;
for j in 0..nb {
acc += work.vwb[s * nb + j] * work.step_beta[j];
}
work.step_log_w[s] = work.dwvw[s] - acc / work.vw[s];
}
for j in 0..nb {
work.new_beta[j] = beta[j] + work.step_beta[j];
}
for s in 0..k {
work.new_log_w[s] = log_w[s] + work.step_log_w[s];
}
log_likelihood_prev = log_likelihood;
log_likelihood = evaluate_trial(&mut work, data, gamma, penalty);
likdif = log_likelihood - log_likelihood_prev;
backtracks = 0;
let mut min_step = STEP_CUTOFF;
while likdif < 0.0 || log_likelihood.is_infinite() {
backtracks += 1;
min_step /= 2.0;
if backtracks > params.max_backtrack {
likdif = 0.0;
log_likelihood = log_likelihood_prev;
let grad_beta = work.db.iter().fold(0.0f64, |m, v| m.max(v.abs()));
let grad_log_w = work.dw.iter().fold(0.0f64, |m, v| m.max(v.abs()));
if grad_beta > GRADIENT_TOLERANCE || grad_log_w > GRADIENT_TOLERANCE {
backtracks += 1;
}
break;
}
for j in 0..nb {
let d = work.step_beta[j];
if d < STEP_CUTOFF && d > -STEP_CUTOFF {
work.damp_beta[j] /= 2.0;
work.new_beta[j] = beta[j] + d * work.damp_beta[j];
} else if d > 0.0 {
work.new_beta[j] = beta[j] + min_step;
} else {
work.new_beta[j] = beta[j] - min_step;
}
}
for s in 0..k {
let d = work.step_log_w[s];
if d < STEP_CUTOFF && d > -STEP_CUTOFF {
work.damp_log_w[s] /= 2.0;
work.new_log_w[s] = log_w[s] + d * work.damp_log_w[s];
} else if d > 0.0 {
work.new_log_w[s] = log_w[s] + min_step;
} else {
work.new_log_w[s] = log_w[s] - min_step;
}
}
log_likelihood = evaluate_trial(&mut work, data, gamma, penalty);
likdif = log_likelihood - log_likelihood_prev;
}
beta.copy_from_slice(&work.new_beta);
log_w.copy_from_slice(&work.new_log_w);
}
let mut log_det = 0.0;
for s in 0..k {
log_det += work.vw[s].abs().ln();
}
if params.reml {
work.factor.copy_from_slice(&work.vb2);
let det = determinant(&mut work.factor, nb, &mut work.perm);
log_det += det.abs().ln();
}
let second_order = match penalty {
Penalty::Gamma { lambda, .. } if params.ord > 1 => {
laplace_correction(&mut work, data, gamma, lambda, params.ord)
}
_ => 0.0,
};
let mut result = PmlResult {
beta,
log_w,
information: work.vb2.clone(),
log_likelihood,
log_likelihood_prev,
log_det,
second_order,
convergence: CONV_SUCCESS,
iterations: step,
backtracks,
};
result.convergence = check_convergence(&result, ¶ms, false, CONV_SUCCESS);
Ok(result)
}
fn evaluate_trial(work: &mut Workspace, data: &PmlData<'_>, gamma: f64, penalty: Penalty) -> f64 {
let beta = std::mem::take(&mut work.new_beta);
let log_w = std::mem::take(&mut work.new_log_w);
let value = work.evaluate(data, &beta, &log_w, gamma, penalty);
work.new_beta = beta;
work.new_log_w = log_w;
value
}
fn laplace_correction(
work: &mut Workspace,
data: &PmlData<'_>,
gamma: f64,
lambda: f64,
ord: u32,
) -> f64 {
let n_cells = data.n_cells();
let k = data.n_subjects();
let mut second = 0.0;
for r in 0..n_cells {
work.phi[r] = work.gstar[r] / (1.0 + gamma / work.extb[r]);
work.extbphil[r] = work.extb[r] + gamma;
work.phi[r] /= work.extbphil[r];
}
for s in 0..k {
let mut acc = 0.0;
for r in data.subject_start[s]..data.subject_start[s + 1] {
acc += work.phi[r];
}
work.vw[s] = gamma * acc + lambda * work.w[s];
}
for r in 0..n_cells {
work.phi[r] /= work.extbphil[r];
work.deriv[r] = work.phi[r] * (gamma - work.extb[r]);
}
for s in 0..k {
let mut acc = 0.0;
for r in data.subject_start[s]..data.subject_start[s + 1] {
acc += work.deriv[r];
}
work.third[s] = gamma * acc + lambda * work.w[s];
}
let mut acc = 0.0;
for s in 0..k {
let v = work.vw[s];
acc += work.third[s] * work.third[s] / (v * v * v);
}
second += 5.0 * acc / 24.0;
if ord > 2 {
for r in 0..n_cells {
work.phi[r] /= work.extbphil[r];
let e = work.extb[r];
work.deriv[r] = work.phi[r] * (gamma * gamma + e * e - (4.0 * gamma) * e);
}
for s in 0..k {
let mut acc = 0.0;
for r in data.subject_start[s]..data.subject_start[s + 1] {
acc += work.deriv[r];
}
work.fourth[s] = gamma * acc + lambda * work.w[s];
}
let mut acc = 0.0;
for s in 0..k {
let v = work.vw[s];
acc += work.fourth[s] / (v * v);
}
second -= acc / 8.0;
for r in 0..n_cells {
work.phi[r] /= work.extbphil[r];
let e = work.extb[r];
let e2 = e * e;
work.deriv[r] = work.phi[r]
* (gamma * gamma * gamma - (11.0 * gamma * gamma) * e + (11.0 * gamma) * e2
- e2 * e);
}
for s in 0..k {
let mut acc = 0.0;
for r in data.subject_start[s]..data.subject_start[s + 1] {
acc += work.deriv[r];
}
work.fourth[s] = gamma * acc + lambda * work.w[s];
}
let mut acc = 0.0;
for s in 0..k {
let vs = work.vw[s] * work.vw[s];
acc += work.fourth[s] * work.third[s] / (vs * vs);
}
second += 7.0 * acc / 48.0;
}
second
}
fn ldlt_solve(a: &mut [f64], b: &mut [f64], n: usize, perm: &mut [usize], tmp: &mut [f64]) {
for step in 0..n {
let mut pivot = step;
let mut best = a[step * n + step].abs();
for i in (step + 1)..n {
let v = a[i * n + i].abs();
if v > best {
best = v;
pivot = i;
}
}
perm[step] = pivot;
if pivot != step {
for j in 0..step {
a.swap(step * n + j, pivot * n + j);
}
for i in (pivot + 1)..n {
a.swap(i * n + step, i * n + pivot);
}
a.swap(step * n + step, pivot * n + pivot);
for i in (step + 1)..pivot {
a.swap(i * n + step, pivot * n + i);
}
}
if step > 0 {
for j in 0..step {
tmp[j] = a[j * n + j] * a[step * n + j];
}
let mut acc = 0.0;
for j in 0..step {
acc += a[step * n + j] * tmp[j];
}
a[step * n + step] -= acc;
for i in (step + 1)..n {
let mut acc = 0.0;
for j in 0..step {
acc += a[i * n + j] * tmp[j];
}
a[i * n + step] -= acc;
}
}
let d = a[step * n + step];
if d != 0.0 {
for i in (step + 1)..n {
a[i * n + step] /= d;
}
}
}
for step in 0..n {
b.swap(step, perm[step]);
}
for i in 0..n {
let mut acc = b[i];
for j in 0..i {
acc -= a[i * n + j] * b[j];
}
b[i] = acc;
}
for i in 0..n {
let d = a[i * n + i];
b[i] = if d.abs() > f64::MIN_POSITIVE {
b[i] / d
} else {
0.0
};
}
for i in (0..n).rev() {
let mut acc = b[i];
for j in (i + 1)..n {
acc -= a[j * n + i] * b[j];
}
b[i] = acc;
}
for step in (0..n).rev() {
b.swap(step, perm[step]);
}
}
fn determinant(a: &mut [f64], n: usize, perm: &mut [usize]) -> f64 {
let mut det = 1.0;
perm.fill(0);
for step in 0..n {
let mut pivot = step;
let mut best = a[step * n + step].abs();
for i in (step + 1)..n {
let v = a[i * n + step].abs();
if v > best {
best = v;
pivot = i;
}
}
if pivot != step {
for j in 0..n {
a.swap(step * n + j, pivot * n + j);
}
det = -det;
}
let d = a[step * n + step];
det *= d;
if d == 0.0 {
return 0.0;
}
for i in (step + 1)..n {
let factor = a[i * n + step] / d;
a[i * n + step] = factor;
for j in (step + 1)..n {
a[i * n + j] -= factor * a[step * n + j];
}
}
}
det
}
fn min_eigenvalue(matrix: &[f64], n: usize) -> f64 {
if n == 0 {
return f64::NAN;
}
if n == 1 {
return matrix[0];
}
let mut a = matrix.to_vec();
let mut frobenius = 0.0;
for &v in &a {
frobenius += v * v;
}
let tolerance = (frobenius * f64::EPSILON * f64::EPSILON).max(f64::MIN_POSITIVE);
for _ in 0..JACOBI_SWEEPS {
let mut off = 0.0;
for p in 0..n {
for q in (p + 1)..n {
off += a[p * n + q] * a[p * n + q];
}
}
if off <= tolerance {
break;
}
for p in 0..n {
for q in (p + 1)..n {
let apq = a[p * n + q];
if apq == 0.0 {
continue;
}
let theta = (a[q * n + q] - a[p * n + p]) / (2.0 * apq);
let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
for r in 0..n {
let arp = a[r * n + p];
let arq = a[r * n + q];
a[r * n + p] = c * arp - s * arq;
a[r * n + q] = s * arp + c * arq;
}
for r in 0..n {
let apr = a[p * n + r];
let aqr = a[q * n + r];
a[p * n + r] = c * apr - s * aqr;
a[q * n + r] = s * apr + c * aqr;
}
}
}
}
(0..n).map(|i| a[i * n + i]).fold(f64::INFINITY, f64::min)
}
#[cfg(test)]
#[allow(clippy::excessive_precision)]
#[allow(clippy::type_complexity)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const DESIGN: [f64; 24] = [
1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
1.0, 0.0, 1.0, 1.0, 1.0, 1.0,
];
const OFFSET: [f64; 12] = [
0.25, -0.5, 0.75, 0.0, 0.5, -0.25, 0.125, 0.375, -0.125, 0.25, 0.0, -0.375,
];
const SUBJECT_START: [usize; 4] = [0, 4, 9, 12];
const COUNTS: [f64; 8] = [3.0, 5.0, 2.0, 4.0, 1.0, 7.0, 2.0, 6.0];
const CELL_INDEX: [usize; 8] = [0, 2, 3, 5, 6, 7, 9, 10];
const SUBJECT_TOTAL: [f64; 3] = [10.0, 12.0, 8.0];
const COUNTS_B: [f64; 5] = [3.0, 5.0, 2.0, 2.0, 6.0];
const CELL_INDEX_B: [usize; 5] = [0, 2, 3, 9, 10];
const SUBJECT_TOTAL_B: [f64; 3] = [10.0, 0.0, 8.0];
const TOL: f64 = 1e-11;
fn fixture_a() -> PmlData<'static> {
PmlData {
design: &DESIGN,
offset: &OFFSET,
counts: &COUNTS,
cell_index: &CELL_INDEX,
subject_start: &SUBJECT_START,
subject_total: &SUBJECT_TOTAL,
}
}
fn fixture_b() -> PmlData<'static> {
PmlData {
design: &DESIGN,
offset: &OFFSET,
counts: &COUNTS_B,
cell_index: &CELL_INDEX_B,
subject_start: &SUBJECT_START,
subject_total: &SUBJECT_TOTAL_B,
}
}
fn assert_slice(got: &[f64], want: &[f64]) {
assert_eq!(got.len(), want.len());
for (g, w) in got.iter().zip(want) {
assert_relative_eq!(*g, *w, max_relative = TOL, epsilon = 1e-12);
}
}
#[test]
fn test_opt_pml_well_behaved_matches_nebula() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let fit = opt_pml(&data, &[0.5, -0.25], &variance, None).expect("fit");
assert_slice(&fit.beta, &[1.1055943907637618, -0.98348970166026206]);
assert_slice(
&fit.log_w,
&[
-0.025429869309964156,
0.022419629678247836,
0.33781545877640862,
],
);
assert_slice(
&fit.information,
&[
5.4708370512237092,
2.6193993778249318,
2.6193993778249318,
4.0223986651682804,
],
);
assert_relative_eq!(fit.log_likelihood, -60.378642466146786, max_relative = TOL);
assert_relative_eq!(fit.log_det, 6.0421923783970968, max_relative = TOL);
assert_eq!(fit.second_order, 0.0);
assert_eq!(fit.iterations, 4);
assert_eq!(fit.backtracks, 0);
assert_eq!(fit.convergence, CONV_SUCCESS);
}
#[test]
fn test_opt_pml_third_order_laplace_correction() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let params = PmlParams {
ord: 3,
..Default::default()
};
let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
assert_slice(&fit.beta, &[1.1055943907637618, -0.98348970166026206]);
assert_relative_eq!(fit.second_order, 0.0028301625845031939, max_relative = TOL);
}
#[test]
fn test_opt_pml_reml_log_determinant() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let params = PmlParams {
reml: true,
..Default::default()
};
let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
assert_relative_eq!(fit.log_det, 8.759838692617901, max_relative = TOL);
assert_slice(&fit.beta, &[1.1055943907637618, -0.98348970166026206]);
}
#[test]
fn test_opt_pml_subject_with_all_zero_counts() {
let data = fixture_b();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let fit = opt_pml(&data, &[0.5, -0.25], &variance, None).expect("fit");
assert_slice(&fit.beta, &[0.15781792039265752, -0.098178037869394941]);
assert_slice(
&fit.log_w,
&[
0.31781101972092152,
-0.83477809302857631,
0.46458776408134972,
],
);
assert_slice(
&fit.information,
&[
4.9151627959404793,
2.59325292186585,
2.59325292186585,
3.5781777249175555,
],
);
assert_relative_eq!(fit.log_likelihood, -50.769290856943599, max_relative = TOL);
assert_relative_eq!(fit.log_det, 5.3802869340559738, max_relative = TOL);
assert_eq!(fit.convergence, CONV_SUCCESS);
}
#[test]
fn test_opt_pml_zero_subject_third_order() {
let data = fixture_b();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let params = PmlParams {
ord: 3,
..Default::default()
};
let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
assert_relative_eq!(fit.second_order, 0.029026901671004347, max_relative = TOL);
}
#[test]
fn test_opt_pml_start_near_bound_backtracks() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let fit = opt_pml(&data, &[90.0, 0.0], &variance, None).expect("fit");
assert_slice(&fit.beta, &[1.1055943759446161, -0.98348968952057669]);
assert_slice(
&fit.log_w,
&[
-0.025429869876909816,
0.022419639320108459,
0.3378154594265026,
],
);
assert_slice(
&fit.information,
&[
5.4711020927179739,
2.6193969725187061,
2.6193969725187061,
4.0224446400576319,
],
);
assert_eq!(fit.iterations, 9);
assert_eq!(fit.convergence, CONV_SUCCESS);
}
#[test]
fn test_opt_pml_far_outside_bounds_uses_fixed_displacement() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let fit = opt_pml(&data, &[-90.0, 60.0], &variance, None).expect("fit");
assert_slice(&fit.beta, &[1.1055943725487707, -0.98348968650982715]);
assert_relative_eq!(fit.log_det, 6.0423215051590002, max_relative = TOL);
assert_eq!(fit.iterations, 12);
}
#[test]
fn test_opt_pml_critical_point_reports_minus_ten() {
let data = fixture_a();
let variance = PmlVariance {
subject: 8.0,
cell: 0.001,
};
let fit = opt_pml(&data, &[15.0, 5.0], &variance, None).expect("fit");
assert_slice(&fit.beta, &[4.9609375, -4.9609375]);
assert_slice(&fit.log_w, &[0.0390625, 0.0390625, 4.5234136840078936]);
assert_slice(
&fit.information,
&[
7.0733722800021132e-05,
7.0105348391562988e-05,
7.0105348391562988e-05,
0.00016310178559274507,
],
);
assert_relative_eq!(fit.log_det, -20.515549220945573, max_relative = TOL);
assert_eq!(fit.iterations, 3);
assert_eq!(fit.backtracks, 11);
assert_eq!(fit.convergence, CONV_CRITICAL_POINT);
}
#[test]
fn test_opt_pml_stalled_reports_minus_forty() {
let data = fixture_a();
let variance = PmlVariance {
subject: 8.0,
cell: 0.001,
};
let fit = opt_pml(&data, &[20.0, 0.0], &variance, None).expect("fit");
assert_slice(&fit.beta, &[14.9609375, -4.9609375]);
assert_slice(&fit.log_w, &[-4.9609375, -4.9609375, 4.9962571608576489]);
assert_eq!(fit.iterations, 2);
assert_eq!(fit.backtracks, 12);
assert_eq!(fit.convergence, CONV_STALLED);
}
#[test]
fn test_opt_pml_iteration_budget_reports_minus_twenty() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let params = PmlParams {
eps: -1e-300,
..Default::default()
};
let fit = opt_pml(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
assert_eq!(fit.iterations, 50);
assert_eq!(fit.convergence, CONV_MAX_ITER);
assert_slice(&fit.beta, &[1.1055943907637629, -0.98348970166026151]);
assert_slice(
&fit.information,
&[
5.4708368635191373,
2.619399280826618,
2.619399280826618,
4.0223986123865263,
],
);
}
#[test]
fn test_opt_pml_collinear_design_reports_minus_twentyfive() {
let design: Vec<f64> = DESIGN
.chunks_exact(2)
.flat_map(|r| [r[0], r[1], r[1]])
.collect();
let data = PmlData {
design: &design,
offset: &OFFSET,
counts: &COUNTS,
cell_index: &CELL_INDEX,
subject_start: &SUBJECT_START,
subject_total: &SUBJECT_TOTAL,
};
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let fit = opt_pml(&data, &[0.5, -0.25, 0.1], &variance, None).expect("fit");
assert_eq!(fit.convergence, CONV_SINGULAR);
assert_slice(
&fit.beta,
&[1.1055943330380458, -1.0834897078722583, 0.10000000000000001],
);
assert_relative_eq!(fit.log_det, 6.042689573294485, max_relative = TOL);
}
#[test]
fn test_opt_pml_single_coefficient() {
let design = [1.0; 12];
let data = PmlData {
design: &design,
offset: &OFFSET,
counts: &COUNTS,
cell_index: &CELL_INDEX,
subject_start: &SUBJECT_START,
subject_total: &SUBJECT_TOTAL,
};
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let params = PmlParams {
reml: true,
ord: 3,
..Default::default()
};
let fit = opt_pml(&data, &[0.0], &variance, Some(params)).expect("fit");
assert_slice(&fit.beta, &[0.64526354365336647]);
assert_slice(
&fit.log_w,
&[
0.040002950937536336,
0.10805651074009694,
0.21875064209166895,
],
);
assert_slice(&fit.information, &[5.7533150000267472]);
assert_relative_eq!(fit.log_det, 7.9235375511695514, max_relative = TOL);
assert_relative_eq!(fit.second_order, 0.0044134970888637676, max_relative = TOL);
}
#[test]
fn test_opt_pml_nbm_matches_nebula() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.3,
cell: 2.0,
};
let params = PmlParams {
reml: true,
..Default::default()
};
let fit = opt_pml_nbm(&data, &[0.5, -0.25], &variance, Some(params)).expect("fit");
assert_slice(&fit.beta, &[1.2213403122060453, -0.98985988766009869]);
assert_slice(
&fit.log_w,
&[
-0.14301073703498665,
-0.095894342874540239,
0.23890507990952689,
],
);
assert_slice(
&fit.information,
&[
5.4025646031315233,
2.5076353884976541,
2.5076353884976541,
3.9141513741254204,
],
);
assert_relative_eq!(fit.log_likelihood, -49.318101708775217, max_relative = TOL);
assert_relative_eq!(fit.log_det, 8.6579114426780528, max_relative = TOL);
assert_eq!(fit.second_order, 0.0);
assert_eq!(fit.iterations, 3);
assert_eq!(fit.convergence, CONV_SUCCESS);
}
#[test]
fn test_pml_log_likelihood_gradient_matches_nebula() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
let out = pml_log_likelihood_gradient(&data, &[0.5, -0.25], &[0.1, -0.2, 0.05], &variance)
.expect("gradient");
assert_relative_eq!(out.objective, 53.926203132942852, max_relative = TOL);
assert_slice(
&out.gradient,
&[
-5.2597994101747254,
-0.19846989257393055,
-0.40624529144311583,
-2.8367693486273806,
-2.0229915782218155,
],
);
}
fn fixture_large() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<usize>, Vec<f64>) {
let n_cells = 64usize;
let mut design = Vec::with_capacity(n_cells * 3);
let mut offset = Vec::with_capacity(n_cells);
let mut counts = Vec::new();
let mut cell_index = Vec::new();
let mut subject_total = vec![0.0; 4];
let starts = [0usize, 12, 32, 48, 64];
for i in 0..n_cells {
design.push(1.0);
design.push(if i % 3 == 0 { 1.0 } else { 0.0 });
design.push((i % 7) as f64 / 8.0);
offset.push(((i % 9) as f64 - 4.0) / 8.0);
let y = ((i * 7 + 3) % 11) as f64;
if y > 0.0 {
counts.push(y);
cell_index.push(i);
}
let s = starts.iter().rposition(|&b| b <= i).unwrap_or(0).min(3);
subject_total[s] += y;
}
(design, offset, counts, cell_index, subject_total)
}
#[test]
fn test_opt_pml_wider_design_and_more_cells() {
let (design, offset, counts, cell_index, subject_total) = fixture_large();
assert_eq!(subject_total, vec![58.0, 100.0, 83.0, 82.0]);
let starts = [0usize, 12, 32, 48, 64];
let data = PmlData {
design: &design,
offset: &offset,
counts: &counts,
cell_index: &cell_index,
subject_start: &starts,
subject_total: &subject_total,
};
let variance = PmlVariance {
subject: 0.35,
cell: 1.5,
};
let params = PmlParams {
reml: true,
ord: 3,
..Default::default()
};
let fit = opt_pml(&data, &[0.5, -0.25, 0.75], &variance, Some(params)).expect("fit");
assert_slice(
&fit.beta,
&[
1.4272053191863423,
0.20743588590645803,
-0.017982095723719935,
],
);
assert_slice(
&fit.log_w,
&[
0.21944093016930771,
0.12415265795703513,
0.20913336580790309,
0.14393280971073011,
],
);
assert_slice(
&fit.information,
&[
8.398261243665857,
2.7062153540842324,
3.0637064599168475,
2.7062153540842324,
16.226178650135836,
1.2821611476475319,
3.0637064599168475,
1.2821611476475319,
5.5864643706193053,
],
);
assert_relative_eq!(fit.log_likelihood, -276.5244116459354, max_relative = TOL);
assert_relative_eq!(fit.log_det, 18.407453390744379, max_relative = TOL);
assert_relative_eq!(fit.second_order, 0.0029993806316514779, max_relative = TOL);
assert_eq!(fit.iterations, 4);
assert_eq!(fit.convergence, CONV_SUCCESS);
let plain = opt_pml(&data, &[0.5, -0.25, 0.75], &variance, None).expect("fit");
assert_relative_eq!(plain.log_det, 12.052188343414912, max_relative = TOL);
assert_eq!(plain.second_order, 0.0);
assert_slice(&plain.beta, &fit.beta);
}
#[test]
fn test_check_convergence_bound_and_nan_branches() {
let base = PmlResult {
beta: vec![0.0, 0.0],
log_w: vec![0.0],
information: vec![4.0, 0.0, 0.0, 4.0],
log_likelihood: -1.0,
log_likelihood_prev: -1.0,
log_det: 0.0,
second_order: 0.0,
convergence: CONV_SUCCESS,
iterations: 3,
backtracks: 0,
};
let params = PmlParams::default();
assert_eq!(
check_convergence(&base, ¶ms, false, CONV_SUCCESS),
CONV_SUCCESS
);
assert_eq!(
check_convergence(&base, ¶ms, true, CONV_SUCCESS),
CONV_AT_BOUND
);
let nan = PmlResult {
log_likelihood: f64::NAN,
..base.clone()
};
assert_eq!(
check_convergence(&nan, ¶ms, false, CONV_SUCCESS),
CONV_NOT_FINITE
);
assert_eq!(check_convergence(&base, ¶ms, false, -50), -50);
let singular = PmlResult {
information: vec![1.0, 1.0, 1.0, 1.0],
..base.clone()
};
assert_eq!(
check_convergence(&singular, ¶ms, true, CONV_SUCCESS),
CONV_SINGULAR
);
}
#[test]
fn test_min_eigenvalue_known_matrix() {
let a = [2.0, 1.0, 1.0, 2.0];
assert_relative_eq!(min_eigenvalue(&a, 2), 1.0, max_relative = 1e-13);
assert_relative_eq!(min_eigenvalue(&[7.0], 1), 7.0, max_relative = 1e-15);
}
#[test]
fn test_ldlt_solve_indefinite_system() {
let mut a = [1.0, 2.0, 2.0, 1.0];
let mut b = [3.0, 3.0];
let mut perm = [0usize; 2];
let mut tmp = [0.0; 2];
ldlt_solve(&mut a, &mut b, 2, &mut perm, &mut tmp);
assert_relative_eq!(b[0], 1.0, max_relative = 1e-13);
assert_relative_eq!(b[1], 1.0, max_relative = 1e-13);
}
#[test]
fn test_determinant_matches_closed_form() {
let mut a = [4.0, 1.0, 2.0, 1.0, 5.0, 3.0, 2.0, 3.0, 6.0];
let mut perm = [0usize; 3];
assert_relative_eq!(
determinant(&mut a, 3, &mut perm),
70.0,
max_relative = 1e-13
);
}
#[test]
fn test_rejects_malformed_input() {
let data = fixture_a();
let variance = PmlVariance {
subject: 0.25,
cell: 2.0,
};
assert!(opt_pml(&data, &[0.5], &variance, None).is_err());
let bad_gamma = PmlVariance {
subject: 0.25,
cell: 0.0,
};
assert!(opt_pml(&data, &[0.5, -0.25], &bad_gamma, None).is_err());
let bad_subject = PmlVariance {
subject: 0.0,
cell: 2.0,
};
assert!(opt_pml(&data, &[0.5, -0.25], &bad_subject, None).is_err());
let short = PmlData {
subject_total: &SUBJECT_TOTAL[..2],
..data
};
assert!(opt_pml(&short, &[0.5, -0.25], &variance, None).is_err());
}
}