use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Salience {
pub urgency: f32,
pub novelty: f32,
pub confidence: f32,
}
impl Default for Salience {
fn default() -> Self {
Self {
urgency: 0.0,
novelty: 0.0,
confidence: 1.0,
}
}
}
impl Salience {
#[must_use]
pub const fn new(urgency: f32, novelty: f32, confidence: f32) -> Self {
Self {
urgency,
novelty,
confidence,
}
}
#[must_use]
pub fn composite(&self) -> f32 {
self.urgency * self.novelty * self.confidence
}
#[must_use]
pub fn is_high_salience(&self) -> bool {
self.composite() > 0.8
}
#[must_use]
pub fn is_negligible(&self) -> bool {
self.composite() < 0.01
}
#[must_use]
pub const fn clamped(&self) -> Self {
Self {
urgency: self.urgency.clamp(0.0, 1.0),
novelty: self.novelty.clamp(0.0, 1.0),
confidence: self.confidence.clamp(0.0, 1.0),
}
}
#[must_use]
pub const fn sanitized(&self) -> Self {
const fn clean(v: f32) -> f32 {
if v.is_nan() || v.is_infinite() {
0.0
} else {
v.clamp(0.0, 1.0)
}
}
Self {
urgency: clean(self.urgency),
novelty: clean(self.novelty),
confidence: clean(self.confidence),
}
}
}
impl std::fmt::Display for Salience {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Salience({:.2}×{:.2}×{:.2}={:.4})",
self.urgency,
self.novelty,
self.confidence,
self.composite()
)
}
}
#[must_use]
pub const fn default_urgency(event_type: &crate::event::EventType) -> f32 {
use crate::event::EventType;
match event_type {
EventType::SafetyAlert => 1.0,
EventType::Error => 0.9,
EventType::ThresholdCrossing => 0.8,
EventType::AttentionRequest => 0.6,
EventType::NovelDetection => 0.4,
EventType::Reward => 0.3,
EventType::DriveUpdate => 0.2,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn composite_multiplicative() {
let s = Salience::new(0.8, 0.5, 1.0);
assert_eq!(s.composite(), 0.4);
}
#[test]
fn composite_zero_urgency() {
let s = Salience::new(0.0, 1.0, 1.0);
assert_eq!(s.composite(), 0.0);
}
#[test]
fn composite_zero_novelty() {
let s = Salience::new(1.0, 0.0, 1.0);
assert_eq!(s.composite(), 0.0);
}
#[test]
fn composite_zero_confidence() {
let s = Salience::new(1.0, 1.0, 0.0);
assert_eq!(s.composite(), 0.0);
}
#[test]
fn composite_all_max() {
let s = Salience::new(1.0, 1.0, 1.0);
assert_eq!(s.composite(), 1.0);
}
#[test]
fn is_high_salience() {
assert!(Salience::new(0.95, 0.95, 0.95).is_high_salience());
assert!(!Salience::new(0.5, 0.5, 0.5).is_high_salience());
}
#[test]
fn is_negligible() {
assert!(Salience::new(0.01, 0.01, 0.01).is_negligible());
assert!(!Salience::new(0.5, 0.5, 0.5).is_negligible());
}
#[test]
fn clamped() {
let s = Salience::new(1.5, -0.5, 2.0);
let c = s.clamped();
assert_eq!(c.urgency, 1.0);
assert_eq!(c.novelty, 0.0);
assert_eq!(c.confidence, 1.0);
}
#[test]
fn default_salience_has_confidence_1() {
let s = Salience::default();
assert_eq!(s.confidence, 1.0);
assert_eq!(s.urgency, 0.0);
assert_eq!(s.novelty, 0.0);
}
#[test]
fn display_format() {
let s = Salience::new(0.5, 0.4, 0.8);
let display = format!("{s}");
assert!(display.contains("0.50"));
assert!(display.contains("0.16"));
}
#[test]
fn default_urgency_safety_alert() {
use crate::event::EventType;
assert_eq!(default_urgency(&EventType::SafetyAlert), 1.0);
}
#[test]
fn default_urgency_error() {
use crate::event::EventType;
assert_eq!(default_urgency(&EventType::Error), 0.9);
}
#[test]
fn default_urgency_drive_update() {
use crate::event::EventType;
assert_eq!(default_urgency(&EventType::DriveUpdate), 0.2);
}
#[test]
fn sanitized_replaces_nan_and_infinity() {
let s = Salience::new(f32::NAN, f32::INFINITY, -0.5);
let clean = s.sanitized();
assert_eq!(clean.urgency, 0.0, "NaN should become 0.0");
assert_eq!(clean.novelty, 0.0, "Infinity should become 0.0");
assert_eq!(clean.confidence, 0.0, "Negative should become 0.0");
}
#[test]
fn sanitized_clamps_oversized() {
let s = Salience::new(2.0, 1.5, 0.8);
let clean = s.sanitized();
assert_eq!(clean.urgency, 1.0);
assert_eq!(clean.novelty, 1.0);
assert_eq!(clean.confidence, 0.8);
}
#[test]
fn sanitized_preserves_valid() {
let s = Salience::new(0.5, 0.3, 0.9);
let clean = s.sanitized();
assert_eq!(clean, s);
}
}