1pub mod argus;
18pub mod ariadne;
19pub mod echo;
20pub mod identity;
21pub mod losses;
22pub mod mnemosyne;
23pub mod polar;
24pub mod themis;
25
26pub use argus::ArgusIris;
31pub use ariadne::AriadneFingerprint;
32pub use echo::EchoSpeaker;
33pub use identity::{AegisIdentity, IdentityBank};
34pub use losses::{
35 AngularMarginLoss, ArgusLoss, CenterLoss, ContrastiveLoss, CrystallizationLoss,
36 DiversityRegularization, EchoLoss, LivenessLoss, ThemisLoss,
37};
38pub use mnemosyne::MnemosyneIdentity;
39pub use themis::ThemisFusion;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum BiometricModality {
48 Face,
50 Fingerprint,
52 Voice,
54 Iris,
56}
57
58impl std::fmt::Display for BiometricModality {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 BiometricModality::Face => write!(f, "Face (Mnemosyne)"),
62 BiometricModality::Fingerprint => write!(f, "Fingerprint (Ariadne)"),
63 BiometricModality::Voice => write!(f, "Voice (Echo)"),
64 BiometricModality::Iris => write!(f, "Iris (Argus)"),
65 }
66 }
67}
68
69impl BiometricModality {
70 pub fn all() -> Vec<Self> {
72 vec![Self::Face, Self::Fingerprint, Self::Voice, Self::Iris]
73 }
74
75 pub fn input_description(&self) -> &'static str {
77 match self {
78 Self::Face => "[B, 3, 64, 64] RGB face image",
79 Self::Fingerprint => "[B, 1, 128, 128] grayscale fingerprint",
80 Self::Voice => "[B, 40, T] mel spectrogram (variable length)",
81 Self::Iris => "[B, 1, 32, 256] polar iris strip or [B, 1, H, W] raw",
82 }
83 }
84
85 pub fn approx_params(&self) -> usize {
87 match self {
88 Self::Face => 43_000,
89 Self::Fingerprint => 65_000,
90 Self::Voice => 68_000,
91 Self::Iris => 65_000,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
101pub struct BiometricEvidence {
102 pub face: Option<axonml_autograd::Variable>,
104 pub fingerprint: Option<axonml_autograd::Variable>,
106 pub voice: Option<axonml_autograd::Variable>,
108 pub iris: Option<axonml_autograd::Variable>,
110 pub face_sequence: Option<Vec<axonml_autograd::Variable>>,
112 pub timestamp: Option<f64>,
114 pub device_id: Option<String>,
116}
117
118impl BiometricEvidence {
119 pub fn empty() -> Self {
121 Self {
122 face: None,
123 fingerprint: None,
124 voice: None,
125 iris: None,
126 face_sequence: None,
127 timestamp: None,
128 device_id: None,
129 }
130 }
131
132 pub fn face(tensor: axonml_autograd::Variable) -> Self {
134 Self {
135 face: Some(tensor),
136 ..Self::empty()
137 }
138 }
139
140 pub fn face_sequence(frames: Vec<axonml_autograd::Variable>) -> Self {
142 let first = frames.first().cloned();
143 Self {
144 face: first,
145 face_sequence: Some(frames),
146 ..Self::empty()
147 }
148 }
149
150 pub fn fingerprint(tensor: axonml_autograd::Variable) -> Self {
152 Self {
153 fingerprint: Some(tensor),
154 ..Self::empty()
155 }
156 }
157
158 pub fn voice(tensor: axonml_autograd::Variable) -> Self {
160 Self {
161 voice: Some(tensor),
162 ..Self::empty()
163 }
164 }
165
166 pub fn iris(tensor: axonml_autograd::Variable) -> Self {
168 Self {
169 iris: Some(tensor),
170 ..Self::empty()
171 }
172 }
173
174 pub fn multi(
176 face: Option<axonml_autograd::Variable>,
177 fingerprint: Option<axonml_autograd::Variable>,
178 voice: Option<axonml_autograd::Variable>,
179 iris: Option<axonml_autograd::Variable>,
180 ) -> Self {
181 Self {
182 face,
183 fingerprint,
184 voice,
185 iris,
186 face_sequence: None,
187 timestamp: None,
188 device_id: None,
189 }
190 }
191
192 pub fn with_timestamp(mut self, ts: f64) -> Self {
194 self.timestamp = Some(ts);
195 self
196 }
197
198 pub fn with_device(mut self, device: String) -> Self {
200 self.device_id = Some(device);
201 self
202 }
203
204 pub fn available_modalities(&self) -> Vec<BiometricModality> {
206 let mut mods = Vec::new();
207 if self.face.is_some() {
208 mods.push(BiometricModality::Face);
209 }
210 if self.fingerprint.is_some() {
211 mods.push(BiometricModality::Fingerprint);
212 }
213 if self.voice.is_some() {
214 mods.push(BiometricModality::Voice);
215 }
216 if self.iris.is_some() {
217 mods.push(BiometricModality::Iris);
218 }
219 mods
220 }
221
222 pub fn modality_count(&self) -> usize {
224 self.available_modalities().len()
225 }
226
227 pub fn has_face_sequence(&self) -> bool {
229 self.face_sequence.as_ref().is_some_and(|s| s.len() > 1)
230 }
231}
232
233#[derive(Debug, Clone)]
239pub struct ModalityOutput {
240 pub embedding: Vec<f32>,
242 pub log_variance: f32,
244 pub modality: BiometricModality,
246}
247
248#[derive(Debug, Clone)]
250pub struct EnrollmentResult {
251 pub success: bool,
253 pub subject_id: u64,
255 pub modalities_enrolled: Vec<BiometricModality>,
257 pub observation_count: usize,
259 pub quality_score: f32,
261}
262
263#[derive(Debug, Clone)]
265pub struct VerificationResult {
266 pub match_score: f32,
268 pub is_match: bool,
270 pub modality_scores: Vec<(BiometricModality, f32)>,
272 pub confidence: f32,
274 pub threshold: f32,
276}
277
278#[derive(Debug, Clone)]
280pub struct IdentificationResult {
281 pub candidates: Vec<IdentificationCandidate>,
283 pub confidence: f32,
285}
286
287#[derive(Debug, Clone)]
289pub struct IdentificationCandidate {
290 pub subject_id: u64,
292 pub score: f32,
294 pub modality_scores: Vec<(BiometricModality, f32)>,
296}
297
298#[derive(Debug, Clone)]
309pub struct LivenessResult {
310 pub liveness_score: f32,
312 pub is_live: bool,
314 pub temporal_variance: f32,
317 pub trajectory_smoothness: f32,
320 pub modality_liveness: Vec<(BiometricModality, f32)>,
322}
323
324impl LivenessResult {
325 pub fn unknown() -> Self {
327 Self {
328 liveness_score: 0.5,
329 is_live: false,
330 temporal_variance: 0.0,
331 trajectory_smoothness: 0.0,
332 modality_liveness: Vec::new(),
333 }
334 }
335}
336
337#[derive(Debug, Clone)]
347pub struct QualityReport {
348 pub overall_score: f32,
350 pub modality_scores: Vec<(BiometricModality, f32)>,
352 pub meets_threshold: bool,
354 pub issues: Vec<QualityIssue>,
356}
357
358#[derive(Debug, Clone)]
360pub struct QualityIssue {
361 pub modality: BiometricModality,
363 pub severity: f32,
365 pub description: String,
367}
368
369impl QualityReport {
370 pub fn all_pass(modalities: &[BiometricModality]) -> Self {
372 Self {
373 overall_score: 1.0,
374 modality_scores: modalities.iter().map(|m| (*m, 1.0)).collect(),
375 meets_threshold: true,
376 issues: Vec::new(),
377 }
378 }
379}
380
381#[derive(Debug, Clone)]
390pub struct ForensicReport {
391 pub modality_reports: Vec<ModalityForensic>,
393 pub cross_modal_consistency: f32,
396 pub dominant_modality: Option<BiometricModality>,
398 pub weakest_modality: Option<BiometricModality>,
400 pub top_contributing_dimensions: Vec<DimensionContribution>,
402}
403
404#[derive(Debug, Clone)]
406pub struct ModalityForensic {
407 pub modality: BiometricModality,
409 pub raw_score: f32,
411 pub uncertainty: f32,
413 pub fusion_weight: f32,
415 pub agrees_with_decision: bool,
417}
418
419#[derive(Debug, Clone)]
421pub struct DimensionContribution {
422 pub dimension: usize,
424 pub contribution: f32,
426 pub modality: BiometricModality,
428}
429
430#[derive(Debug, Clone)]
440pub struct DriftAlert {
441 pub subject_id: u64,
443 pub drift_magnitude: f32,
446 pub drift_rate: f32,
448 pub affected_modalities: Vec<(BiometricModality, f32)>,
450 pub recommendation: DriftRecommendation,
452}
453
454#[derive(Debug, Clone, Copy, PartialEq)]
456pub enum DriftRecommendation {
457 None,
459 Monitor,
461 ReEnroll,
463 Investigate,
465}
466
467impl std::fmt::Display for DriftRecommendation {
468 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469 match self {
470 Self::None => write!(f, "No action"),
471 Self::Monitor => write!(f, "Monitor"),
472 Self::ReEnroll => write!(f, "Re-enroll"),
473 Self::Investigate => write!(f, "Investigate"),
474 }
475 }
476}
477
478#[derive(Debug, Clone)]
488pub struct OperatingPoint {
489 pub threshold: f32,
491 pub far: f32,
493 pub frr: f32,
495 pub is_eer: bool,
497}
498
499#[derive(Debug, Clone)]
501pub struct OperatingCurve {
502 pub points: Vec<OperatingPoint>,
504 pub eer: f32,
506 pub eer_threshold: f32,
508}
509
510impl OperatingCurve {
511 pub fn compute(genuine_scores: &[f32], impostor_scores: &[f32], n_thresholds: usize) -> Self {
517 if genuine_scores.is_empty() || impostor_scores.is_empty() {
518 return Self {
519 points: Vec::new(),
520 eer: 1.0,
521 eer_threshold: 0.5,
522 };
523 }
524
525 let mut points = Vec::new();
526 let mut best_eer_diff = f32::MAX;
527 let mut eer = 0.5;
528 let mut eer_threshold = 0.5;
529
530 for i in 0..=n_thresholds {
531 let threshold = i as f32 / n_thresholds as f32;
532
533 let false_accepts = impostor_scores.iter().filter(|&&s| s > threshold).count();
535 let far = false_accepts as f32 / impostor_scores.len() as f32;
536
537 let false_rejects = genuine_scores.iter().filter(|&&s| s <= threshold).count();
539 let frr = false_rejects as f32 / genuine_scores.len() as f32;
540
541 let eer_diff = (far - frr).abs();
542 if eer_diff < best_eer_diff {
543 best_eer_diff = eer_diff;
544 eer = (far + frr) * 0.5;
545 eer_threshold = threshold;
546 }
547
548 points.push(OperatingPoint {
549 threshold,
550 far,
551 frr,
552 is_eer: false,
553 });
554 }
555
556 for p in &mut points {
558 if (p.threshold - eer_threshold).abs() < 1e-6 {
559 p.is_eer = true;
560 }
561 }
562
563 Self {
564 points,
565 eer,
566 eer_threshold,
567 }
568 }
569
570 pub fn threshold_at_far(&self, target_far: f32) -> Option<f32> {
572 self.points
574 .iter()
575 .filter(|p| p.far <= target_far)
576 .min_by(|a, b| a.threshold.partial_cmp(&b.threshold).unwrap())
577 .map(|p| p.threshold)
578 }
579
580 pub fn threshold_at_frr(&self, target_frr: f32) -> Option<f32> {
582 self.points
583 .iter()
584 .filter(|p| p.frr <= target_frr)
585 .max_by(|a, b| a.threshold.partial_cmp(&b.threshold).unwrap())
586 .map(|p| p.threshold)
587 }
588}
589
590#[derive(Debug, Clone)]
596pub struct BiometricConfig {
597 pub face_embed_dim: usize,
599 pub fingerprint_embed_dim: usize,
601 pub voice_embed_dim: usize,
603 pub iris_embed_dim: usize,
605 pub fusion_dim: usize,
607 pub verify_threshold: f32,
609 pub identify_top_k: usize,
611 pub liveness_threshold: f32,
613 pub quality_threshold: f32,
615 pub drift_threshold: f32,
617 pub crystallization_steps: usize,
619}
620
621impl Default for BiometricConfig {
622 fn default() -> Self {
623 Self {
624 face_embed_dim: 64,
625 fingerprint_embed_dim: 128,
626 voice_embed_dim: 64,
627 iris_embed_dim: 128,
628 fusion_dim: 48,
629 verify_threshold: 0.5,
630 identify_top_k: 5,
631 liveness_threshold: 0.6,
632 quality_threshold: 0.3,
633 drift_threshold: 0.4,
634 crystallization_steps: 5,
635 }
636 }
637}
638
639impl BiometricConfig {
640 pub fn high_security() -> Self {
642 Self {
643 verify_threshold: 0.7,
644 liveness_threshold: 0.8,
645 quality_threshold: 0.5,
646 drift_threshold: 0.3,
647 crystallization_steps: 10,
648 ..Default::default()
649 }
650 }
651
652 pub fn convenience() -> Self {
654 Self {
655 verify_threshold: 0.35,
656 liveness_threshold: 0.4,
657 quality_threshold: 0.2,
658 drift_threshold: 0.6,
659 crystallization_steps: 3,
660 ..Default::default()
661 }
662 }
663}
664
665pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
674 if a.len() != b.len() || a.is_empty() {
675 return 0.0;
676 }
677
678 let mut dot = 0.0f32;
679 let mut norm_a = 0.0f32;
680 let mut norm_b = 0.0f32;
681
682 for i in 0..a.len() {
683 dot += a[i] * b[i];
684 norm_a += a[i] * a[i];
685 norm_b += b[i] * b[i];
686 }
687
688 let denom = (norm_a.sqrt() * norm_b.sqrt()).max(1e-8);
689 dot / denom
690}
691
692pub fn l2_normalize(v: &mut [f32]) {
694 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
695 if norm > 1e-8 {
696 for x in v.iter_mut() {
697 *x /= norm;
698 }
699 }
700}
701
702pub fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
704 assert_eq!(a.len(), b.len());
705 a.iter()
706 .zip(b.iter())
707 .map(|(ai, bi)| (ai - bi) * (ai - bi))
708 .sum::<f32>()
709 .sqrt()
710}
711
712pub fn weighted_cosine_similarity(a: &[f32], b: &[f32], weights: &[f32]) -> f32 {
714 assert_eq!(a.len(), b.len());
715 assert_eq!(a.len(), weights.len());
716
717 let mut dot = 0.0f32;
718 let mut norm_a = 0.0f32;
719 let mut norm_b = 0.0f32;
720
721 for i in 0..a.len() {
722 let w = weights[i].max(0.0);
723 dot += a[i] * b[i] * w;
724 norm_a += a[i] * a[i] * w;
725 norm_b += b[i] * b[i] * w;
726 }
727
728 let denom = (norm_a.sqrt() * norm_b.sqrt()).max(1e-8);
729 dot / denom
730}
731
732pub fn entropy(probs: &[f32]) -> f32 {
735 let mut h = 0.0f32;
736 for &p in probs {
737 if p > 1e-10 {
738 h -= p * p.ln();
739 }
740 }
741 h
742}
743
744#[cfg(test)]
749mod tests {
750 use super::*;
751 use axonml_autograd::Variable;
752 use axonml_tensor::Tensor;
753
754 #[test]
757 fn test_biometric_evidence_empty() {
758 let ev = BiometricEvidence::empty();
759 assert!(ev.available_modalities().is_empty());
760 assert_eq!(ev.modality_count(), 0);
761 assert!(!ev.has_face_sequence());
762 }
763
764 #[test]
765 fn test_biometric_evidence_face_only() {
766 let face = Variable::new(Tensor::zeros(&[1, 3, 64, 64]), false);
767 let ev = BiometricEvidence::face(face);
768 let mods = ev.available_modalities();
769 assert_eq!(mods.len(), 1);
770 assert_eq!(mods[0], BiometricModality::Face);
771 }
772
773 #[test]
774 fn test_biometric_evidence_face_sequence() {
775 let frames: Vec<_> = (0..5)
776 .map(|_| Variable::new(Tensor::zeros(&[1, 3, 64, 64]), false))
777 .collect();
778 let ev = BiometricEvidence::face_sequence(frames);
779 assert!(ev.has_face_sequence());
780 assert_eq!(ev.face_sequence.as_ref().unwrap().len(), 5);
781 assert!(ev.face.is_some()); }
783
784 #[test]
785 fn test_biometric_evidence_multi_modal() {
786 let face = Variable::new(Tensor::zeros(&[1, 3, 64, 64]), false);
787 let voice = Variable::new(Tensor::zeros(&[1, 40, 100]), false);
788 let ev = BiometricEvidence::multi(Some(face), None, Some(voice), None);
789 assert_eq!(ev.modality_count(), 2);
790 assert!(ev.available_modalities().contains(&BiometricModality::Face));
791 assert!(
792 ev.available_modalities()
793 .contains(&BiometricModality::Voice)
794 );
795 }
796
797 #[test]
798 fn test_biometric_evidence_all_modalities() {
799 let face = Variable::new(Tensor::zeros(&[1, 3, 64, 64]), false);
800 let finger = Variable::new(Tensor::zeros(&[1, 1, 128, 128]), false);
801 let voice = Variable::new(Tensor::zeros(&[1, 40, 100]), false);
802 let iris = Variable::new(Tensor::zeros(&[1, 1, 32, 256]), false);
803 let ev = BiometricEvidence::multi(Some(face), Some(finger), Some(voice), Some(iris));
804 assert_eq!(ev.modality_count(), 4);
805 }
806
807 #[test]
808 fn test_biometric_evidence_metadata() {
809 let face = Variable::new(Tensor::zeros(&[1, 3, 64, 64]), false);
810 let ev = BiometricEvidence::face(face)
811 .with_timestamp(1709500000.0)
812 .with_device("cam-001".to_string());
813 assert_eq!(ev.timestamp, Some(1709500000.0));
814 assert_eq!(ev.device_id.as_deref(), Some("cam-001"));
815 }
816
817 #[test]
820 fn test_biometric_config_default() {
821 let config = BiometricConfig::default();
822 assert_eq!(config.face_embed_dim, 64);
823 assert_eq!(config.fusion_dim, 48);
824 assert_eq!(config.verify_threshold, 0.5);
825 assert_eq!(config.crystallization_steps, 5);
826 }
827
828 #[test]
829 fn test_biometric_config_high_security() {
830 let config = BiometricConfig::high_security();
831 assert!(config.verify_threshold > BiometricConfig::default().verify_threshold);
832 assert!(config.liveness_threshold > BiometricConfig::default().liveness_threshold);
833 assert!(config.quality_threshold > BiometricConfig::default().quality_threshold);
834 }
835
836 #[test]
837 fn test_biometric_config_convenience() {
838 let config = BiometricConfig::convenience();
839 assert!(config.verify_threshold < BiometricConfig::default().verify_threshold);
840 assert!(config.crystallization_steps < BiometricConfig::default().crystallization_steps);
841 }
842
843 #[test]
846 fn test_modality_display() {
847 assert_eq!(format!("{}", BiometricModality::Face), "Face (Mnemosyne)");
848 assert_eq!(format!("{}", BiometricModality::Voice), "Voice (Echo)");
849 assert_eq!(
850 format!("{}", BiometricModality::Fingerprint),
851 "Fingerprint (Ariadne)"
852 );
853 assert_eq!(format!("{}", BiometricModality::Iris), "Iris (Argus)");
854 }
855
856 #[test]
857 fn test_modality_all() {
858 let all = BiometricModality::all();
859 assert_eq!(all.len(), 4);
860 }
861
862 #[test]
863 fn test_modality_input_description() {
864 let desc = BiometricModality::Face.input_description();
865 assert!(desc.contains("64"));
866 }
867
868 #[test]
869 fn test_modality_approx_params() {
870 assert!(BiometricModality::Face.approx_params() > 10_000);
871 assert!(BiometricModality::Face.approx_params() < 200_000);
872 }
873
874 #[test]
877 fn test_liveness_result_unknown() {
878 let lr = LivenessResult::unknown();
879 assert_eq!(lr.liveness_score, 0.5);
880 assert!(!lr.is_live);
881 }
882
883 #[test]
886 fn test_quality_report_all_pass() {
887 let report = QualityReport::all_pass(&[BiometricModality::Face, BiometricModality::Voice]);
888 assert_eq!(report.overall_score, 1.0);
889 assert!(report.meets_threshold);
890 assert!(report.issues.is_empty());
891 assert_eq!(report.modality_scores.len(), 2);
892 }
893
894 #[test]
897 fn test_drift_recommendation_display() {
898 assert_eq!(format!("{}", DriftRecommendation::None), "No action");
899 assert_eq!(format!("{}", DriftRecommendation::ReEnroll), "Re-enroll");
900 assert_eq!(
901 format!("{}", DriftRecommendation::Investigate),
902 "Investigate"
903 );
904 }
905
906 #[test]
909 fn test_operating_curve_perfect_separation() {
910 let genuine: Vec<f32> = (0..100).map(|i| 0.8 + 0.2 * (i as f32 / 100.0)).collect();
912 let impostor: Vec<f32> = (0..100).map(|i| 0.0 + 0.2 * (i as f32 / 100.0)).collect();
913 let curve = OperatingCurve::compute(&genuine, &impostor, 100);
914
915 assert!(!curve.points.is_empty());
916 assert!(curve.eer < 0.1, "EER should be very low: {}", curve.eer);
917 }
918
919 #[test]
920 fn test_operating_curve_overlapping() {
921 let genuine: Vec<f32> = (0..100).map(|i| 0.3 + 0.4 * (i as f32 / 100.0)).collect();
922 let impostor: Vec<f32> = (0..100).map(|i| 0.2 + 0.4 * (i as f32 / 100.0)).collect();
923 let curve = OperatingCurve::compute(&genuine, &impostor, 100);
924
925 assert!(curve.eer > 0.0);
926 assert!(curve.eer < 1.0);
927 }
928
929 #[test]
930 fn test_operating_curve_empty() {
931 let curve = OperatingCurve::compute(&[], &[], 100);
932 assert!(curve.points.is_empty());
933 assert_eq!(curve.eer, 1.0);
934 }
935
936 #[test]
937 fn test_operating_curve_threshold_at_far() {
938 let genuine: Vec<f32> = (0..100).map(|i| 0.7 + 0.3 * (i as f32 / 100.0)).collect();
939 let impostor: Vec<f32> = (0..100).map(|i| 0.0 + 0.3 * (i as f32 / 100.0)).collect();
940 let curve = OperatingCurve::compute(&genuine, &impostor, 100);
941
942 let threshold = curve.threshold_at_far(0.01);
943 assert!(threshold.is_some());
944 }
945
946 #[test]
949 fn test_cosine_similarity_identical() {
950 let a = vec![1.0, 0.0, 0.0];
951 assert!((cosine_similarity(&a, &a) - 1.0).abs() < 0.001);
952 }
953
954 #[test]
955 fn test_cosine_similarity_orthogonal() {
956 let a = vec![1.0, 0.0, 0.0];
957 let b = vec![0.0, 1.0, 0.0];
958 assert!(cosine_similarity(&a, &b).abs() < 0.001);
959 }
960
961 #[test]
962 fn test_cosine_similarity_opposite() {
963 let a = vec![1.0, 0.0];
964 let b = vec![-1.0, 0.0];
965 assert!((cosine_similarity(&a, &b) + 1.0).abs() < 0.001);
966 }
967
968 #[test]
969 fn test_cosine_similarity_empty() {
970 assert_eq!(cosine_similarity(&[], &[]), 0.0);
971 }
972
973 #[test]
974 fn test_cosine_similarity_mismatched() {
975 assert_eq!(cosine_similarity(&[1.0], &[1.0, 2.0]), 0.0);
976 }
977
978 #[test]
979 fn test_l2_normalize() {
980 let mut v = vec![3.0, 4.0];
981 l2_normalize(&mut v);
982 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
983 assert!((norm - 1.0).abs() < 0.001);
984 assert!((v[0] - 0.6).abs() < 0.001);
985 assert!((v[1] - 0.8).abs() < 0.001);
986 }
987
988 #[test]
989 fn test_l2_normalize_zero_vector() {
990 let mut v = vec![0.0, 0.0, 0.0];
991 l2_normalize(&mut v); assert_eq!(v, vec![0.0, 0.0, 0.0]);
993 }
994
995 #[test]
996 fn test_euclidean_distance_same() {
997 let a = vec![1.0, 2.0, 3.0];
998 assert!(euclidean_distance(&a, &a) < 0.001);
999 }
1000
1001 #[test]
1002 fn test_euclidean_distance_known() {
1003 let a = vec![0.0, 0.0];
1004 let b = vec![3.0, 4.0];
1005 assert!((euclidean_distance(&a, &b) - 5.0).abs() < 0.001);
1006 }
1007
1008 #[test]
1009 fn test_weighted_cosine_similarity() {
1010 let a = vec![1.0, 0.0, 0.0];
1011 let b = vec![1.0, 0.0, 0.0];
1012 let w = vec![1.0, 1.0, 1.0];
1013 assert!((weighted_cosine_similarity(&a, &b, &w) - 1.0).abs() < 0.001);
1014 }
1015
1016 #[test]
1017 fn test_weighted_cosine_zero_weight() {
1018 let a = vec![1.0, 0.0];
1020 let b = vec![1.0, 1.0];
1021 let w = vec![1.0, 0.0]; let sim = weighted_cosine_similarity(&a, &b, &w);
1023 assert!(
1024 (sim - 1.0).abs() < 0.001,
1025 "Should ignore zero-weighted dim: {}",
1026 sim
1027 );
1028 }
1029
1030 #[test]
1031 fn test_entropy_uniform() {
1032 let probs = vec![0.25, 0.25, 0.25, 0.25];
1033 let h = entropy(&probs);
1034 let expected = -(4.0 * 0.25 * 0.25f32.ln());
1035 assert!((h - expected).abs() < 0.001);
1036 }
1037
1038 #[test]
1039 fn test_entropy_certain() {
1040 let probs = vec![1.0, 0.0, 0.0];
1041 let h = entropy(&probs);
1042 assert!(
1043 h < 0.001,
1044 "Certain distribution should have ~0 entropy: {}",
1045 h
1046 );
1047 }
1048}