1use crate::expr::{Expression, OutputFormat};
6use crate::metrics::{MatchMetrics, OperatorFrequency};
7use crate::pool::{LhsKey, SignatureKey};
8use crate::search::Match;
9use crate::symbol::Symbol;
10use std::collections::HashSet;
11
12#[derive(Debug, Clone, Copy)]
14pub enum DisplayFormat {
15 Infix(OutputFormat),
17 PostfixCompact,
19 PostfixVerbose,
21 Condensed,
23}
24
25fn format_expression_for_display(expression: &Expression, format: DisplayFormat) -> String {
27 match format {
28 DisplayFormat::Infix(inner) => expression.to_infix_with_format(inner),
29 DisplayFormat::PostfixCompact | DisplayFormat::Condensed => expression.to_postfix(),
30 DisplayFormat::PostfixVerbose => expression
31 .symbols()
32 .iter()
33 .map(|sym| postfix_verbose_token(*sym))
34 .collect::<Vec<_>>()
35 .join(" "),
36 }
37}
38
39fn postfix_verbose_token(sym: Symbol) -> String {
40 use Symbol;
41 match sym {
42 Symbol::Neg => "neg".to_string(),
43 Symbol::Recip => "recip".to_string(),
44 Symbol::Sqrt => "sqrt".to_string(),
45 Symbol::Square => "dup*".to_string(),
46 Symbol::Pow => "**".to_string(),
47 Symbol::Root => "root".to_string(),
48 Symbol::Log => "logn".to_string(),
49 Symbol::Exp => "exp".to_string(),
50 _ => sym.display_name(),
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Category {
57 Exact,
59 Best,
61 Elegant,
63 Interesting,
65 Stable,
67}
68
69impl Category {
70 pub fn name(&self) -> &'static str {
71 match self {
72 Category::Exact => "Exact matches",
73 Category::Best => "Best approximations",
74 Category::Elegant => "Elegant/efficient",
75 Category::Interesting => "Interesting/unexpected",
76 Category::Stable => "Stable/robust",
77 }
78 }
79
80 #[allow(dead_code)]
85 pub fn description(&self) -> &'static str {
86 match self {
87 Category::Exact => "Equations that hold exactly at the target value",
88 Category::Best => "Closest approximations to the target",
89 Category::Elegant => "Simplest expressions with good accuracy",
90 Category::Interesting => "Novel or unusual equation structures",
91 Category::Stable => "Matches with robust numerical properties",
92 }
93 }
94}
95
96pub struct Report {
98 pub categories: Vec<(Category, Vec<MatchWithMetrics>)>,
100 pub target: f64,
102}
103
104pub struct MatchWithMetrics {
106 pub m: Match,
107 pub metrics: MatchMetrics,
108}
109
110#[derive(Clone)]
112pub struct ReportConfig {
113 pub top_k: usize,
115 pub categories: Vec<Category>,
117 pub interesting_error_cap: f64,
119}
120
121impl Default for ReportConfig {
122 fn default() -> Self {
123 Self {
124 top_k: 8,
125 categories: vec![
126 Category::Exact,
127 Category::Best,
128 Category::Elegant,
129 Category::Interesting,
130 Category::Stable,
131 ],
132 interesting_error_cap: 1e-6,
133 }
134 }
135}
136
137impl ReportConfig {
138 #[allow(dead_code)]
143 pub fn with_stable(mut self) -> Self {
144 if !self.categories.contains(&Category::Stable) {
145 self.categories.push(Category::Stable);
146 }
147 self
148 }
149
150 pub fn without_stable(mut self) -> Self {
152 self.categories.retain(|c| *c != Category::Stable);
153 self
154 }
155
156 pub fn with_top_k(mut self, k: usize) -> Self {
158 self.top_k = k;
159 self
160 }
161
162 pub fn with_target(mut self, target: f64) -> Self {
164 self.interesting_error_cap = (1e-8_f64).max(1e-6 * target.abs());
166 self
167 }
168}
169
170impl Report {
171 pub fn generate(matches: Vec<Match>, target: f64, config: &ReportConfig) -> Self {
173 let mut freq_map = OperatorFrequency::new();
175 for m in &matches {
176 freq_map.add(m);
177 }
178
179 let mut with_metrics: Vec<MatchWithMetrics> = matches
181 .into_iter()
182 .map(|m| {
183 let metrics = MatchMetrics::from_match(&m, Some(&freq_map));
184 MatchWithMetrics { m, metrics }
185 })
186 .collect();
187
188 let mut categories = Vec::new();
190
191 for &cat in &config.categories {
192 let selected = select_category(&mut with_metrics, cat, config);
193 categories.push((cat, selected));
194 }
195
196 Report { categories, target }
197 }
198
199 pub fn print(&self, absolute: bool, solve: bool, format: DisplayFormat) {
201 for (category, matches) in &self.categories {
202 if matches.is_empty() {
203 continue;
204 }
205
206 println!();
207 println!(" -- {} ({}) --", category.name(), matches.len());
208 println!();
209
210 for mwm in matches {
211 print_match(&mwm.m, &mwm.metrics, self.target, absolute, solve, format);
212 }
213 }
214 }
215}
216
217fn select_category(
219 matches: &mut [MatchWithMetrics],
220 category: Category,
221 config: &ReportConfig,
222) -> Vec<MatchWithMetrics> {
223 let mut candidates: Vec<_> = matches.iter().collect();
225
226 candidates.retain(|mwm| category_filter(mwm, category, config));
228
229 candidates.sort_by(|a, b| category_compare(a, b, category, config));
231
232 let mut result = Vec::new();
234 let mut seen_lhs: HashSet<LhsKey> = HashSet::new();
235 let mut seen_sig: HashSet<SignatureKey> = HashSet::new();
236
237 for mwm in candidates {
238 if result.len() >= config.top_k {
239 break;
240 }
241
242 let accept = match category {
244 Category::Exact => {
245 true
247 }
248 Category::Best | Category::Elegant => {
249 let lhs_key = LhsKey::from_match(&mwm.m);
251 if seen_lhs.contains(&lhs_key) {
252 false
253 } else {
254 seen_lhs.insert(lhs_key);
255 true
256 }
257 }
258 Category::Interesting => {
259 let sig_key = SignatureKey::from_match(&mwm.m);
261 if seen_sig.contains(&sig_key) {
262 false
263 } else {
264 seen_sig.insert(sig_key);
265 true
266 }
267 }
268 Category::Stable => {
269 let lhs_key = LhsKey::from_match(&mwm.m);
271 if seen_lhs.contains(&lhs_key) {
272 false
273 } else {
274 seen_lhs.insert(lhs_key);
275 true
276 }
277 }
278 };
279
280 if accept {
281 result.push(mwm.clone());
282 }
283 }
284
285 result
286}
287
288fn category_filter(mwm: &MatchWithMetrics, category: Category, config: &ReportConfig) -> bool {
290 match category {
291 Category::Exact => mwm.metrics.is_exact,
292 Category::Best => !mwm.metrics.is_exact, Category::Elegant => true, Category::Interesting => {
295 mwm.metrics.error <= config.interesting_error_cap && !mwm.metrics.is_exact
296 }
297 Category::Stable => mwm.metrics.stability > 0.3, }
299}
300
301fn category_compare(
303 a: &MatchWithMetrics,
304 b: &MatchWithMetrics,
305 category: Category,
306 config: &ReportConfig,
307) -> std::cmp::Ordering {
308 use std::cmp::Ordering;
309
310 match category {
311 Category::Exact => {
312 a.metrics
314 .complexity
315 .cmp(&b.metrics.complexity)
316 .then_with(|| {
317 (a.m.lhs.expr.len() + a.m.rhs.expr.len())
318 .cmp(&(b.m.lhs.expr.len() + b.m.rhs.expr.len()))
319 })
320 }
321 Category::Best => {
322 a.metrics
324 .error
325 .partial_cmp(&b.metrics.error)
326 .unwrap_or(Ordering::Equal)
327 }
328 Category::Elegant => {
329 a.metrics
331 .elegant_score()
332 .partial_cmp(&b.metrics.elegant_score())
333 .unwrap_or(Ordering::Equal)
334 }
335 Category::Interesting => {
336 b.metrics
338 .interesting_score(config.interesting_error_cap)
339 .partial_cmp(&a.metrics.interesting_score(config.interesting_error_cap))
340 .unwrap_or(Ordering::Equal)
341 }
342 Category::Stable => {
343 b.metrics
345 .stable_score()
346 .partial_cmp(&a.metrics.stable_score())
347 .unwrap_or(Ordering::Equal)
348 .then_with(|| {
349 a.metrics
350 .error
351 .partial_cmp(&b.metrics.error)
352 .unwrap_or(Ordering::Equal)
353 })
354 }
355 }
356}
357
358impl Clone for MatchWithMetrics {
360 fn clone(&self) -> Self {
361 Self {
362 m: self.m.clone(),
363 metrics: self.metrics.clone(),
364 }
365 }
366}
367
368fn print_match(
370 m: &Match,
371 metrics: &MatchMetrics,
372 _target: f64,
373 absolute: bool,
374 solve: bool,
375 format: DisplayFormat,
376) {
377 let lhs_str = format_expression_for_display(&m.lhs.expr, format);
378 let rhs_str = format_expression_for_display(&m.rhs.expr, format);
379
380 let error_str = if metrics.is_exact {
381 "('exact' match)".to_string()
382 } else if absolute {
383 format!("for x = {:.15}", m.x_value)
384 } else {
385 let sign = if m.error >= 0.0 { "+" } else { "-" };
386 format!("for x = T {} {:.6e}", sign, m.error.abs())
387 };
388
389 let info = format!("{{{}}}", m.complexity);
391
392 if solve {
393 println!(" x = {:40} {} {}", rhs_str, error_str, info);
394 } else {
395 println!("{:>24} = {:<24} {} {}", lhs_str, rhs_str, error_str, info);
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::expr::{EvaluatedExpr, Expression};
403 use crate::symbol::NumType;
404
405 fn make_match(lhs: &str, rhs: &str, error: f64) -> Match {
406 let lhs_expr = Expression::parse(lhs).unwrap();
407 let rhs_expr = Expression::parse(rhs).unwrap();
408 Match {
409 lhs: EvaluatedExpr::new(lhs_expr.clone(), 0.0, 1.0, NumType::Integer),
410 rhs: EvaluatedExpr::new(rhs_expr.clone(), 0.0, 0.0, NumType::Integer),
411 x_value: 2.5,
412 error,
413 complexity: lhs_expr.complexity() + rhs_expr.complexity(),
414 }
415 }
416
417 #[test]
418 fn test_report_generation() {
419 let matches = vec![
420 make_match("2x*", "5", 0.0), make_match("xx^", "ps", 0.00066), make_match("x1+", "35/", 1e-10), ];
424
425 let config = ReportConfig::default().with_target(2.5);
426 let report = Report::generate(matches, 2.5, &config);
427
428 assert!(!report.categories.is_empty());
430 }
431}