Skip to main content

axonml_vision/models/biometric/
mod.rs

1//! Aegis Identity — Novel Unified Biometric Framework
2//!
3//! # File
4//! `crates/axonml-vision/src/models/biometric/mod.rs`
5//!
6//! # Author
7//! Andrew Jewell Sr - AutomataNexus
8//!
9//! # Updated
10//! March 8, 2026
11//!
12//! # Disclaimer
13//! Use at own risk. This software is provided "as is", without warranty of any
14//! kind, express or implied. The author and AutomataNexus shall not be held
15//! liable for any damages arising from the use of this software.
16
17pub mod argus;
18pub mod ariadne;
19pub mod echo;
20pub mod identity;
21pub mod losses;
22pub mod mnemosyne;
23pub mod polar;
24pub mod themis;
25
26// =============================================================================
27// Re-exports
28// =============================================================================
29
30pub 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// =============================================================================
42// Types — Modality & Evidence
43// =============================================================================
44
45/// Biometric modality identifier.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum BiometricModality {
48    /// Face — temporal crystallization (Mnemosyne)
49    Face,
50    /// Fingerprint — ridge event fields (Ariadne)
51    Fingerprint,
52    /// Voice — predictive speaker residuals (Echo)
53    Voice,
54    /// Iris — radial phase encoding (Argus)
55    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    /// All available modalities.
71    pub fn all() -> Vec<Self> {
72        vec![Self::Face, Self::Fingerprint, Self::Voice, Self::Iris]
73    }
74
75    /// Expected input tensor shape description.
76    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    /// Approximate model parameter count for this modality.
86    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/// Biometric evidence from one or more modalities.
97///
98/// Each field is optional — the system gracefully degrades when modalities
99/// are missing (their uncertainty gates go to zero in Themis).
100#[derive(Debug, Clone)]
101pub struct BiometricEvidence {
102    /// Face image tensor [B, 3, 64, 64]
103    pub face: Option<axonml_autograd::Variable>,
104    /// Fingerprint image tensor [B, 1, 128, 128]
105    pub fingerprint: Option<axonml_autograd::Variable>,
106    /// Voice mel spectrogram tensor [B, 40, T] (variable length)
107    pub voice: Option<axonml_autograd::Variable>,
108    /// Iris polar strip tensor [B, 1, 32, 256]
109    pub iris: Option<axonml_autograd::Variable>,
110    /// Optional sequence of face frames for temporal crystallization [N × [B, 3, 64, 64]]
111    pub face_sequence: Option<Vec<axonml_autograd::Variable>>,
112    /// Metadata: capture timestamp (seconds since epoch)
113    pub timestamp: Option<f64>,
114    /// Metadata: capture device identifier
115    pub device_id: Option<String>,
116}
117
118impl BiometricEvidence {
119    /// Create empty evidence (no modalities).
120    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    /// Create face-only evidence.
133    pub fn face(tensor: axonml_autograd::Variable) -> Self {
134        Self {
135            face: Some(tensor),
136            ..Self::empty()
137        }
138    }
139
140    /// Create face evidence with temporal sequence for crystallization.
141    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    /// Create fingerprint-only evidence.
151    pub fn fingerprint(tensor: axonml_autograd::Variable) -> Self {
152        Self {
153            fingerprint: Some(tensor),
154            ..Self::empty()
155        }
156    }
157
158    /// Create voice-only evidence.
159    pub fn voice(tensor: axonml_autograd::Variable) -> Self {
160        Self {
161            voice: Some(tensor),
162            ..Self::empty()
163        }
164    }
165
166    /// Create iris-only evidence.
167    pub fn iris(tensor: axonml_autograd::Variable) -> Self {
168        Self {
169            iris: Some(tensor),
170            ..Self::empty()
171        }
172    }
173
174    /// Create multi-modal evidence.
175    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    /// Attach timestamp to evidence.
193    pub fn with_timestamp(mut self, ts: f64) -> Self {
194        self.timestamp = Some(ts);
195        self
196    }
197
198    /// Attach device identifier.
199    pub fn with_device(mut self, device: String) -> Self {
200        self.device_id = Some(device);
201        self
202    }
203
204    /// Which modalities are present.
205    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    /// Number of modalities present.
223    pub fn modality_count(&self) -> usize {
224        self.available_modalities().len()
225    }
226
227    /// Whether this evidence has temporal face data.
228    pub fn has_face_sequence(&self) -> bool {
229        self.face_sequence.as_ref().is_some_and(|s| s.len() > 1)
230    }
231}
232
233// =============================================================================
234// Types — Results & Outputs
235// =============================================================================
236
237/// Result from modality-specific processing: embedding + uncertainty.
238#[derive(Debug, Clone)]
239pub struct ModalityOutput {
240    /// L2-normalized embedding vector [embed_dim]
241    pub embedding: Vec<f32>,
242    /// Uncertainty estimate (log-variance). Lower = more confident.
243    pub log_variance: f32,
244    /// Which modality produced this.
245    pub modality: BiometricModality,
246}
247
248/// Result of an enrollment operation.
249#[derive(Debug, Clone)]
250pub struct EnrollmentResult {
251    /// Whether enrollment succeeded.
252    pub success: bool,
253    /// Subject ID enrolled.
254    pub subject_id: u64,
255    /// Per-modality embeddings stored.
256    pub modalities_enrolled: Vec<BiometricModality>,
257    /// Number of observations accumulated (for crystallization).
258    pub observation_count: usize,
259    /// Quality score for this enrollment [0, 1] — higher is better.
260    pub quality_score: f32,
261}
262
263/// Result of a verification (1:1) operation.
264#[derive(Debug, Clone)]
265pub struct VerificationResult {
266    /// Fused match probability [0, 1].
267    pub match_score: f32,
268    /// Whether the match exceeds the threshold.
269    pub is_match: bool,
270    /// Per-modality similarity scores.
271    pub modality_scores: Vec<(BiometricModality, f32)>,
272    /// Fused confidence (from Themis uncertainty).
273    pub confidence: f32,
274    /// Decision threshold used.
275    pub threshold: f32,
276}
277
278/// Result of an identification (1:N) operation.
279#[derive(Debug, Clone)]
280pub struct IdentificationResult {
281    /// Top candidate matches, sorted by score descending.
282    pub candidates: Vec<IdentificationCandidate>,
283    /// Fused confidence.
284    pub confidence: f32,
285}
286
287/// A single identification candidate.
288#[derive(Debug, Clone)]
289pub struct IdentificationCandidate {
290    /// Subject ID.
291    pub subject_id: u64,
292    /// Match score.
293    pub score: f32,
294    /// Per-modality scores.
295    pub modality_scores: Vec<(BiometricModality, f32)>,
296}
297
298// =============================================================================
299// Types — Liveness & Anti-Spoofing
300// =============================================================================
301
302/// Result of liveness (anti-spoofing) analysis.
303///
304/// Temporal liveness detection is a novel paradigm unique to AxonML's
305/// crystallization architecture. Real biometrics exhibit micro-variations
306/// in the GRU hidden state trajectory; spoofed inputs (photos, recordings)
307/// produce abnormally smooth or repetitive trajectories.
308#[derive(Debug, Clone)]
309pub struct LivenessResult {
310    /// Liveness probability [0, 1]. Above threshold = live.
311    pub liveness_score: f32,
312    /// Whether the input is judged as live (not spoofed).
313    pub is_live: bool,
314    /// Temporal variance of hidden state updates.
315    /// Real biometrics: high variance. Spoofed: low variance.
316    pub temporal_variance: f32,
317    /// Trajectory smoothness (autocorrelation of hidden state deltas).
318    /// Real: low autocorrelation. Replay: high autocorrelation.
319    pub trajectory_smoothness: f32,
320    /// Per-modality liveness indicators.
321    pub modality_liveness: Vec<(BiometricModality, f32)>,
322}
323
324impl LivenessResult {
325    /// Create a default "unknown" liveness result.
326    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// =============================================================================
338// Types — Quality Assessment
339// =============================================================================
340
341/// Quality assessment for biometric evidence.
342///
343/// Evaluates whether input data is suitable for reliable biometric
344/// recognition before processing. Poor quality inputs should trigger
345/// re-capture rather than producing unreliable results.
346#[derive(Debug, Clone)]
347pub struct QualityReport {
348    /// Overall quality score [0, 1]. Below 0.3 = reject.
349    pub overall_score: f32,
350    /// Per-modality quality scores.
351    pub modality_scores: Vec<(BiometricModality, f32)>,
352    /// Whether the evidence meets minimum quality requirements.
353    pub meets_threshold: bool,
354    /// Human-readable quality issues detected.
355    pub issues: Vec<QualityIssue>,
356}
357
358/// A specific quality issue detected in biometric evidence.
359#[derive(Debug, Clone)]
360pub struct QualityIssue {
361    /// Which modality has the issue.
362    pub modality: BiometricModality,
363    /// Issue severity [0, 1]. Higher = more severe.
364    pub severity: f32,
365    /// Description of the issue.
366    pub description: String,
367}
368
369impl QualityReport {
370    /// Create a report indicating all modalities passed quality checks.
371    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// =============================================================================
382// Types — Forensic Analysis
383// =============================================================================
384
385/// Forensic analysis of a biometric match/non-match decision.
386///
387/// Provides explainability for biometric decisions — critical for
388/// audit trails, legal compliance, and debugging false matches.
389#[derive(Debug, Clone)]
390pub struct ForensicReport {
391    /// Per-modality detailed breakdown.
392    pub modality_reports: Vec<ModalityForensic>,
393    /// Cross-modal consistency score [0, 1].
394    /// High = all modalities agree. Low = conflicting evidence.
395    pub cross_modal_consistency: f32,
396    /// Which modality most influenced the final decision.
397    pub dominant_modality: Option<BiometricModality>,
398    /// Which modality is most uncertain.
399    pub weakest_modality: Option<BiometricModality>,
400    /// Per-dimension contribution to the match score (top-K most influential).
401    pub top_contributing_dimensions: Vec<DimensionContribution>,
402}
403
404/// Forensic breakdown for a single modality.
405#[derive(Debug, Clone)]
406pub struct ModalityForensic {
407    /// Which modality.
408    pub modality: BiometricModality,
409    /// Raw similarity score before fusion.
410    pub raw_score: f32,
411    /// Uncertainty of the modality output.
412    pub uncertainty: f32,
413    /// Weight assigned by Themis fusion.
414    pub fusion_weight: f32,
415    /// Whether this modality agreed with the final decision.
416    pub agrees_with_decision: bool,
417}
418
419/// Contribution of a single embedding dimension to the match decision.
420#[derive(Debug, Clone)]
421pub struct DimensionContribution {
422    /// Embedding dimension index.
423    pub dimension: usize,
424    /// Contribution magnitude (positive = toward match, negative = against).
425    pub contribution: f32,
426    /// Which modality owns this dimension.
427    pub modality: BiometricModality,
428}
429
430// =============================================================================
431// Types — Identity Drift
432// =============================================================================
433
434/// Alert for identity drift — gradual change in biometric template.
435///
436/// Novel to AxonML: by tracking the trajectory of crystallized embeddings
437/// over time, we can detect when a person's biometrics are drifting
438/// (aging, injury, weight change) and trigger re-enrollment.
439#[derive(Debug, Clone)]
440pub struct DriftAlert {
441    /// Subject ID affected.
442    pub subject_id: u64,
443    /// How much the template has drifted since enrollment.
444    /// Cosine distance between current and original embedding.
445    pub drift_magnitude: f32,
446    /// Rate of drift (distance per observation).
447    pub drift_rate: f32,
448    /// Which modalities show drift.
449    pub affected_modalities: Vec<(BiometricModality, f32)>,
450    /// Recommended action.
451    pub recommendation: DriftRecommendation,
452}
453
454/// Recommendation for handling identity drift.
455#[derive(Debug, Clone, Copy, PartialEq)]
456pub enum DriftRecommendation {
457    /// No action needed — drift within normal bounds.
458    None,
459    /// Monitor: drift is approaching threshold.
460    Monitor,
461    /// Re-enroll: significant drift detected.
462    ReEnroll,
463    /// Investigate: abnormal drift pattern (possible impostor).
464    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// =============================================================================
479// Types — Operating Point Analysis
480// =============================================================================
481
482/// Operating point on a FAR/FRR curve.
483///
484/// For real-world deployment, you need to choose a threshold that balances
485/// False Accept Rate (letting impostors in) vs False Reject Rate (locking
486/// out legitimate users).
487#[derive(Debug, Clone)]
488pub struct OperatingPoint {
489    /// Decision threshold at this point.
490    pub threshold: f32,
491    /// False Accept Rate at this threshold.
492    pub far: f32,
493    /// False Reject Rate at this threshold.
494    pub frr: f32,
495    /// Equal Error Rate point (where FAR = FRR).
496    pub is_eer: bool,
497}
498
499/// Full operating curve for threshold selection.
500#[derive(Debug, Clone)]
501pub struct OperatingCurve {
502    /// Points on the FAR/FRR curve.
503    pub points: Vec<OperatingPoint>,
504    /// Equal Error Rate (EER) — where FAR = FRR.
505    pub eer: f32,
506    /// Threshold at EER.
507    pub eer_threshold: f32,
508}
509
510impl OperatingCurve {
511    /// Compute operating curve from genuine and impostor score distributions.
512    ///
513    /// * `genuine_scores` - Match scores for same-identity comparisons
514    /// * `impostor_scores` - Match scores for different-identity comparisons
515    /// * `n_thresholds` - Number of threshold points to evaluate
516    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            // FAR = fraction of impostor scores above threshold
534            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            // FRR = fraction of genuine scores below threshold
538            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        // Mark EER point
557        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    /// Find threshold for a target FAR.
571    pub fn threshold_at_far(&self, target_far: f32) -> Option<f32> {
572        // Find the highest threshold where FAR <= target_far
573        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    /// Find threshold for a target FRR.
581    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// =============================================================================
591// Configuration
592// =============================================================================
593
594/// Configuration for the biometric system.
595#[derive(Debug, Clone)]
596pub struct BiometricConfig {
597    /// Face embedding dimension.
598    pub face_embed_dim: usize,
599    /// Fingerprint embedding dimension.
600    pub fingerprint_embed_dim: usize,
601    /// Voice embedding dimension.
602    pub voice_embed_dim: usize,
603    /// Iris embedding dimension.
604    pub iris_embed_dim: usize,
605    /// Fusion common dimension.
606    pub fusion_dim: usize,
607    /// Verification threshold.
608    pub verify_threshold: f32,
609    /// Number of top-K candidates for identification.
610    pub identify_top_k: usize,
611    /// Liveness detection threshold (above = live).
612    pub liveness_threshold: f32,
613    /// Minimum quality score to proceed with recognition.
614    pub quality_threshold: f32,
615    /// Identity drift threshold before triggering re-enrollment.
616    pub drift_threshold: f32,
617    /// Number of crystallization steps for temporal face processing.
618    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    /// Security-hardened configuration (higher thresholds, stricter checks).
641    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    /// Convenience-optimized configuration (lower thresholds, faster).
653    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
665// =============================================================================
666// Utility Functions
667// =============================================================================
668
669/// Cosine similarity between two f32 vectors.
670///
671/// Returns 0.0 for mismatched or empty vectors. For L2-normalized inputs,
672/// this equals the dot product.
673pub 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
692/// L2-normalize a vector in-place.
693pub 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
702/// Euclidean distance between two vectors.
703pub 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
712/// Weighted cosine similarity with per-dimension precision weighting.
713pub 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
732/// Compute the entropy of a probability distribution.
733/// Higher entropy = more uncertain.
734pub 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// =============================================================================
745// Tests
746// =============================================================================
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use axonml_autograd::Variable;
752    use axonml_tensor::Tensor;
753
754    // --- BiometricEvidence ---
755
756    #[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()); // First frame copied to face
782    }
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    // --- BiometricConfig ---
818
819    #[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    // --- BiometricModality ---
844
845    #[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    // --- LivenessResult ---
875
876    #[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    // --- QualityReport ---
884
885    #[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    // --- DriftRecommendation ---
895
896    #[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    // --- OperatingCurve ---
907
908    #[test]
909    fn test_operating_curve_perfect_separation() {
910        // Genuine scores all above 0.8, impostor scores all below 0.2
911        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    // --- Utility functions ---
947
948    #[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); // Should not panic
992        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        // If weight on differing dimension is zero, similarity should be high
1019        let a = vec![1.0, 0.0];
1020        let b = vec![1.0, 1.0];
1021        let w = vec![1.0, 0.0]; // Ignore second dimension
1022        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}