1use crate::model_types::EstimationError;
21use crate::multinomial_reml::{MultinomialLogitRowProgram, multinomial_logit_probabilities_into};
22use ndarray::{Array1, Array2, Array3, ArrayView2};
23
24#[derive(Clone, Debug)]
32pub enum VectorNoise {
33 Isotropic(f64),
35 Diagonal(Array1<f64>),
37 LowRank {
39 diag: Array1<f64>,
40 factor: Array2<f64>,
41 },
42}
43
44impl VectorNoise {
45 pub fn diag_precision(&self, m: usize) -> Result<Array1<f64>, EstimationError> {
49 match self {
50 Self::Isotropic(sigma) => {
51 if !sigma.is_finite() || *sigma <= 0.0 {
52 crate::bail_invalid_estim!(
53 "VectorNoise::Isotropic: σ must be > 0 and finite (got {sigma})",
54 );
55 }
56 let p = 1.0 / (sigma * sigma);
57 Ok(Array1::from_elem(m, p))
58 }
59 Self::Diagonal(sigma) => {
60 if sigma.len() != m {
61 crate::bail_invalid_estim!(
62 "VectorNoise::Diagonal: σ length {} ≠ M={m}",
63 sigma.len()
64 );
65 }
66 let mut out = Array1::<f64>::zeros(m);
67 for j in 0..m {
68 let s = sigma[j];
69 if !s.is_finite() || s <= 0.0 {
70 crate::bail_invalid_estim!(
71 "VectorNoise::Diagonal: σ[{j}] must be > 0 and finite (got {s})",
72 );
73 }
74 out[j] = 1.0 / (s * s);
75 }
76 Ok(out)
77 }
78 Self::LowRank { diag, .. } => {
79 if diag.len() != m {
80 crate::bail_invalid_estim!(
81 "VectorNoise::LowRank: diag length {} ≠ M={m}",
82 diag.len()
83 );
84 }
85 let mut out = Array1::<f64>::zeros(m);
86 for j in 0..m {
87 let d = diag[j];
88 if !d.is_finite() || d <= 0.0 {
89 crate::bail_invalid_estim!(
90 "VectorNoise::LowRank: diag[{j}] must be > 0 (got {d})",
91 );
92 }
93 out[j] = d;
96 }
97 Ok(out)
98 }
99 }
100 }
101}
102
103#[derive(Clone, Debug)]
109pub struct VectorResponseTarget {
110 pub y: Array2<f64>,
112 pub noise: VectorNoise,
114 pub row_weights: Option<Array1<f64>>,
116}
117
118impl VectorResponseTarget {
119 pub fn new(y: Array2<f64>, noise: VectorNoise) -> Self {
120 Self {
121 y,
122 noise,
123 row_weights: None,
124 }
125 }
126
127 pub fn with_row_weights(mut self, w: Array1<f64>) -> Result<Self, EstimationError> {
128 validate_row_weights(&w, self.y.nrows())?;
129 self.row_weights = Some(w);
130 Ok(self)
131 }
132
133 pub fn n(&self) -> usize {
134 self.y.nrows()
135 }
136 pub fn m(&self) -> usize {
137 self.y.ncols()
138 }
139}
140
141pub(crate) const MULTINOMIAL_SIMPLEX_TOL: f64 = 1.0e-9;
155
156pub(crate) fn validate_multinomial_simplex(
164 y: ArrayView2<f64>,
165 context: &str,
166) -> Result<(), EstimationError> {
167 let (n, k) = y.dim();
168 for row in 0..n {
169 let mut row_sum = 0.0_f64;
170 for c in 0..k {
171 let v = y[[row, c]];
172 if !v.is_finite() {
173 crate::bail_invalid_estim!("{context}: y[{row},{c}] must be finite (got {v})");
174 }
175 if v < 0.0 {
176 crate::bail_invalid_estim!(
177 "{context}: multinomial target must be a probability vector \
178 (y_c ≥ 0); got y[{row},{c}] = {v}"
179 );
180 }
181 row_sum += v;
182 }
183 if (row_sum - 1.0).abs() > MULTINOMIAL_SIMPLEX_TOL {
184 crate::bail_invalid_estim!(
185 "{context}: multinomial target rows must sum to 1 (one-hot for \
186 hard labels, or a label-smoothed probability vector); row {row} \
187 sums to {row_sum}. The softmax residual gradient y_a − p_a and \
188 Fisher block p_a δ_ab − p_a p_b are the derivatives of \
189 Σ_c y_c log p_c only when the row mass is 1."
190 );
191 }
192 }
193 Ok(())
194}
195
196fn validate_row_weights(weights: &Array1<f64>, n: usize) -> Result<(), EstimationError> {
197 if weights.len() != n {
198 crate::bail_invalid_estim!("row_weights length {} ≠ N={n}", weights.len());
199 }
200 for (idx, weight) in weights.iter().copied().enumerate() {
201 if !(weight.is_finite() && weight >= 0.0) {
202 crate::bail_invalid_estim!(
203 "row_weights[{idx}] must be finite and non-negative (got {weight})"
204 );
205 }
206 }
207 Ok(())
208}
209
210pub trait VectorLikelihood {
217 fn log_lik(&self, eta: ArrayView2<f64>, y: ArrayView2<f64>) -> Result<f64, EstimationError>;
219
220 fn grad_eta(
222 &self,
223 eta: ArrayView2<f64>,
224 y: ArrayView2<f64>,
225 ) -> Result<Array2<f64>, EstimationError>;
226
227 fn hess_diag(
230 &self,
231 eta: ArrayView2<f64>,
232 y: ArrayView2<f64>,
233 ) -> Result<Array2<f64>, EstimationError>;
234
235 fn hess_block(
250 &self,
251 eta: ArrayView2<f64>,
252 y: ArrayView2<f64>,
253 ) -> Result<Array3<f64>, EstimationError> {
254 let diag = self.hess_diag(eta, y)?;
255 let (n, m) = diag.dim();
256 let mut out = Array3::<f64>::zeros((n, m, m));
257 for row in 0..n {
258 for j in 0..m {
259 out[[row, j, j]] = diag[[row, j]];
260 }
261 }
262 Ok(out)
263 }
264}
265
266pub(crate) fn validate_vector_likelihood_inputs(
267 context: &str,
268 eta: ArrayView2<'_, f64>,
269 y: ArrayView2<'_, f64>,
270 expected_columns: Option<usize>,
271) -> Result<(), EstimationError> {
272 if eta.dim() != y.dim() {
273 crate::bail_invalid_estim!(
274 "{context}: eta shape {:?} does not match response shape {:?}",
275 eta.dim(),
276 y.dim()
277 );
278 }
279 if let Some(expected) = expected_columns
280 && eta.ncols() != expected
281 {
282 crate::bail_invalid_estim!(
283 "{context}: eta has {} columns; expected {expected}",
284 eta.ncols()
285 );
286 }
287 if let Some(((row, column), value)) = eta.indexed_iter().find(|(_, value)| !value.is_finite()) {
288 crate::bail_invalid_estim!("{context}: eta[{row},{column}] must be finite, got {value}");
289 }
290 if let Some(((row, column), value)) = y.indexed_iter().find(|(_, value)| !value.is_finite()) {
291 crate::bail_invalid_estim!(
292 "{context}: response[{row},{column}] must be finite, got {value}"
293 );
294 }
295 Ok(())
296}
297
298#[derive(Clone, Debug)]
309pub struct GaussianVectorLikelihood {
310 pub precision: Array1<f64>,
314 pub factor: Option<Array2<f64>>,
318 pub row_weights: Option<Array1<f64>>,
320}
321
322impl GaussianVectorLikelihood {
323 pub fn from_target(target: &VectorResponseTarget) -> Result<Self, EstimationError> {
324 if let Some(weights) = target.row_weights.as_ref() {
325 validate_row_weights(weights, target.n())?;
326 }
327 let precision = target.noise.diag_precision(target.m())?;
328 let factor = match &target.noise {
329 VectorNoise::LowRank { factor, .. } => {
330 if factor.nrows() != target.m() {
331 crate::bail_invalid_estim!(
332 "VectorNoise::LowRank: factor has {} rows but M={}",
333 factor.nrows(),
334 target.m()
335 );
336 }
337 for ((row, col), value) in factor.indexed_iter() {
338 if !value.is_finite() {
339 crate::bail_invalid_estim!(
340 "VectorNoise::LowRank: factor[{row},{col}] must be finite (got {value})"
341 );
342 }
343 }
344 Some(factor.clone())
345 }
346 _ => None,
347 };
348 Ok(Self {
349 precision,
350 factor,
351 row_weights: target.row_weights.clone(),
352 })
353 }
354
355 #[inline]
356 fn row_weight(&self, n: usize) -> f64 {
357 self.row_weights.as_ref().map_or(1.0, |w| w[n])
358 }
359}
360
361impl VectorLikelihood for GaussianVectorLikelihood {
362 fn log_lik(&self, eta: ArrayView2<f64>, y: ArrayView2<f64>) -> Result<f64, EstimationError> {
363 validate_vector_likelihood_inputs(
364 "GaussianVectorLikelihood::log_lik",
365 eta,
366 y,
367 Some(self.precision.len()),
368 )?;
369 let m = eta.ncols();
370 let rank = self.factor.as_ref().map_or(0, |f| f.ncols());
371 let mut acc = 0.0;
372 let mut ftr = vec![0.0f64; rank];
374 for n in 0..eta.nrows() {
375 let w = self.row_weight(n);
376 let mut row_acc = 0.0;
378 for j in 0..m {
379 let r = y[[n, j]] - eta[[n, j]];
380 row_acc += self.precision[j] * r * r;
381 }
382 if let Some(f) = self.factor.as_ref() {
384 for k in 0..rank {
385 ftr[k] = 0.0;
386 }
387 for j in 0..m {
388 let r = y[[n, j]] - eta[[n, j]];
389 for k in 0..rank {
390 ftr[k] += f[[j, k]] * r;
391 }
392 }
393 for k in 0..rank {
394 row_acc += ftr[k] * ftr[k];
395 }
396 }
397 acc += w * row_acc;
398 }
399 Ok(-0.5 * acc)
400 }
401
402 fn grad_eta(
403 &self,
404 eta: ArrayView2<f64>,
405 y: ArrayView2<f64>,
406 ) -> Result<Array2<f64>, EstimationError> {
407 validate_vector_likelihood_inputs(
408 "GaussianVectorLikelihood::grad_eta",
409 eta,
410 y,
411 Some(self.precision.len()),
412 )?;
413 let (n_rows, n_cols) = eta.dim();
414 let rank = self.factor.as_ref().map_or(0, |f| f.ncols());
415 let mut out = Array2::<f64>::zeros((n_rows, n_cols));
416 let mut ftr = vec![0.0f64; rank];
417 for n in 0..n_rows {
418 let w = self.row_weight(n);
419 for j in 0..n_cols {
421 out[[n, j]] = w * self.precision[j] * (y[[n, j]] - eta[[n, j]]);
422 }
423 if let Some(f) = self.factor.as_ref() {
425 for k in 0..rank {
426 ftr[k] = 0.0;
427 }
428 for j in 0..n_cols {
429 let r = y[[n, j]] - eta[[n, j]];
430 for k in 0..rank {
431 ftr[k] += f[[j, k]] * r;
432 }
433 }
434 for j in 0..n_cols {
435 let mut s = 0.0;
436 for k in 0..rank {
437 s += f[[j, k]] * ftr[k];
438 }
439 out[[n, j]] += w * s;
440 }
441 }
442 }
443 Ok(out)
444 }
445
446 fn hess_diag(
447 &self,
448 eta: ArrayView2<f64>,
449 y: ArrayView2<f64>,
450 ) -> Result<Array2<f64>, EstimationError> {
451 validate_vector_likelihood_inputs(
452 "GaussianVectorLikelihood::hess_diag",
453 eta,
454 y,
455 Some(self.precision.len()),
456 )?;
457 let (n_rows, n_cols) = eta.dim();
464 let mut out = Array2::<f64>::zeros((n_rows, n_cols));
465 let f_row_sqsum: Option<Array1<f64>> = self.factor.as_ref().map(|f| {
467 let m = f.nrows();
468 let r = f.ncols();
469 let mut s = Array1::<f64>::zeros(m);
470 for j in 0..m {
471 let mut acc = 0.0;
472 for k in 0..r {
473 let v = f[[j, k]];
474 acc += v * v;
475 }
476 s[j] = acc;
477 }
478 s
479 });
480 for n in 0..n_rows {
481 let w = self.row_weight(n);
482 for j in 0..n_cols {
483 let mut d = self.precision[j];
484 if let Some(s) = f_row_sqsum.as_ref() {
485 d += s[j];
486 }
487 out[[n, j]] = w * d;
488 }
489 }
490 Ok(out)
491 }
492
493 fn hess_block(
494 &self,
495 eta: ArrayView2<f64>,
496 y: ArrayView2<f64>,
497 ) -> Result<Array3<f64>, EstimationError> {
498 validate_vector_likelihood_inputs(
507 "GaussianVectorLikelihood::hess_block",
508 eta,
509 y,
510 Some(self.precision.len()),
511 )?;
512 let (n_rows, m) = eta.dim();
513 let rank = self.factor.as_ref().map_or(0, |f| f.ncols());
514
515 let gram: Option<Array2<f64>> = self.factor.as_ref().map(|f| {
518 let mut g = Array2::<f64>::zeros((m, m));
519 for a in 0..m {
520 for b in a..m {
521 let mut acc = 0.0;
522 for k in 0..rank {
523 acc += f[[a, k]] * f[[b, k]];
524 }
525 g[[a, b]] = acc;
526 g[[b, a]] = acc;
527 }
528 }
529 g
530 });
531
532 let mut out = Array3::<f64>::zeros((n_rows, m, m));
533 for n in 0..n_rows {
534 let w = self.row_weight(n);
535 for a in 0..m {
536 for b in 0..m {
537 let mut val = if a == b { self.precision[a] } else { 0.0 };
538 if let Some(g) = gram.as_ref() {
539 val += g[[a, b]];
540 }
541 out[[n, a, b]] = w * val;
542 }
543 }
544 }
545 Ok(out)
546 }
547}
548
549#[derive(Clone, Debug)]
601pub struct MultinomialLogitLikelihood {
602 pub active_classes: usize,
604 pub row_weights: Option<Array1<f64>>,
606}
607
608impl MultinomialLogitLikelihood {
609 pub fn with_classes(total_classes: usize) -> Result<Self, EstimationError> {
611 if total_classes < 2 {
612 crate::bail_invalid_estim!(
613 "MultinomialLogitLikelihood requires K ≥ 2 classes (got {total_classes})"
614 );
615 }
616 Ok(Self {
617 active_classes: total_classes - 1,
618 row_weights: None,
619 })
620 }
621
622 pub fn with_row_weights(mut self, w: Array1<f64>) -> Result<Self, EstimationError> {
624 validate_row_weights(&w, w.len())?;
625 self.row_weights = Some(w);
626 Ok(self)
627 }
628
629 #[inline]
631 pub fn total_classes(&self) -> usize {
632 self.active_classes + 1
633 }
634
635 #[inline]
636 fn row_weight(&self, n: usize) -> f64 {
637 self.row_weights.as_ref().map_or(1.0, |w| w[n])
638 }
639
640 pub fn softmax_with_baseline(eta_active: &[f64], out: &mut [f64]) {
649 multinomial_logit_probabilities_into(eta_active, out);
650 }
651
652 pub fn probabilities(&self, eta: ArrayView2<f64>) -> Array2<f64> {
656 let n = eta.nrows();
657 let m = self.active_classes;
658 assert_eq!(eta.ncols(), m, "η must have K-1 columns");
659 let k = self.total_classes();
660 let mut probs = Array2::<f64>::zeros((n, k));
661 let mut eta_row = vec![0.0_f64; m];
662 let mut probs_row = vec![0.0_f64; k];
663 for row in 0..n {
664 for j in 0..m {
665 eta_row[j] = eta[[row, j]];
666 }
667 Self::softmax_with_baseline(&eta_row, &mut probs_row);
668 for j in 0..k {
669 probs[[row, j]] = probs_row[j];
670 }
671 }
672 probs
673 }
674
675 #[inline]
676 fn row_program<'row>(
677 &self,
678 row: usize,
679 eta: &'row [f64],
680 response: &'row [f64],
681 ) -> Result<MultinomialLogitRowProgram<'row>, EstimationError> {
682 MultinomialLogitRowProgram::new(eta, response, self.row_weight(row)).map_err(|error| {
683 EstimationError::InvalidInput(format!("invalid multinomial row {row}: {error}"))
684 })
685 }
686
687 pub(crate) fn value_gradient_hessian(
691 &self,
692 eta: ArrayView2<f64>,
693 y: ArrayView2<f64>,
694 ) -> Result<(f64, Array2<f64>, Array3<f64>), EstimationError> {
695 let n = eta.nrows();
696 let m = self.active_classes;
697 let k = self.total_classes();
698 if y.dim() != (n, k) {
699 crate::bail_invalid_estim!(
700 "MultinomialLogitLikelihood::value_gradient_hessian: response shape {:?} must be ({n}, {k})",
701 y.dim()
702 );
703 }
704 validate_vector_likelihood_inputs(
705 "MultinomialLogitLikelihood::value_gradient_hessian active response",
706 eta,
707 y.slice(ndarray::s![.., ..m]),
708 Some(m),
709 )?;
710 let mut gradient_log_likelihood = Array2::<f64>::zeros((n, m));
711 let mut hessian = Array3::<f64>::zeros((n, m, m));
712 let mut eta_row = vec![0.0_f64; m];
713 let mut response_row = vec![0.0_f64; k];
714 let mut probabilities = vec![0.0_f64; k];
715 let mut gradient_nll = vec![0.0_f64; m];
716 let mut hessian_row = vec![0.0_f64; m * m];
717 let mut negative_log_likelihood = 0.0_f64;
718 for row in 0..n {
719 for axis in 0..m {
720 eta_row[axis] = eta[[row, axis]];
721 }
722 for class in 0..k {
723 response_row[class] = y[[row, class]];
724 }
725 let program = self.row_program(row, &eta_row, &response_row)?;
726 negative_log_likelihood += program.value_gradient_hessian_into(
727 &mut probabilities,
728 &mut gradient_nll,
729 &mut hessian_row,
730 );
731 for axis in 0..m {
732 gradient_log_likelihood[[row, axis]] = -gradient_nll[axis];
733 for other in 0..m {
734 hessian[[row, axis, other]] = hessian_row[axis * m + other];
735 }
736 }
737 }
738 Ok((-negative_log_likelihood, gradient_log_likelihood, hessian))
739 }
740
741 pub(crate) fn value_gradient(
743 &self,
744 eta: ArrayView2<f64>,
745 y: ArrayView2<f64>,
746 ) -> Result<(f64, Array2<f64>), EstimationError> {
747 let n = eta.nrows();
748 let m = self.active_classes;
749 let k = self.total_classes();
750 if y.dim() != (n, k) {
751 crate::bail_invalid_estim!(
752 "MultinomialLogitLikelihood::value_gradient: response shape {:?} must be ({n}, {k})",
753 y.dim()
754 );
755 }
756 validate_vector_likelihood_inputs(
757 "MultinomialLogitLikelihood::value_gradient active response",
758 eta,
759 y.slice(ndarray::s![.., ..m]),
760 Some(m),
761 )?;
762 let mut gradient_log_likelihood = Array2::<f64>::zeros((n, m));
763 let mut eta_row = vec![0.0_f64; m];
764 let mut response_row = vec![0.0_f64; k];
765 let mut probabilities = vec![0.0_f64; k];
766 let mut gradient_nll = vec![0.0_f64; m];
767 let mut negative_log_likelihood = 0.0_f64;
768 for row in 0..n {
769 for axis in 0..m {
770 eta_row[axis] = eta[[row, axis]];
771 }
772 for class in 0..k {
773 response_row[class] = y[[row, class]];
774 }
775 let program = self.row_program(row, &eta_row, &response_row)?;
776 negative_log_likelihood +=
777 program.value_gradient_into(&mut probabilities, &mut gradient_nll);
778 for axis in 0..m {
779 gradient_log_likelihood[[row, axis]] = -gradient_nll[axis];
780 }
781 }
782 Ok((-negative_log_likelihood, gradient_log_likelihood))
783 }
784}
785
786impl VectorLikelihood for MultinomialLogitLikelihood {
787 fn log_lik(&self, eta: ArrayView2<f64>, y: ArrayView2<f64>) -> Result<f64, EstimationError> {
788 let n = eta.nrows();
789 let m = self.active_classes;
790 let k = self.total_classes();
791 if y.dim() != (n, k) {
792 crate::bail_invalid_estim!(
793 "MultinomialLogitLikelihood::log_lik: response shape {:?} must be ({n}, {k})",
794 y.dim()
795 );
796 }
797 validate_vector_likelihood_inputs(
798 "MultinomialLogitLikelihood::log_lik active response",
799 eta,
800 y.slice(ndarray::s![.., ..m]),
801 Some(m),
802 )?;
803 let mut eta_row = vec![0.0_f64; m];
804 let mut response_row = vec![0.0_f64; k];
805 let mut negative_log_likelihood = 0.0_f64;
806 for row in 0..n {
807 for axis in 0..m {
808 eta_row[axis] = eta[[row, axis]];
809 }
810 for class in 0..k {
811 response_row[class] = y[[row, class]];
812 }
813 negative_log_likelihood += self
814 .row_program(row, &eta_row, &response_row)?
815 .negative_log_likelihood();
816 }
817 Ok(-negative_log_likelihood)
818 }
819
820 fn grad_eta(
821 &self,
822 eta: ArrayView2<f64>,
823 y: ArrayView2<f64>,
824 ) -> Result<Array2<f64>, EstimationError> {
825 Ok(self.value_gradient(eta, y)?.1)
826 }
827
828 fn hess_diag(
829 &self,
830 eta: ArrayView2<f64>,
831 y: ArrayView2<f64>,
832 ) -> Result<Array2<f64>, EstimationError> {
833 let n = eta.nrows();
838 let m = self.active_classes;
839 let k = self.total_classes();
840 if y.dim() != (n, k) {
841 crate::bail_invalid_estim!(
842 "MultinomialLogitLikelihood::hess_diag: response shape {:?} must be ({n}, {k})",
843 y.dim()
844 );
845 }
846 validate_vector_likelihood_inputs(
847 "MultinomialLogitLikelihood::hess_diag active response",
848 eta,
849 y.slice(ndarray::s![.., ..m]),
850 Some(m),
851 )?;
852 let mut out = Array2::<f64>::zeros((n, m));
853 let mut eta_row = vec![0.0_f64; m];
854 let mut response_row = vec![0.0_f64; k];
855 let mut probabilities = vec![0.0_f64; k];
856 let mut diagonal = vec![0.0_f64; m];
857 for row in 0..n {
858 for axis in 0..m {
859 eta_row[axis] = eta[[row, axis]];
860 }
861 for class in 0..k {
862 response_row[class] = y[[row, class]];
863 }
864 self.row_program(row, &eta_row, &response_row)?
865 .hessian_diagonal_into(&mut probabilities, &mut diagonal);
866 for axis in 0..m {
867 out[[row, axis]] = diagonal[axis];
868 }
869 }
870 Ok(out)
871 }
872
873 fn hess_block(
874 &self,
875 eta: ArrayView2<f64>,
876 y: ArrayView2<f64>,
877 ) -> Result<Array3<f64>, EstimationError> {
878 Ok(self.value_gradient_hessian(eta, y)?.2)
879 }
880}
881
882#[cfg(test)]
883mod tests {
884 use super::*;
885 use ndarray::{Array1, Array2};
886
887 macro_rules! expect_invalid_input {
891 ($result:expr, $needle:expr $(,)?) => {{
892 let needle: &str = $needle;
893 match $result {
894 Ok(_) => {
895 panic!("expected EstimationError::InvalidInput containing `{needle}`, got Ok")
896 }
897 Err(EstimationError::InvalidInput(msg)) => {
898 assert!(
899 msg.contains(needle),
900 "InvalidInput message `{msg}` does not contain `{needle}`"
901 );
902 msg
903 }
904 Err(other) => panic!(
905 "expected EstimationError::InvalidInput containing `{needle}`, got {other:?}"
906 ),
907 }
908 }};
909 }
910
911 fn dummy_target(n: usize, m: usize) -> VectorResponseTarget {
912 VectorResponseTarget::new(Array2::<f64>::zeros((n, m)), VectorNoise::Isotropic(1.0))
913 }
914
915 #[test]
916 fn with_row_weights_rejects_wrong_length() {
917 let target = dummy_target(4, 2);
918 let weights = Array1::from(vec![1.0, 1.0, 1.0]);
919 expect_invalid_input!(target.with_row_weights(weights), "row_weights length");
920 }
921
922 #[test]
923 fn with_row_weights_rejects_negative_entry() {
924 let target = dummy_target(3, 2);
925 let weights = Array1::from(vec![1.0, -0.5, 2.0]);
926 expect_invalid_input!(
927 target.with_row_weights(weights),
928 "must be finite and non-negative",
929 );
930 }
931
932 #[test]
933 fn with_row_weights_rejects_nan_entry() {
934 let target = dummy_target(3, 2);
935 let weights = Array1::from(vec![1.0, f64::NAN, 2.0]);
936 expect_invalid_input!(
937 target.with_row_weights(weights),
938 "must be finite and non-negative",
939 );
940 }
941
942 #[test]
943 fn with_row_weights_rejects_infinite_entry() {
944 let target = dummy_target(3, 2);
945 let weights = Array1::from(vec![1.0, f64::INFINITY, 2.0]);
946 expect_invalid_input!(
947 target.with_row_weights(weights),
948 "must be finite and non-negative",
949 );
950 }
951
952 #[test]
953 fn with_row_weights_accepts_zero_and_positive() {
954 let target = dummy_target(3, 2);
955 let weights = Array1::from(vec![0.0, 1.5, 3.0]);
956 let weighted = target
957 .with_row_weights(weights)
958 .expect("zero / positive weights should be accepted");
959 assert!(weighted.row_weights.is_some());
960 }
961
962 #[test]
963 fn from_target_rejects_low_rank_factor_with_wrong_row_count() {
964 let n = 4;
965 let m = 3;
966 let factor = Array2::from_shape_vec((2, 2), vec![0.1, 0.2, 0.3, 0.4]).unwrap();
968 let target = VectorResponseTarget::new(
969 Array2::<f64>::zeros((n, m)),
970 VectorNoise::LowRank {
971 diag: Array1::from(vec![1.0; m]),
972 factor,
973 },
974 );
975 expect_invalid_input!(GaussianVectorLikelihood::from_target(&target), "factor has",);
976 }
977
978 #[test]
979 fn from_target_rejects_non_finite_low_rank_factor_entry() {
980 let n = 4;
981 let m = 3;
982 let mut factor = Array2::<f64>::zeros((m, 2));
983 factor[[1, 0]] = f64::NAN;
984 let target = VectorResponseTarget::new(
985 Array2::<f64>::zeros((n, m)),
986 VectorNoise::LowRank {
987 diag: Array1::from(vec![1.0; m]),
988 factor,
989 },
990 );
991 expect_invalid_input!(
992 GaussianVectorLikelihood::from_target(&target),
993 "must be finite",
994 );
995 }
996
997 #[test]
998 fn from_target_accepts_well_formed_low_rank_factor() {
999 let n = 2;
1000 let m = 3;
1001 let factor = Array2::from_shape_vec((m, 2), vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]).unwrap();
1002 let target = VectorResponseTarget::new(
1003 Array2::<f64>::zeros((n, m)),
1004 VectorNoise::LowRank {
1005 diag: Array1::from(vec![1.0; m]),
1006 factor: factor.clone(),
1007 },
1008 );
1009 let lik = GaussianVectorLikelihood::from_target(&target)
1010 .expect("well-formed low-rank factor should be accepted");
1011 let stored = lik.factor.expect("low-rank factor should be carried");
1012 assert_eq!(stored.dim(), (m, 2));
1013 for ((i, j), v) in stored.indexed_iter() {
1014 assert_eq!(*v, factor[[i, j]]);
1015 }
1016 assert_eq!(m, lik.precision.len());
1024 }
1025
1026 #[test]
1027 fn from_target_propagates_row_weight_length_mismatch() {
1028 let n = 3;
1029 let m = 2;
1030 let target = VectorResponseTarget {
1031 y: Array2::<f64>::zeros((n, m)),
1032 noise: VectorNoise::Isotropic(1.0),
1033 row_weights: Some(Array1::from(vec![1.0, 1.0])),
1034 };
1035 expect_invalid_input!(
1036 GaussianVectorLikelihood::from_target(&target),
1037 "row_weights length",
1038 );
1039 }
1040
1041 #[test]
1042 fn vector_likelihood_rejects_nonfinite_optimizer_state_without_panicking_932() {
1043 let target = dummy_target(1, 2);
1044 let likelihood =
1045 GaussianVectorLikelihood::from_target(&target).expect("finite Gaussian vector target");
1046 let eta = Array2::from_shape_vec((1, 2), vec![0.0, f64::NAN]).expect("eta shape");
1047 expect_invalid_input!(
1048 likelihood.log_lik(eta.view(), target.y.view()),
1049 "eta[0,1] must be finite",
1050 );
1051 }
1052
1053 #[test]
1054 fn multinomial_row_validation_propagates_as_typed_likelihood_error_932() {
1055 let likelihood = MultinomialLogitLikelihood::with_classes(3)
1056 .expect("three-class reference-coded likelihood");
1057 let eta =
1058 Array2::from_shape_vec((1, 2), vec![f64::INFINITY, 0.0]).expect("active eta shape");
1059 let response =
1060 Array2::from_shape_vec((1, 3), vec![1.0, 0.0, 0.0]).expect("simplex response shape");
1061 expect_invalid_input!(
1062 likelihood.log_lik(eta.view(), response.view()),
1063 "eta[0,0] must be finite",
1064 );
1065 }
1066}