1use scirs2_core::ndarray::{Array1, Array2};
25use scirs2_core::numeric::Float;
26use std::collections::HashMap;
27use std::fmt::Debug;
28
29use crate::error::{OptimError, Result};
30
31pub const PREDICTION_FEATURE_COUNT: usize = 16;
33
34#[derive(Debug, Clone, PartialEq)]
38pub struct PredictionFeatures {
39 pub complexity: f64,
41 pub difficulty: f64,
43 pub landscape_confidence: f64,
45 pub expected_improvement: f64,
47 pub adaptation_confidence: f64,
49 pub change_count: f64,
51 pub layer_count: f64,
53 pub hidden_size: f64,
55 pub head_count: f64,
57 pub dropout: f64,
59 pub interaction: f64,
61 pub complexity_sq: f64,
63 pub difficulty_sq: f64,
65 pub strategy_conservative: f64,
68 pub strategy_aggressive: f64,
70 pub strategy_exploratory: f64,
72}
73
74impl PredictionFeatures {
75 pub fn to_vec<T>(&self) -> Array1<T>
77 where
78 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
79 {
80 let raw = [
81 self.complexity,
82 self.difficulty,
83 self.landscape_confidence,
84 self.expected_improvement,
85 self.adaptation_confidence,
86 self.change_count,
87 self.layer_count,
88 self.hidden_size,
89 self.head_count,
90 self.dropout,
91 self.interaction,
92 self.complexity_sq,
93 self.difficulty_sq,
94 self.strategy_conservative,
95 self.strategy_aggressive,
96 self.strategy_exploratory,
97 ];
98 debug_assert_eq!(raw.len(), PREDICTION_FEATURE_COUNT);
99 Array1::from_shape_fn(PREDICTION_FEATURE_COUNT, |i| {
100 scirs2_core::numeric::NumCast::from(raw[i]).unwrap_or_else(|| T::zero())
101 })
102 }
103
104 pub fn cache_key(&self) -> String {
107 let raw = [
108 self.complexity,
109 self.difficulty,
110 self.landscape_confidence,
111 self.expected_improvement,
112 self.adaptation_confidence,
113 self.change_count,
114 self.layer_count,
115 self.hidden_size,
116 self.head_count,
117 self.dropout,
118 self.strategy_conservative,
119 self.strategy_aggressive,
120 self.strategy_exploratory,
121 ];
122 let mut key = String::with_capacity(raw.len() * 8);
123 for v in raw {
124 key.push_str(&format!("{:.4}|", v));
125 }
126 key
127 }
128}
129
130#[derive(Debug, Clone)]
132pub struct PredictorSample {
133 pub features: PredictionFeatures,
135 pub convergence_improvement: f64,
137 pub final_performance: f64,
139}
140
141#[derive(Debug, Clone, PartialEq)]
143pub struct PredictorOutput {
144 pub convergence_improvement: f64,
146 pub final_performance: f64,
148 pub uncertainty: f64,
150 pub confidence: f64,
153}
154
155#[derive(Debug, Clone, PartialEq)]
157pub struct PredictorFitReport {
158 pub convergence_rmse: f64,
160 pub performance_rmse: f64,
162 pub samples: usize,
164}
165
166#[derive(Debug, Clone)]
168pub struct PredictorNetwork<
169 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
170> {
171 hidden_weights: Vec<Array2<T>>,
173 hidden_biases: Vec<Array1<T>>,
175 convergence_head: Array1<T>,
177 performance_head: Array1<T>,
179 posterior_precision_inv: Option<Array2<T>>,
181 residual_std: T,
183 ridge_lambda: T,
185 architecture: Vec<usize>,
187 trained: bool,
189}
190
191impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
192 PredictorNetwork<T>
193{
194 pub fn new(architecture: Vec<usize>, ridge_lambda: T) -> Result<Self> {
207 if architecture.len() < 2 {
208 return Err(OptimError::InvalidConfig(
209 "predictor architecture needs an input and at least one hidden layer".to_string(),
210 ));
211 }
212 if architecture.contains(&0) {
213 return Err(OptimError::InvalidConfig(
214 "predictor layer widths must be positive".to_string(),
215 ));
216 }
217 if architecture[0] != PREDICTION_FEATURE_COUNT {
218 return Err(OptimError::InvalidConfig(format!(
219 "predictor input width {} must equal PREDICTION_FEATURE_COUNT ({})",
220 architecture[0], PREDICTION_FEATURE_COUNT
221 )));
222 }
223 if ridge_lambda <= T::zero() {
224 return Err(OptimError::InvalidConfig(
225 "ridge_lambda must be positive".to_string(),
226 ));
227 }
228
229 let mut hidden_weights = Vec::with_capacity(architecture.len() - 1);
230 let mut hidden_biases = Vec::with_capacity(architecture.len() - 1);
231 let mut rng = scirs2_core::random::thread_rng();
232 for i in 0..architecture.len() - 1 {
233 let fan_in = architecture[i];
234 let fan_out = architecture[i + 1];
235 let bound = (6.0 / (fan_in + fan_out) as f64).sqrt();
237 let weight = Array2::from_shape_fn((fan_out, fan_in), |_| {
238 let sample = rng.random::<f64>() * 2.0 - 1.0;
239 scirs2_core::numeric::NumCast::from(sample * bound).unwrap_or_else(|| T::zero())
240 });
241 hidden_weights.push(weight);
242 hidden_biases.push(Array1::zeros(fan_out));
243 }
244
245 let feature_width = architecture[architecture.len() - 1];
246 Ok(Self {
247 hidden_weights,
248 hidden_biases,
249 convergence_head: Array1::zeros(feature_width + 1),
253 performance_head: Array1::zeros(feature_width + 1),
254 posterior_precision_inv: None,
255 residual_std: T::zero(),
256 ridge_lambda,
257 architecture,
258 trained: false,
259 })
260 }
261
262 pub fn is_trained(&self) -> bool {
264 self.trained
265 }
266
267 pub fn architecture(&self) -> &[usize] {
269 &self.architecture
270 }
271
272 pub fn feature_width(&self) -> usize {
274 self.architecture[self.architecture.len() - 1]
275 }
276
277 pub fn parameter_count(&self) -> usize {
279 let hidden: usize = self.hidden_weights.iter().map(|w| w.len()).sum::<usize>()
280 + self.hidden_biases.iter().map(|b| b.len()).sum::<usize>();
281 hidden + self.convergence_head.len() + self.performance_head.len()
282 }
283
284 fn feature_map(&self, input: &Array1<T>) -> Result<Array1<T>> {
286 if input.len() != self.architecture[0] {
287 return Err(OptimError::InvalidConfig(format!(
288 "predictor expected {} features, got {}",
289 self.architecture[0],
290 input.len()
291 )));
292 }
293 let mut activation = input.clone();
294 for (weight, bias) in self.hidden_weights.iter().zip(self.hidden_biases.iter()) {
295 let pre = weight.dot(&activation) + bias;
296 activation = pre.mapv(|x| x.tanh());
299 }
300
301 let mut augmented = Array1::zeros(activation.len() + 1);
302 for (i, &v) in activation.iter().enumerate() {
303 augmented[i] = v;
304 }
305 augmented[activation.len()] = T::one();
306 Ok(augmented)
307 }
308
309 pub fn fit(&mut self, samples: &[PredictorSample]) -> Result<PredictorFitReport> {
319 if samples.is_empty() {
320 return Err(OptimError::InsufficientData(
321 "predictor training needs at least one sample".to_string(),
322 ));
323 }
324
325 let width = self.feature_width() + 1;
326 let mut design: Vec<Array1<T>> = Vec::with_capacity(samples.len());
327 for sample in samples {
328 design.push(self.feature_map(&sample.features.to_vec::<T>())?);
329 }
330
331 let mut gram = Array2::<T>::zeros((width, width));
333 let mut rhs_conv = Array1::<T>::zeros(width);
334 let mut rhs_perf = Array1::<T>::zeros(width);
335 for (phi, sample) in design.iter().zip(samples.iter()) {
336 let y_c: T = scirs2_core::numeric::NumCast::from(sample.convergence_improvement)
337 .unwrap_or_else(|| T::zero());
338 let y_p: T = scirs2_core::numeric::NumCast::from(sample.final_performance)
339 .unwrap_or_else(|| T::zero());
340 for i in 0..width {
341 rhs_conv[i] = rhs_conv[i] + phi[i] * y_c;
342 rhs_perf[i] = rhs_perf[i] + phi[i] * y_p;
343 for j in 0..width {
344 gram[[i, j]] = gram[[i, j]] + phi[i] * phi[j];
345 }
346 }
347 }
348 for i in 0..width {
349 gram[[i, i]] = gram[[i, i]] + self.ridge_lambda;
350 }
351
352 let chol = cholesky(&gram)?;
353 self.convergence_head = cholesky_solve(&chol, &rhs_conv);
354 self.performance_head = cholesky_solve(&chol, &rhs_perf);
355 self.posterior_precision_inv = Some(cholesky_inverse(&chol));
356
357 let mut sq_conv = T::zero();
359 let mut sq_perf = T::zero();
360 for (phi, sample) in design.iter().zip(samples.iter()) {
361 let pred_c = dot(&self.convergence_head, phi);
362 let pred_p = dot(&self.performance_head, phi);
363 let y_c: T = scirs2_core::numeric::NumCast::from(sample.convergence_improvement)
364 .unwrap_or_else(|| T::zero());
365 let y_p: T = scirs2_core::numeric::NumCast::from(sample.final_performance)
366 .unwrap_or_else(|| T::zero());
367 sq_conv = sq_conv + (pred_c - y_c) * (pred_c - y_c);
368 sq_perf = sq_perf + (pred_p - y_p) * (pred_p - y_p);
369 }
370 let n = T::from(samples.len()).unwrap_or_else(|| T::one());
371 let rmse_conv = (sq_conv / n).sqrt();
372 let rmse_perf = (sq_perf / n).sqrt();
373 self.residual_std = if rmse_conv > rmse_perf {
374 rmse_conv
375 } else {
376 rmse_perf
377 };
378 self.trained = true;
379
380 Ok(PredictorFitReport {
381 convergence_rmse: rmse_conv.to_f64().unwrap_or(0.0),
382 performance_rmse: rmse_perf.to_f64().unwrap_or(0.0),
383 samples: samples.len(),
384 })
385 }
386
387 pub fn predict(&self, features: &PredictionFeatures) -> Result<PredictorOutput> {
393 let phi = self.feature_map(&features.to_vec::<T>())?;
394 if !self.trained {
395 return Ok(PredictorOutput {
396 convergence_improvement: 0.0,
397 final_performance: 0.0,
398 uncertainty: 1.0,
399 confidence: 0.0,
400 });
401 }
402
403 let mean_c = dot(&self.convergence_head, &phi).to_f64().unwrap_or(0.0);
404 let mean_p = dot(&self.performance_head, &phi).to_f64().unwrap_or(0.0);
405
406 let leverage = match &self.posterior_precision_inv {
408 Some(a_inv) => {
409 let av = a_inv.dot(&phi);
410 dot(&av, &phi).to_f64().unwrap_or(0.0).max(0.0)
411 }
412 None => 0.0,
413 };
414 let sigma = self.residual_std.to_f64().unwrap_or(0.0).abs();
415 let uncertainty = sigma * (1.0 + leverage).sqrt();
416
417 Ok(PredictorOutput {
418 convergence_improvement: mean_c,
419 final_performance: mean_p,
420 uncertainty,
421 confidence: 1.0 / (1.0 + uncertainty),
422 })
423 }
424}
425
426#[derive(Debug, Clone)]
431pub struct PredictionCache {
432 entries: HashMap<String, PredictorOutput>,
433 order: Vec<String>,
435 capacity: usize,
436 hits: u64,
437 misses: u64,
438}
439
440impl PredictionCache {
441 pub fn new(capacity: usize) -> Self {
444 Self {
445 entries: HashMap::new(),
446 order: Vec::new(),
447 capacity,
448 hits: 0,
449 misses: 0,
450 }
451 }
452
453 pub fn get(&mut self, key: &str) -> Option<PredictorOutput> {
455 match self.entries.get(key) {
456 Some(v) => {
457 self.hits += 1;
458 Some(v.clone())
459 }
460 None => {
461 self.misses += 1;
462 None
463 }
464 }
465 }
466
467 pub fn insert(&mut self, key: String, value: PredictorOutput) {
469 if self.capacity == 0 {
470 return;
471 }
472 if self.entries.insert(key.clone(), value).is_none() {
473 self.order.push(key);
474 while self.order.len() > self.capacity {
475 let evicted = self.order.remove(0);
476 self.entries.remove(&evicted);
477 }
478 }
479 }
480
481 pub fn hit_rate(&self) -> f64 {
483 let total = self.hits + self.misses;
484 if total == 0 {
485 0.0
486 } else {
487 self.hits as f64 / total as f64
488 }
489 }
490
491 pub fn len(&self) -> usize {
493 self.entries.len()
494 }
495
496 pub fn is_empty(&self) -> bool {
498 self.entries.is_empty()
499 }
500
501 pub fn clear(&mut self) {
503 self.entries.clear();
504 self.order.clear();
505 }
506}
507
508fn dot<T: Float>(a: &Array1<T>, b: &Array1<T>) -> T {
513 let n = a.len().min(b.len());
514 let mut acc = T::zero();
515 for i in 0..n {
516 acc = acc + a[i] * b[i];
517 }
518 acc
519}
520
521fn cholesky<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
526 a: &Array2<T>,
527) -> Result<Array2<T>> {
528 let n = a.nrows();
529 if a.ncols() != n {
530 return Err(OptimError::InvalidConfig(
531 "Cholesky requires a square matrix".to_string(),
532 ));
533 }
534 let mut l = Array2::<T>::zeros((n, n));
535 for i in 0..n {
536 for j in 0..=i {
537 let mut sum = a[[i, j]];
538 for k in 0..j {
539 sum = sum - l[[i, k]] * l[[j, k]];
540 }
541 if i == j {
542 if sum <= T::zero() {
543 return Err(OptimError::ComputationError(format!(
544 "matrix is not positive-definite at pivot {i}"
545 )));
546 }
547 l[[i, j]] = sum.sqrt();
548 } else {
549 l[[i, j]] = sum / l[[j, j]];
550 }
551 }
552 }
553 Ok(l)
554}
555
556fn cholesky_solve<
558 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
559>(
560 l: &Array2<T>,
561 b: &Array1<T>,
562) -> Array1<T> {
563 let n = l.nrows();
564 let mut y = Array1::<T>::zeros(n);
566 for i in 0..n {
567 let mut sum = b[i];
568 for k in 0..i {
569 sum = sum - l[[i, k]] * y[k];
570 }
571 y[i] = sum / l[[i, i]];
572 }
573 let mut x = Array1::<T>::zeros(n);
575 for i in (0..n).rev() {
576 let mut sum = y[i];
577 for k in (i + 1)..n {
578 sum = sum - l[[k, i]] * x[k];
579 }
580 x[i] = sum / l[[i, i]];
581 }
582 x
583}
584
585fn cholesky_inverse<
587 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
588>(
589 l: &Array2<T>,
590) -> Array2<T> {
591 let n = l.nrows();
592 let mut inv = Array2::<T>::zeros((n, n));
593 let mut unit = Array1::<T>::zeros(n);
594 for col in 0..n {
595 unit.fill(T::zero());
596 unit[col] = T::one();
597 let solution = cholesky_solve(l, &unit);
598 for row in 0..n {
599 inv[[row, col]] = solution[row];
600 }
601 }
602 inv
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 fn features(complexity: f64, difficulty: f64) -> PredictionFeatures {
610 PredictionFeatures {
611 complexity,
612 difficulty,
613 landscape_confidence: 0.8,
614 expected_improvement: 0.1,
615 adaptation_confidence: 0.7,
616 change_count: 0.125,
617 layer_count: 6.0 / 24.0,
618 hidden_size: 512.0 / 2048.0,
619 head_count: 8.0 / 32.0,
620 dropout: 0.1,
621 interaction: complexity * difficulty,
622 complexity_sq: complexity * complexity,
623 difficulty_sq: difficulty * difficulty,
624 strategy_conservative: 0.0,
625 strategy_aggressive: 0.0,
626 strategy_exploratory: 1.0,
627 }
628 }
629
630 fn network() -> PredictorNetwork<f64> {
631 PredictorNetwork::<f64>::new(vec![PREDICTION_FEATURE_COUNT, 32, 24], 1e-4)
632 .expect("network construction")
633 }
634
635 fn truth(f: &PredictionFeatures) -> (f64, f64) {
638 let improvement = 0.30 - 0.20 * f.difficulty - 0.05 * f.complexity;
639 let performance = 0.95 - 0.30 * f.difficulty - 0.10 * f.complexity;
640 (improvement, performance)
641 }
642
643 fn training_set(n: usize) -> Vec<PredictorSample> {
644 (0..n)
645 .map(|i| {
646 let c = (i as f64 * 0.037).fract();
647 let d = (i as f64 * 0.091).fract();
648 let f = features(c, d);
649 let (ci, fp) = truth(&f);
650 PredictorSample {
651 features: f,
652 convergence_improvement: ci,
653 final_performance: fp,
654 }
655 })
656 .collect()
657 }
658
659 #[test]
660 fn rejects_bad_architecture() {
661 assert!(PredictorNetwork::<f64>::new(vec![PREDICTION_FEATURE_COUNT], 1e-4).is_err());
662 assert!(PredictorNetwork::<f64>::new(vec![3, 8], 1e-4).is_err());
663 assert!(PredictorNetwork::<f64>::new(vec![PREDICTION_FEATURE_COUNT, 0], 1e-4).is_err());
664 assert!(PredictorNetwork::<f64>::new(vec![PREDICTION_FEATURE_COUNT, 8], 0.0).is_err());
665 }
666
667 #[test]
670 fn hidden_layers_are_not_zero_initialized() {
671 let net = network();
672 let phi_a = net
673 .feature_map(&features(0.1, 0.9).to_vec::<f64>())
674 .expect("map");
675 let phi_b = net
676 .feature_map(&features(0.9, 0.1).to_vec::<f64>())
677 .expect("map");
678 let magnitude = phi_a.iter().map(|v| v.abs()).fold(0.0, f64::max);
679 assert!(
680 magnitude > 1e-6,
681 "feature map collapsed to zero (max |phi| {magnitude})"
682 );
683 let delta = phi_a
684 .iter()
685 .zip(phi_b.iter())
686 .map(|(a, b)| (a - b).abs())
687 .fold(0.0, f64::max);
688 assert!(
689 delta > 1e-6,
690 "feature map ignores its input (delta {delta})"
691 );
692 }
693
694 #[test]
695 fn untrained_predictor_reports_no_information() {
696 let net = network();
697 let out = net.predict(&features(0.5, 0.5)).expect("predict");
698 assert_eq!(out.convergence_improvement, 0.0);
699 assert_eq!(out.confidence, 0.0);
700 assert_eq!(out.uncertainty, 1.0);
701 }
702
703 #[test]
705 fn different_inputs_give_different_predictions() {
706 let mut net = network();
707 net.fit(&training_set(200)).expect("fit");
711
712 let easy = net.predict(&features(0.05, 0.05)).expect("predict easy");
713 let hard = net.predict(&features(0.95, 0.95)).expect("predict hard");
714
715 assert!(
716 (easy.convergence_improvement - hard.convergence_improvement).abs() > 1e-4,
717 "convergence prediction is constant: {} vs {}",
718 easy.convergence_improvement,
719 hard.convergence_improvement
720 );
721 assert!(
722 (easy.final_performance - hard.final_performance).abs() > 1e-4,
723 "performance prediction is constant: {} vs {}",
724 easy.final_performance,
725 hard.final_performance
726 );
727 assert!(
730 easy.convergence_improvement > hard.convergence_improvement,
731 "easy {} should beat hard {}",
732 easy.convergence_improvement,
733 hard.convergence_improvement
734 );
735 }
736
737 #[test]
739 fn training_reduces_held_out_prediction_error() {
740 let mut net = network();
741 let train = training_set(90);
742 let held_out: Vec<PredictorSample> = (0..25)
743 .map(|i| {
744 let c = ((i as f64 + 0.5) * 0.061).fract();
745 let d = ((i as f64 + 0.5) * 0.113).fract();
746 let f = features(c, d);
747 let (ci, fp) = truth(&f);
748 PredictorSample {
749 features: f,
750 convergence_improvement: ci,
751 final_performance: fp,
752 }
753 })
754 .collect();
755
756 let error = |net: &PredictorNetwork<f64>| -> f64 {
757 let mut acc = 0.0;
758 for s in &held_out {
759 let p = net.predict(&s.features).expect("predict");
760 acc += (p.convergence_improvement - s.convergence_improvement).powi(2);
761 acc += (p.final_performance - s.final_performance).powi(2);
762 }
763 (acc / (2.0 * held_out.len() as f64)).sqrt()
764 };
765
766 let before = error(&net);
767 let report = net.fit(&train).expect("fit");
768 let after = error(&net);
769
770 assert_eq!(report.samples, train.len());
771 assert!(
772 after < before,
773 "training did not reduce held-out RMSE: {before} -> {after}"
774 );
775 assert!(
776 after < 0.02,
777 "held-out RMSE {after} is too large for a learnable target"
778 );
779 assert!(
780 report.convergence_rmse < 0.02,
781 "train RMSE {}",
782 report.convergence_rmse
783 );
784 }
785
786 #[test]
787 fn training_produces_real_uncertainty_and_confidence() {
788 let mut net = network();
789 net.fit(&training_set(80)).expect("fit");
790 let out = net.predict(&features(0.4, 0.6)).expect("predict");
791 assert!(out.uncertainty.is_finite() && out.uncertainty >= 0.0);
792 assert!(out.confidence > 0.0 && out.confidence <= 1.0);
793 assert!((out.confidence - 1.0 / (1.0 + out.uncertainty)).abs() < 1e-12);
795 }
796
797 #[test]
798 fn fit_rejects_an_empty_training_set() {
799 let mut net = network();
800 assert!(net.fit(&[]).is_err());
801 }
802
803 #[test]
804 fn feature_vector_has_the_declared_width() {
805 assert_eq!(
806 features(0.3, 0.4).to_vec::<f64>().len(),
807 PREDICTION_FEATURE_COUNT
808 );
809 }
810
811 #[test]
812 fn cache_tracks_hits_and_evicts_at_capacity() {
813 let mut cache = PredictionCache::new(2);
814 let out = PredictorOutput {
815 convergence_improvement: 0.1,
816 final_performance: 0.9,
817 uncertainty: 0.05,
818 confidence: 0.95,
819 };
820 assert!(cache.get("a").is_none());
821 cache.insert("a".to_string(), out.clone());
822 cache.insert("b".to_string(), out.clone());
823 assert_eq!(cache.len(), 2);
824 cache.insert("c".to_string(), out);
825 assert_eq!(cache.len(), 2, "capacity was not enforced");
826 assert!(cache.get("a").is_none(), "oldest entry should be evicted");
827 assert!(cache.get("c").is_some());
828 assert!(cache.hit_rate() > 0.0 && cache.hit_rate() < 1.0);
829 }
830
831 #[test]
832 fn zero_capacity_cache_stores_nothing() {
833 let mut cache = PredictionCache::new(0);
834 cache.insert(
835 "a".to_string(),
836 PredictorOutput {
837 convergence_improvement: 0.0,
838 final_performance: 0.0,
839 uncertainty: 0.0,
840 confidence: 1.0,
841 },
842 );
843 assert!(cache.is_empty());
844 }
845
846 #[test]
847 fn cholesky_solves_a_known_system() {
848 let a = Array2::from_shape_vec((2, 2), vec![4.0, 2.0, 2.0, 3.0]).expect("shape");
850 let b = Array1::from_vec(vec![2.0, 1.0]);
851 let l = cholesky(&a).expect("factor");
852 let x = cholesky_solve(&l, &b);
853 let residual = a.dot(&x) - &b;
854 let err = residual.iter().map(|v| v.abs()).fold(0.0, f64::max);
855 assert!(err < 1e-12, "residual {err}");
856
857 let inv = cholesky_inverse(&l);
858 let identity = a.dot(&inv);
859 for i in 0..2 {
860 for j in 0..2 {
861 let expected = if i == j { 1.0 } else { 0.0 };
862 assert!((identity[[i, j]] - expected).abs() < 1e-12);
863 }
864 }
865 }
866
867 #[test]
868 fn cholesky_rejects_indefinite_matrices() {
869 let a = Array2::from_shape_vec((2, 2), vec![0.0, 1.0, 1.0, 0.0]).expect("shape");
870 assert!(cholesky(&a).is_err());
871 }
872}