use anyhow::{Context, Result}; use arrow::array::Array; use arrow::array::StringArray; use arrow::datatypes::{DataType, Field, Schema}; use arrow::record_batch::RecordBatch; use clap::Parser; use dashmap::DashMap; use indicatif::{ProgressBar, ProgressStyle}; use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; use parquet::arrow::ArrowWriter; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use rayon::ThreadPoolBuilder; use regex::Regex; use serde::Serialize; use std::collections::HashMap; use std::fs::{self, File}; use std::io::{BufRead, BufReader, Write}; use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; // ── CLI ────────────────────────────────────────────────────────────────────── #[derive(Parser, Debug)] #[command(name = "math_self_discover")] #[command(about = "Unsupervised structural taxonomy from naked equations.")] struct Config { /// Input parquet (math-raw) #[arg( short, long, default_value = "/home/allaun/Documents/Research Stack/3-Mathematical-Models/equations_parquet_tagged/equations_math_raw.parquet" )] input: String, /// Output JSON report #[arg( short, long, default_value = "/home/allaun/Documents/Research Stack/3-Mathematical-Models/math_self_discovered.json" )] report: String, /// Output parquet with cluster assignments #[arg( long, default_value = "/home/allaun/Documents/Research Stack/3-Mathematical-Models/equations_parquet_tagged/equations_self_clustered.parquet" )] clustered: String, /// Max CPU threads (0 = num_cpus / 2) #[arg(long, default_value = "0")] threads: usize, /// Memory cap in MB (0 = no cap) #[arg(long, default_value = "0")] memory_cap_mb: usize, /// Batch size for reading #[arg(long, default_value = "10000")] batch_size: usize, /// Pause if system CPU % exceeds this #[arg(long, default_value = "80")] cpu_threshold: f32, /// Pause if system RAM % exceeds this #[arg(long, default_value = "90")] memory_threshold: f32, /// Number of top motifs to report #[arg(long, default_value = "100")] top_motifs: usize, /// Directory for on-disk fingerprint cache #[arg(long, default_value = "")] cache_dir: String, /// Max RAM cache entries (0 = unlimited) #[arg(long, default_value = "0")] ram_cache_cap: usize, } // ── Resource Monitor ───────────────────────────────────────────────────────── struct ResourceMonitor { sys: Mutex, cpu_threshold: f32, memory_threshold: f32, } impl ResourceMonitor { fn new(cpu_threshold: f32, memory_threshold: f32) -> Self { Self { sys: Mutex::new(System::new_with_specifics( RefreshKind::new() .with_cpu(CpuRefreshKind::everything()) .with_memory(MemoryRefreshKind::everything()), )), cpu_threshold, memory_threshold, } } fn check(&self) -> Result { let mut sys = self.sys.lock().unwrap(); sys.refresh_specifics( RefreshKind::new() .with_cpu(CpuRefreshKind::everything()) .with_memory(MemoryRefreshKind::everything()), ); let cpu_usage: f32 = sys.cpus().iter().map(|c| c.cpu_usage()).sum::() / sys.cpus().len() as f32; let mem_usage = sys.used_memory() as f32 / sys.total_memory() as f32 * 100.0; let ok = cpu_usage < self.cpu_threshold && mem_usage < self.memory_threshold; if !ok { eprintln!( " [THROTTLE] CPU: {:.1}%, RAM: {:.1}% — sleeping...", cpu_usage, mem_usage ); } Ok(ok) } fn wait(&self) { while !self.check().unwrap_or(false) { std::thread::sleep(std::time::Duration::from_millis(500)); } } } // ── Cache Manager (RAM + Disk) ───────────────────────────────────────────── struct CacheManager { /// In-memory equation → fingerprint map ram: DashMap, /// On-disk cache file path disk_path: PathBuf, /// Max RAM entries before spilling (0 = unlimited) cap: usize, /// Cache hits hits: AtomicUsize, /// Cache misses misses: AtomicUsize, } impl CacheManager { fn new(cache_dir: &str, cap: usize) -> Result { let disk_path = if cache_dir.is_empty() { let mut path = dirs::cache_dir().unwrap_or_else(|| std::env::temp_dir()); path.push("math_self_discover"); path.push("fp_cache.jsonl"); path } else { let mut path = PathBuf::from(cache_dir); fs::create_dir_all(&path)?; path.push("fp_cache.jsonl"); path }; let ram = DashMap::new(); // Warm RAM cache from disk if present if disk_path.exists() { eprintln!(" [cache] warming from disk: {}", disk_path.display()); let file = File::open(&disk_path)?; let reader = BufReader::new(file); for line in reader.lines() { if let Ok(line) = line { if let Some((eq, fp)) = line.split_once('\t') { ram.insert(eq.to_string(), fp.to_string()); } } } eprintln!(" [cache] loaded {} entries into RAM", ram.len()); } Ok(Self { ram, disk_path, cap, hits: AtomicUsize::new(0), misses: AtomicUsize::new(0), }) } fn get(&self, eq: &str) -> Option { if let Some(fp) = self.ram.get(eq).map(|e| e.clone()) { self.hits.fetch_add(1, Ordering::Relaxed); return Some(fp); } None } fn insert(&self, eq: String, fp: String) { // If capped, skip inserting (simple eviction: don't grow beyond cap) if self.cap > 0 && self.ram.len() >= self.cap { return; } self.ram.insert(eq, fp); } fn record_miss(&self) { self.misses.fetch_add(1, Ordering::Relaxed); } fn stats(&self) -> (usize, usize, usize) { ( self.ram.len(), self.hits.load(Ordering::Relaxed), self.misses.load(Ordering::Relaxed), ) } fn flush_to_disk(&self) -> Result<()> { if let Some(parent) = self.disk_path.parent() { fs::create_dir_all(parent)?; } let mut file = File::create(&self.disk_path)?; for entry in self.ram.iter() { writeln!(file, "{}\t{}", entry.key(), entry.value())?; } Ok(()) } } // ── Fingerprinting ─────────────────────────────────────────────────────────── struct FingerprintEngine { latex_re: Regex, number_re: Regex, greek_re: Regex, multi_latin_re: Regex, whitespace_re: Regex, collapse_n_re: Regex, } impl FingerprintEngine { fn new() -> Result { Ok(Self { latex_re: Regex::new(r"\\([a-zA-Z]+)")?, number_re: Regex::new(r"\d+(?:\.\d+)?")?, greek_re: Regex::new(r"[αβγδεζηθικλμνξοπρστυφχψωΓΔΘΛΞΠΣΦΨΩ]")?, multi_latin_re: Regex::new(r"[a-zA-Z]{2,}")?, whitespace_re: Regex::new(r"\s+")?, collapse_n_re: Regex::new(r"N\s+N")?, }) } fn replace_single_letters( text: &str, var_map: &mut HashMap, counter: &mut usize, ) -> String { let chars: Vec = text.chars().collect(); let mut result = String::with_capacity(text.len()); let mut i = 0; while i < chars.len() { if chars[i].is_ascii_alphabetic() { let prev_is_letter = i > 0 && chars[i - 1].is_ascii_alphabetic(); let next_is_letter = i + 1 < chars.len() && chars[i + 1].is_ascii_alphabetic(); if !prev_is_letter && !next_is_letter { let c = chars[i].to_lowercase().to_string(); let token = var_map.entry(c.clone()).or_insert_with(|| { let t = format!("v{}", *counter); *counter += 1; t }); result.push_str(token); i += 1; continue; } } result.push(chars[i]); i += 1; } result } fn fingerprint(&self, eq: &str) -> String { let mut text = eq.trim().to_string(); if text.is_empty() { return String::new(); } // Remove LaTeX backslashes text = self.latex_re.replace_all(&text, "$1").into_owned(); // Replace numbers with N text = self.number_re.replace_all(&text, "N").into_owned(); // Replace Greek letters with g0, g1, ... let mut greek_map: HashMap = HashMap::new(); let mut greek_counter = 0usize; text = self .greek_re .replace_all(&text, |caps: ®ex::Captures| { let c = caps.get(0).unwrap().as_str().to_string(); if let Some(v) = greek_map.get(&c) { return v.clone(); } let token = format!("g{}", greek_counter); greek_counter += 1; greek_map.insert(c, token.clone()); token }) .into_owned(); // Single Latin letters → vN (manual scan, no lookaround) let mut var_map: HashMap = HashMap::new(); let mut var_counter = 0usize; text = Self::replace_single_letters(&text, &mut var_map, &mut var_counter); // Multi-letter identifiers let math_funcs: std::collections::HashSet<&str> = [ "sin", "cos", "tan", "exp", "log", "ln", "sqrt", "det", "tr", "dim", "ker", "rank", "span", "frac", "sum", "prod", "int", ] .iter() .copied() .collect(); text = self .multi_latin_re .replace_all(&text, |caps: ®ex::Captures| { let w = caps.get(0).unwrap().as_str().to_lowercase(); if math_funcs.contains(w.as_str()) { return w; } if let Some(v) = var_map.get(&w) { return v.clone(); } let token = format!("v{}", var_counter); var_counter += 1; var_map.insert(w, token.clone()); token }) .into_owned(); // Normalize whitespace text = self.whitespace_re.replace_all(&text, " ").into_owned(); text = text.trim().to_string(); // Collapse repeated N loop { let collapsed = self.collapse_n_re.replace_all(&text, "N").into_owned(); if collapsed == text { break; } text = collapsed; } text } } // ── Clustering ─────────────────────────────────────────────────────────────── #[derive(Serialize)] struct Motif { rank: usize, fingerprint: String, count: u64, percentage: f64, } #[derive(Serialize)] struct ClusterReport { timestamp: String, total_equations: u64, unique_structural_forms: u64, top_motifs: Vec, cluster_distribution: Vec<(String, u64)>, } fn main() -> Result<()> { let config = Config::parse(); // ── Resource-aware thread pool ─────────────────────────────────────────── let n_cpus = std::thread::available_parallelism()?.get(); let threads = if config.threads == 0 { n_cpus / 2 } else { config.threads }; let threads = threads.max(1); eprintln!("════════════════════════════════════════════════════════════"); eprintln!(" MATH SELF-DISCOVERY (Rust) — Stream Copy + SIMD Surface"); eprintln!(" Threads: {} / {} CPUs", threads, n_cpus); eprintln!(" Memory cap: {} MB", config.memory_cap_mb); eprintln!(" Batch size: {}", config.batch_size); eprintln!(" CPU threshold: {}%", config.cpu_threshold); eprintln!(" Memory threshold: {}%", config.memory_threshold); eprintln!( " Cache dir: {}", if config.cache_dir.is_empty() { "default" } else { &config.cache_dir } ); eprintln!("════════════════════════════════════════════════════════════"); ThreadPoolBuilder::new() .num_threads(threads) .build_global() .context("failed to build thread pool")?; let monitor = Arc::new(ResourceMonitor::new( config.cpu_threshold, config.memory_threshold, )); // ── Cache manager ────────────────────────────────────────────────────────── let cache = Arc::new(CacheManager::new(&config.cache_dir, config.ram_cache_cap)?); // ── Open input + output (stream copy) ──────────────────────────────────── eprintln!("\nOpening input parquet..."); let file = File::open(&config.input).with_context(|| format!("failed to open {}", config.input))?; let builder = ParquetRecordBatchReaderBuilder::try_new(file)?; let total_rows = builder.metadata().file_metadata().num_rows() as u64; eprintln!(" Total rows: {}", total_rows); let reader = builder.with_batch_size(config.batch_size).build()?; let schema = Schema::new(vec![ Field::new("uuid", DataType::Utf8, false), Field::new("equation", DataType::Utf8, true), Field::new("refined_equation", DataType::Utf8, true), Field::new("fingerprint", DataType::Utf8, false), Field::new("structural_cluster", DataType::Utf8, false), ]); let out_file = File::create(&config.clustered)?; let props = parquet::file::properties::WriterProperties::builder() .set_compression(parquet::basic::Compression::ZSTD( parquet::basic::ZstdLevel::try_new(3)?, )) .build(); let mut writer = ArrowWriter::try_new(out_file, Arc::new(schema.clone()), Some(props))?; // ── Single-pass: fingerprint, cluster count, write ─────────────────────── eprintln!("\nStreaming: fingerprint → count → write..."); let engine = Arc::new(FingerprintEngine::new()?); let cluster_counts: DashMap = DashMap::new(); let total_processed = AtomicUsize::new(0); let pb = ProgressBar::new(total_rows); pb.set_style( ProgressStyle::with_template( "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({per_sec}) {msg}", )? .progress_chars("#>-"), ); let start_time = std::time::Instant::now(); for batch in reader { let batch = batch.context("failed to read batch")?; let n = batch.num_rows(); // Check resource caps monitor.wait(); // Extract columns let uuid_col = batch .column_by_name("uuid") .and_then(|c| c.as_any().downcast_ref::()) .context("missing or wrong type: uuid")?; let eq_col = batch .column_by_name("equation") .and_then(|c| c.as_any().downcast_ref::()) .context("missing or wrong type: equation")?; let refined_col = batch .column_by_name("refined_equation") .and_then(|c| c.as_any().downcast_ref::()); // Build equation strings for cache/SIMD surface let eqs: Vec = (0..n).map(|i| eq_col.value(i).to_string()).collect(); // Fingerprint via cache-first SIMD batch surface let fingerprints: Vec = eqs .into_par_iter() .map(|eq| { if let Some(fp) = cache.get(&eq) { return fp; } cache.record_miss(); let fp = engine.fingerprint(&eq); cache.insert(eq, fp.clone()); fp }) .collect(); // Aggregate counts for fp in fingerprints.iter() { if fp.is_empty() { continue; } cluster_counts .entry(fp.clone()) .or_insert_with(|| AtomicUsize::new(0)) .fetch_add(1, Ordering::Relaxed); } // Stream copy: build output batch and write immediately let mut uuids = Vec::with_capacity(n); let mut equations = Vec::with_capacity(n); let mut refined = Vec::with_capacity(n); let mut fps = Vec::with_capacity(n); let mut clusters = Vec::with_capacity(n); for i in 0..n { uuids.push(Some(uuid_col.value(i).to_string())); equations.push(Some(eq_col.value(i).to_string())); if let Some(ref_col) = refined_col { refined.push(if ref_col.is_null(i) { None } else { Some(ref_col.value(i).to_string()) }); } else { refined.push(None); } let fp = &fingerprints[i]; fps.push(Some(fp.clone())); clusters.push(Some(fp.clone())); } let out_batch = RecordBatch::try_new( Arc::new(schema.clone()), vec![ Arc::new(StringArray::from(uuids)), Arc::new(StringArray::from(equations)), Arc::new(StringArray::from(refined)), Arc::new(StringArray::from(fps)), Arc::new(StringArray::from(clusters)), ], )?; writer.write(&out_batch)?; let processed = total_processed.fetch_add(n, Ordering::Relaxed) + n; pb.set_position(processed as u64); } pb.finish_with_message("done"); writer.close()?; let elapsed = start_time.elapsed(); let total = total_processed.load(Ordering::Relaxed); eprintln!("\n Processed {} equations in {:.1?}", total, elapsed); eprintln!(" Rate: {:.0} eq/s", total as f64 / elapsed.as_secs_f64()); // ── Cache stats ────────────────────────────────────────────────────────── let (cache_entries, hits, misses) = cache.stats(); eprintln!( " Cache: {} entries, {} hits, {} misses", cache_entries, hits, misses ); eprintln!(" Flushing cache to disk..."); cache.flush_to_disk()?; eprintln!(" Cache flushed to: {}", cache.disk_path.display()); // ── Build report ───────────────────────────────────────────────────────── eprintln!("\nBuilding report..."); let mut clusters: Vec<(String, usize)> = cluster_counts .into_iter() .map(|(k, v)| (k, v.load(Ordering::Relaxed))) .collect(); clusters.sort_by(|a, b| b.1.cmp(&a.1)); let n_unique = clusters.len() as u64; eprintln!(" Unique structural forms: {}", n_unique); let top_motifs: Vec = clusters .iter() .take(config.top_motifs) .enumerate() .map(|(rank, (fp, cnt))| Motif { rank: rank + 1, fingerprint: fp.clone(), count: *cnt as u64, percentage: (*cnt as f64 / total as f64) * 100.0, }) .collect(); eprintln!("\n Top {} Structural Motifs:", config.top_motifs.min(20)); eprintln!(" {:>5} {:>10} {:>6} Motif", "Rank", "Count", "%"); for motif in top_motifs.iter().take(20) { let fp_display = if motif.fingerprint.len() > 80 { format!("{}...", &motif.fingerprint[..80]) } else { motif.fingerprint.clone() }; eprintln!( " {:>5} {:>10} {:>6.2}% {}", motif.rank, motif.count, motif.percentage, fp_display ); } let report = ClusterReport { timestamp: chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(), total_equations: total as u64, unique_structural_forms: n_unique, top_motifs, cluster_distribution: clusters .iter() .map(|(fp, cnt)| (fp.clone(), *cnt as u64)) .collect(), }; let report_file = File::create(&config.report) .with_context(|| format!("failed to create {}", config.report))?; serde_json::to_writer_pretty(report_file, &report)?; eprintln!("\n Report written to {}", config.report); eprintln!(" Clustered parquet written to {}", config.clustered); eprintln!("\n════════════════════════════════════════════════════════════"); eprintln!(" MATH SELF-DISCOVERY COMPLETE (single-pass stream copy)"); eprintln!(" {} equations → {} structural forms", total, n_unique); eprintln!("════════════════════════════════════════════════════════════"); Ok(()) }