persistence_agent/
agent_features.rs1use serde::{Deserialize, Serialize};
2use crate::barcode::Barcode;
3use crate::point_cloud::PointCloud;
4use crate::vietoris_rips::VietorisRipsComplex;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub enum AgentArchetype {
9 Steady,
11 Explorer,
13 Volatile,
15 Deep,
17 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#[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
42pub 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 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 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 let short_h1 = h1_pairs
92 .iter()
93 .filter(|p| p.death.is_finite() && p.persistence() < max_pers * 0.3)
94 .count();
95
96 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 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 if long_h0 == 1 && h1_pairs.len() <= 1 && higher_dim_count == 0 {
111 return AgentArchetype::Steady;
112 }
113
114 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 if higher_dim_count >= 1 {
121 return AgentArchetype::Deep;
122 }
123
124 AgentArchetype::Balanced
125 }
126}