Skip to main content

entrenar/monitor/inference/
mod.rs

1//! Real-Time Audit Log & Explainability for APR Format Models
2//!
3//! # Toyota Way: 現地現物 (Genchi Genbutsu)
4//! Every decision is traceable to ground truth. All predictions can be explained.
5//!
6//! # Architecture
7//!
8//! - **DecisionPath**: Model-specific explanation of how a decision was made
9//! - **DecisionTrace**: Complete record of a single prediction
10//! - **Explainable**: Trait for models that can explain their predictions
11//! - **TraceCollector**: Strategy for collecting decision traces
12//!
13//! # Collectors
14//!
15//! - **RingCollector**: Stack-allocated, <100ns, for games/drones
16//! - **StreamCollector**: Write-through, <1µs, for persistent logging
17//! - **HashChainCollector**: SHA-256 chain, <10µs, for safety-critical
18//!
19//! # Example
20//!
21//! ```ignore
22//! use entrenar::monitor::inference::{
23//!     InferenceMonitor, RingCollector, LinearPath,
24//! };
25//!
26//! let model = LinearRegressor::load("model.apr")?;
27//! let collector = RingCollector::<LinearPath, 64>::new();
28//! let mut monitor = InferenceMonitor::new(model, collector);
29//!
30//! let prediction = monitor.predict(&input);
31//! let traces = monitor.traces();
32//! ```
33
34pub mod collector;
35pub mod counterfactual;
36pub mod path;
37pub mod provenance;
38pub mod safety_andon;
39pub mod serialization;
40pub mod trace;
41
42#[cfg(test)]
43mod tests;
44
45// Re-exports
46pub use collector::{
47    ChainEntry, ChainVerification, HashChainCollector, RingCollector, StreamCollector,
48    StreamFormat, TraceCollector,
49};
50pub use counterfactual::{Counterfactual, FeatureChange};
51pub use path::{
52    DecisionPath, ForestPath, KNNPath, LeafInfo, LinearPath, NeuralPath, PathError, TreePath,
53    TreeSplit,
54};
55pub use provenance::{
56    Anomaly, AttackPath, CausalRelation, IncidentReconstructor, NodeId, ProvenanceEdge,
57    ProvenanceGraph, ProvenanceNode,
58};
59pub use safety_andon::{EmergencyCondition, SafetyAndon, SafetyIntegrityLevel};
60pub use serialization::{
61    PathType, SerializationError, TraceFormat, TraceSerializer, APRT_MAGIC, APRT_VERSION,
62};
63pub use trace::DecisionTrace;
64
65use std::time::Instant;
66
67/// Monotonic nanosecond timestamp
68#[inline]
69pub fn monotonic_ns() -> u64 {
70    // Use Instant for monotonic clock
71    static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
72    let start = START.get_or_init(Instant::now);
73    start.elapsed().as_nanos() as u64
74}
75
76/// FNV-1a hash for input features
77#[inline]
78pub fn fnv1a_hash(data: &[u8]) -> u64 {
79    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
80    const FNV_PRIME: u64 = 0x100000001b3;
81
82    let mut hash = FNV_OFFSET;
83    for byte in data {
84        hash ^= u64::from(*byte);
85        hash = hash.wrapping_mul(FNV_PRIME);
86    }
87    hash
88}
89
90/// Hash a slice of f32 values
91#[inline]
92pub fn hash_features(features: &[f32]) -> u64 {
93    let bytes: &[u8] = bytemuck::cast_slice(features);
94    fnv1a_hash(bytes)
95}
96
97// GH-305: Explainable trait now lives in aprender (source of truth).
98// Re-exported here for backwards compatibility.
99pub use aprender::explainable::path::Explainable;
100
101/// High-level inference monitor
102///
103/// Wraps a model and collector to automatically trace all predictions.
104pub struct InferenceMonitor<M, C>
105where
106    M: Explainable,
107    C: TraceCollector<M::Path>,
108{
109    model: M,
110    collector: C,
111    andon: Option<SafetyAndon>,
112    latency_budget_ns: u64,
113    sequence: u64,
114}
115
116impl<M, C> InferenceMonitor<M, C>
117where
118    M: Explainable,
119    C: TraceCollector<M::Path>,
120{
121    /// Create a new inference monitor
122    pub fn new(model: M, collector: C) -> Self {
123        Self {
124            model,
125            collector,
126            andon: None,
127            latency_budget_ns: 10_000_000, // 10ms default
128            sequence: 0,
129        }
130    }
131
132    /// Set the Andon system for alerting
133    pub fn with_andon(mut self, andon: SafetyAndon) -> Self {
134        self.andon = Some(andon);
135        self
136    }
137
138    /// Set the latency budget in nanoseconds
139    pub fn with_latency_budget_ns(mut self, budget: u64) -> Self {
140        self.latency_budget_ns = budget;
141        self
142    }
143
144    /// Predict with automatic tracing
145    pub fn predict(&mut self, x: &[f32], n_samples: usize) -> Vec<f32> {
146        let start = Instant::now();
147        let timestamp_ns = monotonic_ns();
148
149        let (outputs, paths) = self.model.predict_explained(x, n_samples);
150
151        let elapsed_ns = start.elapsed().as_nanos() as u64;
152
153        let features_per_sample = x.len() / n_samples;
154
155        for (i, (output, path)) in outputs.iter().zip(paths.into_iter()).enumerate() {
156            let sample_start = i * features_per_sample;
157            let sample_end = sample_start + features_per_sample;
158            let sample_features = &x[sample_start..sample_end];
159
160            let trace = DecisionTrace {
161                timestamp_ns,
162                sequence: self.sequence,
163                input_hash: hash_features(sample_features),
164                path,
165                output: *output,
166                latency_ns: elapsed_ns,
167            };
168
169            self.sequence += 1;
170
171            // Andon checks
172            if let Some(andon) = &mut self.andon {
173                andon.check_trace(&trace, self.latency_budget_ns);
174            }
175
176            self.collector.record(trace);
177        }
178
179        outputs
180    }
181
182    /// Get reference to the collector
183    pub fn collector(&self) -> &C {
184        &self.collector
185    }
186
187    /// Get mutable reference to the collector
188    pub fn collector_mut(&mut self) -> &mut C {
189        &mut self.collector
190    }
191
192    /// Get reference to the model
193    pub fn model(&self) -> &M {
194        &self.model
195    }
196
197    /// Get the current sequence number
198    pub fn sequence(&self) -> u64 {
199        self.sequence
200    }
201}