pub mod collector;
pub mod counterfactual;
pub mod path;
pub mod provenance;
pub mod safety_andon;
pub mod serialization;
pub mod trace;
#[cfg(test)]
mod tests;
pub use collector::{
ChainEntry, ChainVerification, HashChainCollector, RingCollector, StreamCollector,
StreamFormat, TraceCollector,
};
pub use counterfactual::{Counterfactual, FeatureChange};
pub use path::{
DecisionPath, ForestPath, KNNPath, LeafInfo, LinearPath, NeuralPath, PathError, TreePath,
TreeSplit,
};
pub use provenance::{
Anomaly, AttackPath, CausalRelation, IncidentReconstructor, NodeId, ProvenanceEdge,
ProvenanceGraph, ProvenanceNode,
};
pub use safety_andon::{EmergencyCondition, SafetyAndon, SafetyIntegrityLevel};
pub use serialization::{
PathType, SerializationError, TraceFormat, TraceSerializer, APRT_MAGIC, APRT_VERSION,
};
pub use trace::DecisionTrace;
use std::time::Instant;
#[inline]
pub fn monotonic_ns() -> u64 {
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
let start = START.get_or_init(Instant::now);
start.elapsed().as_nanos() as u64
}
#[inline]
pub fn fnv1a_hash(data: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET;
for byte in data {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[inline]
pub fn hash_features(features: &[f32]) -> u64 {
let bytes: &[u8] = bytemuck::cast_slice(features);
fnv1a_hash(bytes)
}
pub use aprender::explainable::path::Explainable;
pub struct InferenceMonitor<M, C>
where
M: Explainable,
C: TraceCollector<M::Path>,
{
model: M,
collector: C,
andon: Option<SafetyAndon>,
latency_budget_ns: u64,
sequence: u64,
}
impl<M, C> InferenceMonitor<M, C>
where
M: Explainable,
C: TraceCollector<M::Path>,
{
pub fn new(model: M, collector: C) -> Self {
Self {
model,
collector,
andon: None,
latency_budget_ns: 10_000_000, sequence: 0,
}
}
pub fn with_andon(mut self, andon: SafetyAndon) -> Self {
self.andon = Some(andon);
self
}
pub fn with_latency_budget_ns(mut self, budget: u64) -> Self {
self.latency_budget_ns = budget;
self
}
pub fn predict(&mut self, x: &[f32], n_samples: usize) -> Vec<f32> {
let start = Instant::now();
let timestamp_ns = monotonic_ns();
let (outputs, paths) = self.model.predict_explained(x, n_samples);
let elapsed_ns = start.elapsed().as_nanos() as u64;
let features_per_sample = x.len() / n_samples;
for (i, (output, path)) in outputs.iter().zip(paths.into_iter()).enumerate() {
let sample_start = i * features_per_sample;
let sample_end = sample_start + features_per_sample;
let sample_features = &x[sample_start..sample_end];
let trace = DecisionTrace {
timestamp_ns,
sequence: self.sequence,
input_hash: hash_features(sample_features),
path,
output: *output,
latency_ns: elapsed_ns,
};
self.sequence += 1;
if let Some(andon) = &mut self.andon {
andon.check_trace(&trace, self.latency_budget_ns);
}
self.collector.record(trace);
}
outputs
}
pub fn collector(&self) -> &C {
&self.collector
}
pub fn collector_mut(&mut self) -> &mut C {
&mut self.collector
}
pub fn model(&self) -> &M {
&self.model
}
pub fn sequence(&self) -> u64 {
self.sequence
}
}