use crate::search::Match;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StabilityClass {
Stable,
ModeratelyStable,
Fragile,
Anomalous,
}
impl StabilityClass {
pub fn name(&self) -> &'static str {
match self {
StabilityClass::Stable => "stable",
StabilityClass::ModeratelyStable => "moderate",
StabilityClass::Fragile => "fragile",
StabilityClass::Anomalous => "anomalous",
}
}
pub fn description(&self) -> &'static str {
match self {
StabilityClass::Stable => "Persists at all precision levels",
StabilityClass::ModeratelyStable => "Persists at most precision levels",
StabilityClass::Fragile => "Only appears at low precision (impostor)",
StabilityClass::Anomalous => "Anomalous stability pattern",
}
}
}
#[derive(Debug, Clone)]
pub struct StabilityResult {
pub match_: Match,
pub class: StabilityClass,
pub appearance_count: usize,
pub total_levels: usize,
pub score: f64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ExprKey {
lhs: String,
rhs: String,
}
impl ExprKey {
fn from_match(m: &Match) -> Self {
Self {
lhs: m.lhs.expr.to_postfix(),
rhs: m.rhs.expr.to_postfix(),
}
}
}
#[derive(Debug, Clone)]
pub struct StabilityConfig {
pub tolerance_factors: Vec<f64>,
pub stable_threshold: f64,
pub moderate_threshold: f64,
}
impl Default for StabilityConfig {
fn default() -> Self {
Self {
tolerance_factors: vec![1.0, 0.1, 0.01, 0.001, 0.0001],
stable_threshold: 0.8, moderate_threshold: 0.5, }
}
}
impl StabilityConfig {
pub fn quick() -> Self {
Self {
tolerance_factors: vec![1.0, 0.01, 0.0001],
stable_threshold: 0.67,
moderate_threshold: 0.34,
}
}
pub fn thorough() -> Self {
Self {
tolerance_factors: vec![1.0, 0.5, 0.1, 0.05, 0.01, 0.005, 0.001, 0.0001],
stable_threshold: 0.75,
moderate_threshold: 0.5,
}
}
}
pub struct StabilityAnalyzer {
config: StabilityConfig,
levels: Vec<HashMap<ExprKey, Match>>,
}
impl StabilityAnalyzer {
pub fn new(config: StabilityConfig) -> Self {
Self {
config,
levels: Vec::new(),
}
}
pub fn add_level(&mut self, matches: Vec<Match>) {
let mut level_map = HashMap::new();
for m in matches {
let key = ExprKey::from_match(&m);
level_map.insert(key, m);
}
self.levels.push(level_map);
}
pub fn level_count(&self) -> usize {
self.levels.len()
}
pub fn analyze(&self) -> Vec<StabilityResult> {
let total_levels = self.levels.len();
if total_levels == 0 {
return Vec::new();
}
let mut appearance_counts: HashMap<ExprKey, usize> = HashMap::new();
let mut best_matches: HashMap<ExprKey, Match> = HashMap::new();
for level in &self.levels {
for (key, m) in level {
*appearance_counts.entry(key.clone()).or_insert(0) += 1;
best_matches.insert(key.clone(), m.clone());
}
}
let mut results: Vec<StabilityResult> = appearance_counts
.into_iter()
.map(|(key, count)| {
let match_ = best_matches.remove(&key).unwrap();
let ratio = count as f64 / total_levels as f64;
let class = if ratio >= self.config.stable_threshold {
StabilityClass::Stable
} else if ratio >= self.config.moderate_threshold {
StabilityClass::ModeratelyStable
} else if count == 1 {
if self.is_from_loose_level(&key) {
StabilityClass::Fragile
} else {
StabilityClass::Anomalous
}
} else {
StabilityClass::Fragile
};
StabilityResult {
match_,
class,
appearance_count: count,
total_levels,
score: ratio,
}
})
.collect();
results.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
a.match_
.error
.abs()
.partial_cmp(&b.match_.error.abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
});
results
}
fn is_from_loose_level(&self, key: &ExprKey) -> bool {
if self.levels.is_empty() {
return false;
}
self.levels[0].contains_key(key) && !self.levels.iter().skip(1).any(|l| l.contains_key(key))
}
}
pub fn format_stability_report(results: &[StabilityResult], max_display: usize) -> String {
let mut output = String::new();
let stable: Vec<_> = results
.iter()
.filter(|r| r.class == StabilityClass::Stable)
.take(max_display)
.collect();
let moderate: Vec<_> = results
.iter()
.filter(|r| r.class == StabilityClass::ModeratelyStable)
.take(max_display)
.collect();
let fragile: Vec<_> = results
.iter()
.filter(|r| r.class == StabilityClass::Fragile)
.take(max_display)
.collect();
if !stable.is_empty() {
output.push_str("\n -- Stable formulas (high confidence) --\n\n");
for r in &stable {
output.push_str(&format!(
" {:<24} = {:<24} [{}/{} levels] {{{}}}\n",
r.match_.lhs.expr.to_infix(),
r.match_.rhs.expr.to_infix(),
r.appearance_count,
r.total_levels,
r.match_.complexity
));
}
}
if !moderate.is_empty() {
output.push_str("\n -- Moderately stable (medium confidence) --\n\n");
for r in &moderate {
output.push_str(&format!(
" {:<24} = {:<24} [{}/{} levels] {{{}}}\n",
r.match_.lhs.expr.to_infix(),
r.match_.rhs.expr.to_infix(),
r.appearance_count,
r.total_levels,
r.match_.complexity
));
}
}
if !fragile.is_empty() {
output.push_str("\n -- Fragile (likely impostors) --\n\n");
for r in &fragile {
output.push_str(&format!(
" {:<24} = {:<24} [{}/{} levels] {{{}}}\n",
r.match_.lhs.expr.to_infix(),
r.match_.rhs.expr.to_infix(),
r.appearance_count,
r.total_levels,
r.match_.complexity
));
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::{EvaluatedExpr, Expression};
use crate::symbol::NumType;
fn make_test_match(lhs: &str, rhs: &str, error: f64) -> Match {
let lhs_expr = Expression::parse(lhs).unwrap();
let rhs_expr = Expression::parse(rhs).unwrap();
Match {
lhs: EvaluatedExpr::new(lhs_expr.clone(), 0.0, 1.0, NumType::Integer),
rhs: EvaluatedExpr::new(rhs_expr.clone(), 0.0, 0.0, NumType::Integer),
x_value: 2.5,
error,
complexity: lhs_expr.complexity() + rhs_expr.complexity(),
}
}
#[test]
fn test_stability_classification() {
let mut analyzer = StabilityAnalyzer::new(StabilityConfig::default());
analyzer.add_level(vec![
make_test_match("x", "5", 0.01),
make_test_match("2x*", "5", 0.001),
]);
analyzer.add_level(vec![make_test_match("x", "5", 0.001)]);
analyzer.add_level(vec![make_test_match("x", "5", 0.0001)]);
let results = analyzer.analyze();
assert_eq!(results.len(), 2);
let stable = results
.iter()
.find(|r| r.match_.lhs.expr.to_postfix() == "x");
assert!(stable.is_some());
assert_eq!(stable.unwrap().class, StabilityClass::Stable);
assert_eq!(stable.unwrap().appearance_count, 3);
let fragile = results
.iter()
.find(|r| r.match_.lhs.expr.to_postfix() == "2x*");
assert!(fragile.is_some());
assert_eq!(fragile.unwrap().class, StabilityClass::Fragile);
assert_eq!(fragile.unwrap().appearance_count, 1);
}
#[test]
fn test_empty_analyzer() {
let analyzer = StabilityAnalyzer::new(StabilityConfig::default());
assert_eq!(analyzer.analyze().len(), 0);
}
}