use crate::model_types::EstimationError;
use crate::multinomial_reml::{MultinomialLogitRowProgram, multinomial_logit_probabilities_into};
use ndarray::{Array1, Array2, Array3, ArrayView2};
#[derive(Clone, Debug)]
pub enum VectorNoise {
Isotropic(f64),
Diagonal(Array1<f64>),
LowRank {
diag: Array1<f64>,
factor: Array2<f64>,
},
}
impl VectorNoise {
pub fn diag_precision(&self, m: usize) -> Result<Array1<f64>, EstimationError> {
match self {
Self::Isotropic(sigma) => {
if !sigma.is_finite() || *sigma <= 0.0 {
crate::bail_invalid_estim!(
"VectorNoise::Isotropic: σ must be > 0 and finite (got {sigma})",
);
}
let p = 1.0 / (sigma * sigma);
Ok(Array1::from_elem(m, p))
}
Self::Diagonal(sigma) => {
if sigma.len() != m {
crate::bail_invalid_estim!(
"VectorNoise::Diagonal: σ length {} ≠ M={m}",
sigma.len()
);
}
let mut out = Array1::<f64>::zeros(m);
for j in 0..m {
let s = sigma[j];
if !s.is_finite() || s <= 0.0 {
crate::bail_invalid_estim!(
"VectorNoise::Diagonal: σ[{j}] must be > 0 and finite (got {s})",
);
}
out[j] = 1.0 / (s * s);
}
Ok(out)
}
Self::LowRank { diag, .. } => {
if diag.len() != m {
crate::bail_invalid_estim!(
"VectorNoise::LowRank: diag length {} ≠ M={m}",
diag.len()
);
}
let mut out = Array1::<f64>::zeros(m);
for j in 0..m {
let d = diag[j];
if !d.is_finite() || d <= 0.0 {
crate::bail_invalid_estim!(
"VectorNoise::LowRank: diag[{j}] must be > 0 (got {d})",
);
}
out[j] = d;
}
Ok(out)
}
}
}
}
#[derive(Clone, Debug)]
pub struct VectorResponseTarget {
pub y: Array2<f64>,
pub noise: VectorNoise,
pub row_weights: Option<Array1<f64>>,
}
impl VectorResponseTarget {
pub fn new(y: Array2<f64>, noise: VectorNoise) -> Self {
Self {
y,
noise,
row_weights: None,
}
}
pub fn with_row_weights(mut self, w: Array1<f64>) -> Result<Self, EstimationError> {
validate_row_weights(&w, self.y.nrows())?;
self.row_weights = Some(w);
Ok(self)
}
pub fn n(&self) -> usize {
self.y.nrows()
}
pub fn m(&self) -> usize {
self.y.ncols()
}
}
pub(crate) const MULTINOMIAL_SIMPLEX_TOL: f64 = 1.0e-9;
pub(crate) fn validate_multinomial_simplex(
y: ArrayView2<f64>,
context: &str,
) -> Result<(), EstimationError> {
let (n, k) = y.dim();
for row in 0..n {
let mut row_sum = 0.0_f64;
for c in 0..k {
let v = y[[row, c]];
if !v.is_finite() {
crate::bail_invalid_estim!("{context}: y[{row},{c}] must be finite (got {v})");
}
if v < 0.0 {
crate::bail_invalid_estim!(
"{context}: multinomial target must be a probability vector \
(y_c ≥ 0); got y[{row},{c}] = {v}"
);
}
row_sum += v;
}
if (row_sum - 1.0).abs() > MULTINOMIAL_SIMPLEX_TOL {
crate::bail_invalid_estim!(
"{context}: multinomial target rows must sum to 1 (one-hot for \
hard labels, or a label-smoothed probability vector); row {row} \
sums to {row_sum}. The softmax residual gradient y_a − p_a and \
Fisher block p_a δ_ab − p_a p_b are the derivatives of \
Σ_c y_c log p_c only when the row mass is 1."
);
}
}
Ok(())
}
fn validate_row_weights(weights: &Array1<f64>, n: usize) -> Result<(), EstimationError> {
if weights.len() != n {
crate::bail_invalid_estim!("row_weights length {} ≠ N={n}", weights.len());
}
for (idx, weight) in weights.iter().copied().enumerate() {
if !(weight.is_finite() && weight >= 0.0) {
crate::bail_invalid_estim!(
"row_weights[{idx}] must be finite and non-negative (got {weight})"
);
}
}
Ok(())
}
pub trait VectorLikelihood {
fn log_lik(&self, eta: ArrayView2<f64>, y: ArrayView2<f64>) -> Result<f64, EstimationError>;
fn grad_eta(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array2<f64>, EstimationError>;
fn hess_diag(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array2<f64>, EstimationError>;
fn hess_block(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array3<f64>, EstimationError> {
let diag = self.hess_diag(eta, y)?;
let (n, m) = diag.dim();
let mut out = Array3::<f64>::zeros((n, m, m));
for row in 0..n {
for j in 0..m {
out[[row, j, j]] = diag[[row, j]];
}
}
Ok(out)
}
}
pub(crate) fn validate_vector_likelihood_inputs(
context: &str,
eta: ArrayView2<'_, f64>,
y: ArrayView2<'_, f64>,
expected_columns: Option<usize>,
) -> Result<(), EstimationError> {
if eta.dim() != y.dim() {
crate::bail_invalid_estim!(
"{context}: eta shape {:?} does not match response shape {:?}",
eta.dim(),
y.dim()
);
}
if let Some(expected) = expected_columns
&& eta.ncols() != expected
{
crate::bail_invalid_estim!(
"{context}: eta has {} columns; expected {expected}",
eta.ncols()
);
}
if let Some(((row, column), value)) = eta.indexed_iter().find(|(_, value)| !value.is_finite()) {
crate::bail_invalid_estim!("{context}: eta[{row},{column}] must be finite, got {value}");
}
if let Some(((row, column), value)) = y.indexed_iter().find(|(_, value)| !value.is_finite()) {
crate::bail_invalid_estim!(
"{context}: response[{row},{column}] must be finite, got {value}"
);
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct GaussianVectorLikelihood {
pub precision: Array1<f64>,
pub factor: Option<Array2<f64>>,
pub row_weights: Option<Array1<f64>>,
}
impl GaussianVectorLikelihood {
pub fn from_target(target: &VectorResponseTarget) -> Result<Self, EstimationError> {
if let Some(weights) = target.row_weights.as_ref() {
validate_row_weights(weights, target.n())?;
}
let precision = target.noise.diag_precision(target.m())?;
let factor = match &target.noise {
VectorNoise::LowRank { factor, .. } => {
if factor.nrows() != target.m() {
crate::bail_invalid_estim!(
"VectorNoise::LowRank: factor has {} rows but M={}",
factor.nrows(),
target.m()
);
}
for ((row, col), value) in factor.indexed_iter() {
if !value.is_finite() {
crate::bail_invalid_estim!(
"VectorNoise::LowRank: factor[{row},{col}] must be finite (got {value})"
);
}
}
Some(factor.clone())
}
_ => None,
};
Ok(Self {
precision,
factor,
row_weights: target.row_weights.clone(),
})
}
#[inline]
fn row_weight(&self, n: usize) -> f64 {
self.row_weights.as_ref().map_or(1.0, |w| w[n])
}
}
impl VectorLikelihood for GaussianVectorLikelihood {
fn log_lik(&self, eta: ArrayView2<f64>, y: ArrayView2<f64>) -> Result<f64, EstimationError> {
validate_vector_likelihood_inputs(
"GaussianVectorLikelihood::log_lik",
eta,
y,
Some(self.precision.len()),
)?;
let m = eta.ncols();
let rank = self.factor.as_ref().map_or(0, |f| f.ncols());
let mut acc = 0.0;
let mut ftr = vec![0.0f64; rank];
for n in 0..eta.nrows() {
let w = self.row_weight(n);
let mut row_acc = 0.0;
for j in 0..m {
let r = y[[n, j]] - eta[[n, j]];
row_acc += self.precision[j] * r * r;
}
if let Some(f) = self.factor.as_ref() {
for k in 0..rank {
ftr[k] = 0.0;
}
for j in 0..m {
let r = y[[n, j]] - eta[[n, j]];
for k in 0..rank {
ftr[k] += f[[j, k]] * r;
}
}
for k in 0..rank {
row_acc += ftr[k] * ftr[k];
}
}
acc += w * row_acc;
}
Ok(-0.5 * acc)
}
fn grad_eta(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array2<f64>, EstimationError> {
validate_vector_likelihood_inputs(
"GaussianVectorLikelihood::grad_eta",
eta,
y,
Some(self.precision.len()),
)?;
let (n_rows, n_cols) = eta.dim();
let rank = self.factor.as_ref().map_or(0, |f| f.ncols());
let mut out = Array2::<f64>::zeros((n_rows, n_cols));
let mut ftr = vec![0.0f64; rank];
for n in 0..n_rows {
let w = self.row_weight(n);
for j in 0..n_cols {
out[[n, j]] = w * self.precision[j] * (y[[n, j]] - eta[[n, j]]);
}
if let Some(f) = self.factor.as_ref() {
for k in 0..rank {
ftr[k] = 0.0;
}
for j in 0..n_cols {
let r = y[[n, j]] - eta[[n, j]];
for k in 0..rank {
ftr[k] += f[[j, k]] * r;
}
}
for j in 0..n_cols {
let mut s = 0.0;
for k in 0..rank {
s += f[[j, k]] * ftr[k];
}
out[[n, j]] += w * s;
}
}
}
Ok(out)
}
fn hess_diag(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array2<f64>, EstimationError> {
validate_vector_likelihood_inputs(
"GaussianVectorLikelihood::hess_diag",
eta,
y,
Some(self.precision.len()),
)?;
let (n_rows, n_cols) = eta.dim();
let mut out = Array2::<f64>::zeros((n_rows, n_cols));
let f_row_sqsum: Option<Array1<f64>> = self.factor.as_ref().map(|f| {
let m = f.nrows();
let r = f.ncols();
let mut s = Array1::<f64>::zeros(m);
for j in 0..m {
let mut acc = 0.0;
for k in 0..r {
let v = f[[j, k]];
acc += v * v;
}
s[j] = acc;
}
s
});
for n in 0..n_rows {
let w = self.row_weight(n);
for j in 0..n_cols {
let mut d = self.precision[j];
if let Some(s) = f_row_sqsum.as_ref() {
d += s[j];
}
out[[n, j]] = w * d;
}
}
Ok(out)
}
fn hess_block(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array3<f64>, EstimationError> {
validate_vector_likelihood_inputs(
"GaussianVectorLikelihood::hess_block",
eta,
y,
Some(self.precision.len()),
)?;
let (n_rows, m) = eta.dim();
let rank = self.factor.as_ref().map_or(0, |f| f.ncols());
let gram: Option<Array2<f64>> = self.factor.as_ref().map(|f| {
let mut g = Array2::<f64>::zeros((m, m));
for a in 0..m {
for b in a..m {
let mut acc = 0.0;
for k in 0..rank {
acc += f[[a, k]] * f[[b, k]];
}
g[[a, b]] = acc;
g[[b, a]] = acc;
}
}
g
});
let mut out = Array3::<f64>::zeros((n_rows, m, m));
for n in 0..n_rows {
let w = self.row_weight(n);
for a in 0..m {
for b in 0..m {
let mut val = if a == b { self.precision[a] } else { 0.0 };
if let Some(g) = gram.as_ref() {
val += g[[a, b]];
}
out[[n, a, b]] = w * val;
}
}
}
Ok(out)
}
}
#[derive(Clone, Debug)]
pub struct MultinomialLogitLikelihood {
pub active_classes: usize,
pub row_weights: Option<Array1<f64>>,
}
impl MultinomialLogitLikelihood {
pub fn with_classes(total_classes: usize) -> Result<Self, EstimationError> {
if total_classes < 2 {
crate::bail_invalid_estim!(
"MultinomialLogitLikelihood requires K ≥ 2 classes (got {total_classes})"
);
}
Ok(Self {
active_classes: total_classes - 1,
row_weights: None,
})
}
pub fn with_row_weights(mut self, w: Array1<f64>) -> Result<Self, EstimationError> {
validate_row_weights(&w, w.len())?;
self.row_weights = Some(w);
Ok(self)
}
#[inline]
pub fn total_classes(&self) -> usize {
self.active_classes + 1
}
#[inline]
fn row_weight(&self, n: usize) -> f64 {
self.row_weights.as_ref().map_or(1.0, |w| w[n])
}
pub fn softmax_with_baseline(eta_active: &[f64], out: &mut [f64]) {
multinomial_logit_probabilities_into(eta_active, out);
}
pub fn probabilities(&self, eta: ArrayView2<f64>) -> Array2<f64> {
let n = eta.nrows();
let m = self.active_classes;
assert_eq!(eta.ncols(), m, "η must have K-1 columns");
let k = self.total_classes();
let mut probs = Array2::<f64>::zeros((n, k));
let mut eta_row = vec![0.0_f64; m];
let mut probs_row = vec![0.0_f64; k];
for row in 0..n {
for j in 0..m {
eta_row[j] = eta[[row, j]];
}
Self::softmax_with_baseline(&eta_row, &mut probs_row);
for j in 0..k {
probs[[row, j]] = probs_row[j];
}
}
probs
}
#[inline]
fn row_program<'row>(
&self,
row: usize,
eta: &'row [f64],
response: &'row [f64],
) -> Result<MultinomialLogitRowProgram<'row>, EstimationError> {
MultinomialLogitRowProgram::new(eta, response, self.row_weight(row)).map_err(|error| {
EstimationError::InvalidInput(format!("invalid multinomial row {row}: {error}"))
})
}
pub(crate) fn value_gradient_hessian(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<(f64, Array2<f64>, Array3<f64>), EstimationError> {
let n = eta.nrows();
let m = self.active_classes;
let k = self.total_classes();
if y.dim() != (n, k) {
crate::bail_invalid_estim!(
"MultinomialLogitLikelihood::value_gradient_hessian: response shape {:?} must be ({n}, {k})",
y.dim()
);
}
validate_vector_likelihood_inputs(
"MultinomialLogitLikelihood::value_gradient_hessian active response",
eta,
y.slice(ndarray::s![.., ..m]),
Some(m),
)?;
let mut gradient_log_likelihood = Array2::<f64>::zeros((n, m));
let mut hessian = Array3::<f64>::zeros((n, m, m));
let mut eta_row = vec![0.0_f64; m];
let mut response_row = vec![0.0_f64; k];
let mut probabilities = vec![0.0_f64; k];
let mut gradient_nll = vec![0.0_f64; m];
let mut hessian_row = vec![0.0_f64; m * m];
let mut negative_log_likelihood = 0.0_f64;
for row in 0..n {
for axis in 0..m {
eta_row[axis] = eta[[row, axis]];
}
for class in 0..k {
response_row[class] = y[[row, class]];
}
let program = self.row_program(row, &eta_row, &response_row)?;
negative_log_likelihood += program.value_gradient_hessian_into(
&mut probabilities,
&mut gradient_nll,
&mut hessian_row,
);
for axis in 0..m {
gradient_log_likelihood[[row, axis]] = -gradient_nll[axis];
for other in 0..m {
hessian[[row, axis, other]] = hessian_row[axis * m + other];
}
}
}
Ok((-negative_log_likelihood, gradient_log_likelihood, hessian))
}
pub(crate) fn value_gradient(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<(f64, Array2<f64>), EstimationError> {
let n = eta.nrows();
let m = self.active_classes;
let k = self.total_classes();
if y.dim() != (n, k) {
crate::bail_invalid_estim!(
"MultinomialLogitLikelihood::value_gradient: response shape {:?} must be ({n}, {k})",
y.dim()
);
}
validate_vector_likelihood_inputs(
"MultinomialLogitLikelihood::value_gradient active response",
eta,
y.slice(ndarray::s![.., ..m]),
Some(m),
)?;
let mut gradient_log_likelihood = Array2::<f64>::zeros((n, m));
let mut eta_row = vec![0.0_f64; m];
let mut response_row = vec![0.0_f64; k];
let mut probabilities = vec![0.0_f64; k];
let mut gradient_nll = vec![0.0_f64; m];
let mut negative_log_likelihood = 0.0_f64;
for row in 0..n {
for axis in 0..m {
eta_row[axis] = eta[[row, axis]];
}
for class in 0..k {
response_row[class] = y[[row, class]];
}
let program = self.row_program(row, &eta_row, &response_row)?;
negative_log_likelihood +=
program.value_gradient_into(&mut probabilities, &mut gradient_nll);
for axis in 0..m {
gradient_log_likelihood[[row, axis]] = -gradient_nll[axis];
}
}
Ok((-negative_log_likelihood, gradient_log_likelihood))
}
}
impl VectorLikelihood for MultinomialLogitLikelihood {
fn log_lik(&self, eta: ArrayView2<f64>, y: ArrayView2<f64>) -> Result<f64, EstimationError> {
let n = eta.nrows();
let m = self.active_classes;
let k = self.total_classes();
if y.dim() != (n, k) {
crate::bail_invalid_estim!(
"MultinomialLogitLikelihood::log_lik: response shape {:?} must be ({n}, {k})",
y.dim()
);
}
validate_vector_likelihood_inputs(
"MultinomialLogitLikelihood::log_lik active response",
eta,
y.slice(ndarray::s![.., ..m]),
Some(m),
)?;
let mut eta_row = vec![0.0_f64; m];
let mut response_row = vec![0.0_f64; k];
let mut negative_log_likelihood = 0.0_f64;
for row in 0..n {
for axis in 0..m {
eta_row[axis] = eta[[row, axis]];
}
for class in 0..k {
response_row[class] = y[[row, class]];
}
negative_log_likelihood += self
.row_program(row, &eta_row, &response_row)?
.negative_log_likelihood();
}
Ok(-negative_log_likelihood)
}
fn grad_eta(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array2<f64>, EstimationError> {
Ok(self.value_gradient(eta, y)?.1)
}
fn hess_diag(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array2<f64>, EstimationError> {
let n = eta.nrows();
let m = self.active_classes;
let k = self.total_classes();
if y.dim() != (n, k) {
crate::bail_invalid_estim!(
"MultinomialLogitLikelihood::hess_diag: response shape {:?} must be ({n}, {k})",
y.dim()
);
}
validate_vector_likelihood_inputs(
"MultinomialLogitLikelihood::hess_diag active response",
eta,
y.slice(ndarray::s![.., ..m]),
Some(m),
)?;
let mut out = Array2::<f64>::zeros((n, m));
let mut eta_row = vec![0.0_f64; m];
let mut response_row = vec![0.0_f64; k];
let mut probabilities = vec![0.0_f64; k];
let mut diagonal = vec![0.0_f64; m];
for row in 0..n {
for axis in 0..m {
eta_row[axis] = eta[[row, axis]];
}
for class in 0..k {
response_row[class] = y[[row, class]];
}
self.row_program(row, &eta_row, &response_row)?
.hessian_diagonal_into(&mut probabilities, &mut diagonal);
for axis in 0..m {
out[[row, axis]] = diagonal[axis];
}
}
Ok(out)
}
fn hess_block(
&self,
eta: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array3<f64>, EstimationError> {
Ok(self.value_gradient_hessian(eta, y)?.2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array1, Array2};
macro_rules! expect_invalid_input {
($result:expr, $needle:expr $(,)?) => {{
let needle: &str = $needle;
match $result {
Ok(_) => {
panic!("expected EstimationError::InvalidInput containing `{needle}`, got Ok")
}
Err(EstimationError::InvalidInput(msg)) => {
assert!(
msg.contains(needle),
"InvalidInput message `{msg}` does not contain `{needle}`"
);
msg
}
Err(other) => panic!(
"expected EstimationError::InvalidInput containing `{needle}`, got {other:?}"
),
}
}};
}
fn dummy_target(n: usize, m: usize) -> VectorResponseTarget {
VectorResponseTarget::new(Array2::<f64>::zeros((n, m)), VectorNoise::Isotropic(1.0))
}
#[test]
fn with_row_weights_rejects_wrong_length() {
let target = dummy_target(4, 2);
let weights = Array1::from(vec![1.0, 1.0, 1.0]);
expect_invalid_input!(target.with_row_weights(weights), "row_weights length");
}
#[test]
fn with_row_weights_rejects_negative_entry() {
let target = dummy_target(3, 2);
let weights = Array1::from(vec![1.0, -0.5, 2.0]);
expect_invalid_input!(
target.with_row_weights(weights),
"must be finite and non-negative",
);
}
#[test]
fn with_row_weights_rejects_nan_entry() {
let target = dummy_target(3, 2);
let weights = Array1::from(vec![1.0, f64::NAN, 2.0]);
expect_invalid_input!(
target.with_row_weights(weights),
"must be finite and non-negative",
);
}
#[test]
fn with_row_weights_rejects_infinite_entry() {
let target = dummy_target(3, 2);
let weights = Array1::from(vec![1.0, f64::INFINITY, 2.0]);
expect_invalid_input!(
target.with_row_weights(weights),
"must be finite and non-negative",
);
}
#[test]
fn with_row_weights_accepts_zero_and_positive() {
let target = dummy_target(3, 2);
let weights = Array1::from(vec![0.0, 1.5, 3.0]);
let weighted = target
.with_row_weights(weights)
.expect("zero / positive weights should be accepted");
assert!(weighted.row_weights.is_some());
}
#[test]
fn from_target_rejects_low_rank_factor_with_wrong_row_count() {
let n = 4;
let m = 3;
let factor = Array2::from_shape_vec((2, 2), vec![0.1, 0.2, 0.3, 0.4]).unwrap();
let target = VectorResponseTarget::new(
Array2::<f64>::zeros((n, m)),
VectorNoise::LowRank {
diag: Array1::from(vec![1.0; m]),
factor,
},
);
expect_invalid_input!(GaussianVectorLikelihood::from_target(&target), "factor has",);
}
#[test]
fn from_target_rejects_non_finite_low_rank_factor_entry() {
let n = 4;
let m = 3;
let mut factor = Array2::<f64>::zeros((m, 2));
factor[[1, 0]] = f64::NAN;
let target = VectorResponseTarget::new(
Array2::<f64>::zeros((n, m)),
VectorNoise::LowRank {
diag: Array1::from(vec![1.0; m]),
factor,
},
);
expect_invalid_input!(
GaussianVectorLikelihood::from_target(&target),
"must be finite",
);
}
#[test]
fn from_target_accepts_well_formed_low_rank_factor() {
let n = 2;
let m = 3;
let factor = Array2::from_shape_vec((m, 2), vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]).unwrap();
let target = VectorResponseTarget::new(
Array2::<f64>::zeros((n, m)),
VectorNoise::LowRank {
diag: Array1::from(vec![1.0; m]),
factor: factor.clone(),
},
);
let lik = GaussianVectorLikelihood::from_target(&target)
.expect("well-formed low-rank factor should be accepted");
let stored = lik.factor.expect("low-rank factor should be carried");
assert_eq!(stored.dim(), (m, 2));
for ((i, j), v) in stored.indexed_iter() {
assert_eq!(*v, factor[[i, j]]);
}
assert_eq!(m, lik.precision.len());
}
#[test]
fn from_target_propagates_row_weight_length_mismatch() {
let n = 3;
let m = 2;
let target = VectorResponseTarget {
y: Array2::<f64>::zeros((n, m)),
noise: VectorNoise::Isotropic(1.0),
row_weights: Some(Array1::from(vec![1.0, 1.0])),
};
expect_invalid_input!(
GaussianVectorLikelihood::from_target(&target),
"row_weights length",
);
}
#[test]
fn vector_likelihood_rejects_nonfinite_optimizer_state_without_panicking_932() {
let target = dummy_target(1, 2);
let likelihood =
GaussianVectorLikelihood::from_target(&target).expect("finite Gaussian vector target");
let eta = Array2::from_shape_vec((1, 2), vec![0.0, f64::NAN]).expect("eta shape");
expect_invalid_input!(
likelihood.log_lik(eta.view(), target.y.view()),
"eta[0,1] must be finite",
);
}
#[test]
fn multinomial_row_validation_propagates_as_typed_likelihood_error_932() {
let likelihood = MultinomialLogitLikelihood::with_classes(3)
.expect("three-class reference-coded likelihood");
let eta =
Array2::from_shape_vec((1, 2), vec![f64::INFINITY, 0.0]).expect("active eta shape");
let response =
Array2::from_shape_vec((1, 3), vec![1.0, 0.0, 0.0]).expect("simplex response shape");
expect_invalid_input!(
likelihood.log_lik(eta.view(), response.view()),
"eta[0,0] must be finite",
);
}
}