1use crate::search::Match;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum StabilityClass {
12 Stable,
14 ModeratelyStable,
16 Fragile,
18 Anomalous,
20}
21
22impl StabilityClass {
23 pub fn name(&self) -> &'static str {
25 match self {
26 StabilityClass::Stable => "stable",
27 StabilityClass::ModeratelyStable => "moderate",
28 StabilityClass::Fragile => "fragile",
29 StabilityClass::Anomalous => "anomalous",
30 }
31 }
32
33 pub fn description(&self) -> &'static str {
35 match self {
36 StabilityClass::Stable => "Persists at all precision levels",
37 StabilityClass::ModeratelyStable => "Persists at most precision levels",
38 StabilityClass::Fragile => "Only appears at low precision (impostor)",
39 StabilityClass::Anomalous => "Anomalous stability pattern",
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct StabilityResult {
47 pub match_: Match,
49 pub class: StabilityClass,
51 pub appearance_count: usize,
53 pub total_levels: usize,
55 pub score: f64,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61struct ExprKey {
62 lhs: String,
63 rhs: String,
64}
65
66impl ExprKey {
67 fn from_match(m: &Match) -> Self {
68 Self {
69 lhs: m.lhs.expr.to_postfix(),
70 rhs: m.rhs.expr.to_postfix(),
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct StabilityConfig {
78 pub tolerance_factors: Vec<f64>,
81 pub stable_threshold: f64,
83 pub moderate_threshold: f64,
85}
86
87impl Default for StabilityConfig {
88 fn default() -> Self {
89 Self {
90 tolerance_factors: vec![1.0, 0.1, 0.01, 0.001, 0.0001],
92 stable_threshold: 0.8, moderate_threshold: 0.5, }
95 }
96}
97
98impl StabilityConfig {
99 pub fn quick() -> Self {
101 Self {
102 tolerance_factors: vec![1.0, 0.01, 0.0001],
103 stable_threshold: 0.67,
104 moderate_threshold: 0.34,
105 }
106 }
107
108 pub fn thorough() -> Self {
110 Self {
111 tolerance_factors: vec![1.0, 0.5, 0.1, 0.05, 0.01, 0.005, 0.001, 0.0001],
112 stable_threshold: 0.75,
113 moderate_threshold: 0.5,
114 }
115 }
116}
117
118pub struct StabilityAnalyzer {
120 config: StabilityConfig,
121 levels: Vec<HashMap<ExprKey, Match>>,
123}
124
125impl StabilityAnalyzer {
126 pub fn new(config: StabilityConfig) -> Self {
128 Self {
129 config,
130 levels: Vec::new(),
131 }
132 }
133
134 pub fn add_level(&mut self, matches: Vec<Match>) {
136 let mut level_map = HashMap::new();
137 for m in matches {
138 let key = ExprKey::from_match(&m);
139 level_map.insert(key, m);
140 }
141 self.levels.push(level_map);
142 }
143
144 pub fn level_count(&self) -> usize {
146 self.levels.len()
147 }
148
149 pub fn analyze(&self) -> Vec<StabilityResult> {
151 let total_levels = self.levels.len();
152 if total_levels == 0 {
153 return Vec::new();
154 }
155
156 let mut appearance_counts: HashMap<ExprKey, usize> = HashMap::new();
158 let mut best_matches: HashMap<ExprKey, Match> = HashMap::new();
159
160 for level in &self.levels {
161 for (key, m) in level {
162 *appearance_counts.entry(key.clone()).or_insert(0) += 1;
163 best_matches.insert(key.clone(), m.clone());
165 }
166 }
167
168 let mut results: Vec<StabilityResult> = appearance_counts
170 .into_iter()
171 .map(|(key, count)| {
172 let match_ = best_matches.remove(&key).unwrap();
173 let ratio = count as f64 / total_levels as f64;
174 let class = if ratio >= self.config.stable_threshold {
175 StabilityClass::Stable
176 } else if ratio >= self.config.moderate_threshold {
177 StabilityClass::ModeratelyStable
178 } else if count == 1 {
179 if self.is_from_loose_level(&key) {
181 StabilityClass::Fragile
182 } else {
183 StabilityClass::Anomalous
184 }
185 } else {
186 StabilityClass::Fragile
187 };
188
189 StabilityResult {
190 match_,
191 class,
192 appearance_count: count,
193 total_levels,
194 score: ratio,
195 }
196 })
197 .collect();
198
199 results.sort_by(|a, b| {
201 b.score
202 .partial_cmp(&a.score)
203 .unwrap_or(std::cmp::Ordering::Equal)
204 .then_with(|| {
205 a.match_
206 .error
207 .abs()
208 .partial_cmp(&b.match_.error.abs())
209 .unwrap_or(std::cmp::Ordering::Equal)
210 })
211 });
212
213 results
214 }
215
216 fn is_from_loose_level(&self, key: &ExprKey) -> bool {
218 if self.levels.is_empty() {
219 return false;
220 }
221 self.levels[0].contains_key(key) && !self.levels.iter().skip(1).any(|l| l.contains_key(key))
223 }
224}
225
226pub fn format_stability_report(results: &[StabilityResult], max_display: usize) -> String {
228 let mut output = String::new();
229
230 let stable: Vec<_> = results
232 .iter()
233 .filter(|r| r.class == StabilityClass::Stable)
234 .take(max_display)
235 .collect();
236 let moderate: Vec<_> = results
237 .iter()
238 .filter(|r| r.class == StabilityClass::ModeratelyStable)
239 .take(max_display)
240 .collect();
241 let fragile: Vec<_> = results
242 .iter()
243 .filter(|r| r.class == StabilityClass::Fragile)
244 .take(max_display)
245 .collect();
246
247 if !stable.is_empty() {
248 output.push_str("\n -- Stable formulas (high confidence) --\n\n");
249 for r in &stable {
250 output.push_str(&format!(
251 " {:<24} = {:<24} [{}/{} levels] {{{}}}\n",
252 r.match_.lhs.expr.to_infix(),
253 r.match_.rhs.expr.to_infix(),
254 r.appearance_count,
255 r.total_levels,
256 r.match_.complexity
257 ));
258 }
259 }
260
261 if !moderate.is_empty() {
262 output.push_str("\n -- Moderately stable (medium confidence) --\n\n");
263 for r in &moderate {
264 output.push_str(&format!(
265 " {:<24} = {:<24} [{}/{} levels] {{{}}}\n",
266 r.match_.lhs.expr.to_infix(),
267 r.match_.rhs.expr.to_infix(),
268 r.appearance_count,
269 r.total_levels,
270 r.match_.complexity
271 ));
272 }
273 }
274
275 if !fragile.is_empty() {
276 output.push_str("\n -- Fragile (likely impostors) --\n\n");
277 for r in &fragile {
278 output.push_str(&format!(
279 " {:<24} = {:<24} [{}/{} levels] {{{}}}\n",
280 r.match_.lhs.expr.to_infix(),
281 r.match_.rhs.expr.to_infix(),
282 r.appearance_count,
283 r.total_levels,
284 r.match_.complexity
285 ));
286 }
287 }
288
289 output
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::expr::{EvaluatedExpr, Expression};
296 use crate::symbol::NumType;
297
298 fn make_test_match(lhs: &str, rhs: &str, error: f64) -> Match {
299 let lhs_expr = Expression::parse(lhs).unwrap();
300 let rhs_expr = Expression::parse(rhs).unwrap();
301 Match {
302 lhs: EvaluatedExpr::new(lhs_expr.clone(), 0.0, 1.0, NumType::Integer),
303 rhs: EvaluatedExpr::new(rhs_expr.clone(), 0.0, 0.0, NumType::Integer),
304 x_value: 2.5,
305 error,
306 complexity: lhs_expr.complexity() + rhs_expr.complexity(),
307 }
308 }
309
310 #[test]
311 fn test_stability_classification() {
312 let mut analyzer = StabilityAnalyzer::new(StabilityConfig::default());
313
314 analyzer.add_level(vec![
316 make_test_match("x", "5", 0.01),
317 make_test_match("2x*", "5", 0.001),
318 ]);
319
320 analyzer.add_level(vec![make_test_match("x", "5", 0.001)]);
322
323 analyzer.add_level(vec![make_test_match("x", "5", 0.0001)]);
325
326 let results = analyzer.analyze();
327 assert_eq!(results.len(), 2);
328
329 let stable = results
331 .iter()
332 .find(|r| r.match_.lhs.expr.to_postfix() == "x");
333 assert!(stable.is_some());
334 assert_eq!(stable.unwrap().class, StabilityClass::Stable);
335 assert_eq!(stable.unwrap().appearance_count, 3);
336
337 let fragile = results
339 .iter()
340 .find(|r| r.match_.lhs.expr.to_postfix() == "2x*");
341 assert!(fragile.is_some());
342 assert_eq!(fragile.unwrap().class, StabilityClass::Fragile);
343 assert_eq!(fragile.unwrap().appearance_count, 1);
344 }
345
346 #[test]
347 fn test_empty_analyzer() {
348 let analyzer = StabilityAnalyzer::new(StabilityConfig::default());
349 assert_eq!(analyzer.analyze().len(), 0);
350 }
351}