Skip to main content

persistence_agent/
agent_features.rs

1use serde::{Deserialize, Serialize};
2use crate::barcode::Barcode;
3use crate::point_cloud::PointCloud;
4use crate::vietoris_rips::VietorisRipsComplex;
5
6/// Topological archetype of an agent's behavior pattern.
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub enum AgentArchetype {
9    /// Single persistent cluster — stable, predictable behavior.
10    Steady,
11    /// Many short-lived loops — exploration-heavy, wandering.
12    Explorer,
13    /// Many disconnected components — volatile, switching between modes.
14    Volatile,
15    /// Long-lived higher-dimensional features — complex, structured behavior.
16    Deep,
17    /// Mix of features — no dominant topological signature.
18    Balanced,
19}
20
21impl std::fmt::Display for AgentArchetype {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            AgentArchetype::Steady => write!(f, "Steady"),
25            AgentArchetype::Explorer => write!(f, "Explorer"),
26            AgentArchetype::Volatile => write!(f, "Volatile"),
27            AgentArchetype::Deep => write!(f, "Deep"),
28            AgentArchetype::Balanced => write!(f, "Balanced"),
29        }
30    }
31}
32
33/// A topological profile of an agent's behavior.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AgentProfile {
36    pub archetype: AgentArchetype,
37    pub persistence_entropy: f64,
38    pub max_persistence: f64,
39    pub betti_numbers: Vec<usize>,
40}
41
42/// Profiler that maps topological features to agent behavior archetypes.
43pub struct AgentProfiler {
44    pub max_dimension: usize,
45}
46
47impl AgentProfiler {
48    pub fn new(max_dimension: usize) -> Self {
49        Self { max_dimension }
50    }
51
52    /// Profile an agent from its state-space trajectory (sequence of observation vectors).
53    pub fn profile(&self, observations: Vec<Vec<f64>>) -> Result<AgentProfile, crate::error::PersistenceError> {
54        let cloud = PointCloud::new(observations)?;
55        let max_eps = cloud.max_distance();
56        let vr = VietorisRipsComplex::build(&cloud, self.max_dimension, max_eps)?;
57        let barcode = Barcode::compute(&vr)?;
58
59        let entropy = barcode.persistence_entropy();
60        let max_pers = barcode.max_persistence();
61        let betti = barcode.betti_numbers_at(max_eps / 2.0);
62
63        let archetype = self.classify(&barcode, &betti, cloud.n_points());
64
65        Ok(AgentProfile {
66            archetype,
67            persistence_entropy: entropy,
68            max_persistence: max_pers,
69            betti_numbers: betti,
70        })
71    }
72
73    fn classify(
74        &self,
75        barcode: &Barcode,
76        _betti: &[usize],
77        n_points: usize,
78    ) -> AgentArchetype {
79        let h0_pairs: Vec<_> = barcode.pairs_of_dimension(0);
80        let h1_pairs: Vec<_> = barcode.pairs_of_dimension(1);
81
82        let n_components = h0_pairs.len();
83        // Long-lived H0 features (persistence > half of max)
84        let max_pers = barcode.max_persistence();
85        let long_h0 = h0_pairs
86            .iter()
87            .filter(|p| p.death.is_finite() && p.persistence() > max_pers * 0.5)
88            .count();
89
90        // Short-lived H1 features (persistence < 0.3 * max)
91        let short_h1 = h1_pairs
92            .iter()
93            .filter(|p| p.death.is_finite() && p.persistence() < max_pers * 0.3)
94            .count();
95
96        // Long-lived H1+ features
97        let higher_dim_count: usize = barcode
98            .pairs
99            .iter()
100            .filter(|p| p.dimension >= 1 && p.death.is_finite() && p.persistence() > max_pers * 0.4)
101            .count();
102
103        // Many disconnected components relative to point count → Volatile
104        let component_ratio = n_components as f64 / n_points.max(1) as f64;
105        if component_ratio > 0.5 && long_h0 <= 1 {
106            return AgentArchetype::Volatile;
107        }
108
109        // Single persistent cluster (1 long H0, few other features)
110        if long_h0 == 1 && h1_pairs.len() <= 1 && higher_dim_count == 0 {
111            return AgentArchetype::Steady;
112        }
113
114        // Many short-lived loops
115        if short_h1 >= 3 || (h1_pairs.len() as f64 / n_points.max(1) as f64 > 0.3 && short_h1 >= 1) {
116            return AgentArchetype::Explorer;
117        }
118
119        // Long-lived higher-dimensional features
120        if higher_dim_count >= 1 {
121            return AgentArchetype::Deep;
122        }
123
124        AgentArchetype::Balanced
125    }
126}