Skip to main content

ries_rs/
fast_match.rs

1//! Fast-path exact match detection
2//!
3//! Before doing expensive expression generation, check if the target
4//! is a simple exact match (like pi, e, sqrt(2), etc.) that can be
5//! found instantly.
6
7use crate::eval::{evaluate_with_context, EvalContext};
8use crate::expr::{EvaluatedExpr, Expression};
9use crate::profile::UserConstant;
10use crate::search::Match;
11use crate::symbol::{NumType, Symbol};
12use crate::symbol_table::SymbolTable;
13use std::collections::HashSet;
14
15/// Tolerance for exact match detection
16const EXACT_TOLERANCE: f64 = 1e-14;
17
18/// Build an expression from symbols using table-based weights
19fn expr_from_symbols_with_table(symbols: &[Symbol], table: &SymbolTable) -> Expression {
20    let mut expr = Expression::new();
21    for &sym in symbols {
22        expr.push_with_table(sym, table);
23    }
24    expr
25}
26
27/// Get the num_type of an expression based on its symbols
28/// This is a simplified type inference for the fast_match candidates
29fn get_num_type(symbols: &[Symbol]) -> NumType {
30    // For fast match candidates, we use simplified type inference:
31    // - Integer constants → Integer
32    // - Rational operations (division) → Rational
33    // - Sqrt of integer → Algebraic (constructible)
34    // - Transcendental constants → Transcendental
35    // - Any transcendental function → Transcendental
36
37    use Symbol::*;
38
39    // Handle simple patterns
40    if symbols.len() == 1 {
41        return symbols[0].inherent_type();
42    }
43
44    // Check for sqrt of integer (like 2q = sqrt(2))
45    if symbols.len() == 2 {
46        if matches!(symbols[1], Sqrt) {
47            if matches!(
48                symbols[0],
49                One | Two | Three | Four | Five | Six | Seven | Eight | Nine
50            ) {
51                return NumType::Algebraic; // sqrt of integer is algebraic
52            }
53            // sqrt of transcendental constant is transcendental
54            if matches!(symbols[0], Pi | E | Gamma | Apery | Catalan) {
55                return NumType::Transcendental;
56            }
57        }
58        // Check for reciprocal of integer
59        if matches!(symbols[1], Recip)
60            && matches!(
61                symbols[0],
62                One | Two | Three | Four | Five | Six | Seven | Eight | Nine
63            )
64        {
65            return NumType::Rational;
66        }
67    }
68
69    // Check for division pattern (3 symbols: num, denom, /)
70    if symbols.len() == 3 && matches!(symbols[2], Div) {
71        // Integer / Integer = Rational
72        if matches!(
73            symbols[0],
74            One | Two | Three | Four | Five | Six | Seven | Eight | Nine
75        ) && matches!(
76            symbols[1],
77            One | Two | Three | Four | Five | Six | Seven | Eight | Nine
78        ) {
79            return NumType::Rational;
80        }
81    }
82
83    // Default: check if any symbol is transcendental
84    for &sym in symbols {
85        let sym_type = sym.inherent_type();
86        if sym_type == NumType::Transcendental {
87            return NumType::Transcendental;
88        }
89    }
90
91    // If we have any algebraic constants (phi, plastic), result is algebraic
92    for &sym in symbols {
93        if matches!(sym, Phi | Plastic) {
94            return NumType::Algebraic;
95        }
96    }
97
98    // Default to transcendental (most general)
99    NumType::Transcendental
100}
101
102/// Check if any symbol in the expression is excluded
103fn contains_excluded(symbols: &[Symbol], excluded: &HashSet<u8>) -> bool {
104    symbols.iter().any(|s| excluded.contains(&(*s as u8)))
105}
106
107/// A candidate for a fast exact match
108struct FastCandidate {
109    /// The expression (as symbols)
110    symbols: &'static [Symbol],
111}
112
113/// Generate fast candidates for common constants and simple expressions
114fn get_constant_candidates() -> &'static [FastCandidate] {
115    static CANDIDATES: &[FastCandidate] = &[
116        // Integers
117        FastCandidate {
118            symbols: &[Symbol::One],
119        },
120        FastCandidate {
121            symbols: &[Symbol::Two],
122        },
123        FastCandidate {
124            symbols: &[Symbol::Three],
125        },
126        FastCandidate {
127            symbols: &[Symbol::Four],
128        },
129        FastCandidate {
130            symbols: &[Symbol::Five],
131        },
132        FastCandidate {
133            symbols: &[Symbol::Six],
134        },
135        FastCandidate {
136            symbols: &[Symbol::Seven],
137        },
138        FastCandidate {
139            symbols: &[Symbol::Eight],
140        },
141        FastCandidate {
142            symbols: &[Symbol::Nine],
143        },
144        // Named constants
145        FastCandidate {
146            symbols: &[Symbol::Pi],
147        },
148        FastCandidate {
149            symbols: &[Symbol::E],
150        },
151        FastCandidate {
152            symbols: &[Symbol::Phi],
153        },
154        FastCandidate {
155            symbols: &[Symbol::Gamma],
156        },
157        FastCandidate {
158            symbols: &[Symbol::Plastic],
159        },
160        FastCandidate {
161            symbols: &[Symbol::Apery],
162        },
163        FastCandidate {
164            symbols: &[Symbol::Catalan],
165        },
166        // Simple rationals (common ones)
167        FastCandidate {
168            symbols: &[Symbol::One, Symbol::Two, Symbol::Div],
169        },
170        FastCandidate {
171            symbols: &[Symbol::One, Symbol::Three, Symbol::Div],
172        },
173        FastCandidate {
174            symbols: &[Symbol::Two, Symbol::Three, Symbol::Div],
175        },
176        FastCandidate {
177            symbols: &[Symbol::One, Symbol::Four, Symbol::Div],
178        },
179        FastCandidate {
180            symbols: &[Symbol::Three, Symbol::Four, Symbol::Div],
181        },
182        // Simple roots
183        FastCandidate {
184            symbols: &[Symbol::Two, Symbol::Sqrt],
185        },
186        FastCandidate {
187            symbols: &[Symbol::Three, Symbol::Sqrt],
188        },
189        FastCandidate {
190            symbols: &[Symbol::Five, Symbol::Sqrt],
191        },
192        FastCandidate {
193            symbols: &[Symbol::Six, Symbol::Sqrt],
194        },
195        FastCandidate {
196            symbols: &[Symbol::Seven, Symbol::Sqrt],
197        },
198        FastCandidate {
199            symbols: &[Symbol::Eight, Symbol::Sqrt],
200        },
201        FastCandidate {
202            symbols: &[Symbol::Pi, Symbol::Sqrt],
203        },
204        FastCandidate {
205            symbols: &[Symbol::E, Symbol::Sqrt],
206        },
207        // Simple logs
208        FastCandidate {
209            symbols: &[Symbol::Two, Symbol::Ln],
210        },
211        FastCandidate {
212            symbols: &[Symbol::Pi, Symbol::Ln],
213        },
214        // e ± small integers
215        FastCandidate {
216            symbols: &[Symbol::E, Symbol::One, Symbol::Sub],
217        },
218        FastCandidate {
219            symbols: &[Symbol::E, Symbol::One, Symbol::Add],
220        },
221        // pi ± small integers
222        FastCandidate {
223            symbols: &[Symbol::Pi, Symbol::One, Symbol::Sub],
224        },
225        FastCandidate {
226            symbols: &[Symbol::Pi, Symbol::One, Symbol::Add],
227        },
228        FastCandidate {
229            symbols: &[Symbol::Pi, Symbol::Two, Symbol::Sub],
230        },
231        // Common combinations
232        FastCandidate {
233            symbols: &[Symbol::One, Symbol::Two, Symbol::Add],
234        },
235        FastCandidate {
236            symbols: &[Symbol::One, Symbol::Sqrt, Symbol::One, Symbol::Add],
237        },
238        FastCandidate {
239            symbols: &[Symbol::Two, Symbol::Sqrt, Symbol::One, Symbol::Add],
240        },
241        // phi combinations (golden ratio)
242        FastCandidate {
243            symbols: &[Symbol::Phi, Symbol::One, Symbol::Add],
244        },
245        FastCandidate {
246            symbols: &[Symbol::Phi, Symbol::Two, Symbol::Add],
247        },
248        FastCandidate {
249            symbols: &[Symbol::Phi, Symbol::Square],
250        },
251        // Reciprocals of constants
252        FastCandidate {
253            symbols: &[Symbol::Pi, Symbol::Recip],
254        },
255        FastCandidate {
256            symbols: &[Symbol::E, Symbol::Recip],
257        },
258        FastCandidate {
259            symbols: &[Symbol::Phi, Symbol::Recip],
260        },
261    ];
262
263    CANDIDATES
264}
265
266/// Check if target matches a simple integer
267fn check_integer(target: f64) -> Option<(i64, f64)> {
268    let rounded = target.round();
269    let error = (target - rounded).abs();
270    if error < EXACT_TOLERANCE && rounded.abs() < 1000.0 {
271        Some((rounded as i64, error))
272    } else {
273        None
274    }
275}
276
277/// Configuration for fast match filtering
278pub struct FastMatchConfig<'a> {
279    /// Symbols that are excluded (via -N flag)
280    pub excluded_symbols: &'a HashSet<u8>,
281    /// Symbols that are explicitly allowed (all symbols must be in set)
282    pub allowed_symbols: Option<&'a HashSet<u8>>,
283    /// Minimum numeric type required (via -a, -r, -i flags)
284    pub min_num_type: NumType,
285}
286
287#[inline]
288fn passes_symbol_filters(symbols: &[Symbol], config: &FastMatchConfig<'_>) -> bool {
289    if contains_excluded(symbols, config.excluded_symbols) {
290        return false;
291    }
292    if let Some(allowed) = config.allowed_symbols {
293        if symbols.iter().any(|s| !allowed.contains(&(*s as u8))) {
294            return false;
295        }
296    }
297    true
298}
299
300/// Try to find a fast exact match for the target value
301///
302/// Returns a Match if found, or None if no simple exact match exists.
303/// This function is designed to be called before expensive generation.
304pub fn find_fast_match(
305    target: f64,
306    user_constants: &[UserConstant],
307    config: &FastMatchConfig<'_>,
308    table: &SymbolTable,
309) -> Option<Match> {
310    let context = EvalContext::from_slices(user_constants, &[]);
311    find_fast_match_with_context(target, &context, config, table)
312}
313
314/// Try to find a fast exact match for the target value using an explicit evaluation context.
315pub fn find_fast_match_with_context(
316    target: f64,
317    context: &EvalContext<'_>,
318    config: &FastMatchConfig<'_>,
319    table: &SymbolTable,
320) -> Option<Match> {
321    // First check integers (fastest)
322    if let Some((n, error)) = check_integer(target) {
323        if (1..=9).contains(&n) {
324            // We have a direct constant for 1-9
325            let symbols: &[Symbol] = match n {
326                1 => &[Symbol::One],
327                2 => &[Symbol::Two],
328                3 => &[Symbol::Three],
329                4 => &[Symbol::Four],
330                5 => &[Symbol::Five],
331                6 => &[Symbol::Six],
332                7 => &[Symbol::Seven],
333                8 => &[Symbol::Eight],
334                9 => &[Symbol::Nine],
335                _ => return None,
336            };
337            // Check if excluded or wrong type
338            if passes_symbol_filters(symbols, config)
339                && get_num_type(symbols) >= config.min_num_type
340            {
341                if let Some(m) = make_match(symbols, target, error, table, context) {
342                    return Some(m);
343                }
344            }
345        }
346        // For other integers, check if they match user constants
347        for (idx, uc) in context.user_constants.iter().enumerate() {
348            if idx < 16 && (uc.value - target).abs() < EXACT_TOLERANCE {
349                if let Some(sym) = Symbol::from_byte(128 + idx as u8) {
350                    let symbols = [sym];
351                    if passes_symbol_filters(&symbols, config) && uc.num_type >= config.min_num_type
352                    {
353                        if let Some(m) =
354                            make_match(&symbols, target, (uc.value - target).abs(), table, context)
355                        {
356                            return Some(m);
357                        }
358                    }
359                }
360            }
361        }
362    }
363
364    // Check user constants first (they're explicitly defined)
365    for (idx, uc) in context.user_constants.iter().enumerate() {
366        if idx >= 16 {
367            break;
368        }
369        if (uc.value - target).abs() < EXACT_TOLERANCE {
370            if let Some(sym) = Symbol::from_byte(128 + idx as u8) {
371                let symbols = [sym];
372                if passes_symbol_filters(&symbols, config) && uc.num_type >= config.min_num_type {
373                    if let Some(m) =
374                        make_match(&symbols, target, (uc.value - target).abs(), table, context)
375                    {
376                        return Some(m);
377                    }
378                }
379            }
380        }
381    }
382
383    // Check known constant candidates
384    let candidates = get_constant_candidates();
385    for candidate in candidates {
386        // Skip if contains excluded symbols
387        if !passes_symbol_filters(candidate.symbols, config) {
388            continue;
389        }
390        // Skip if type doesn't meet requirement
391        if get_num_type(candidate.symbols) < config.min_num_type {
392            continue;
393        }
394
395        let expr = expr_from_symbols_with_table(candidate.symbols, table);
396        if let Ok(result) = evaluate_with_context(&expr, target, context) {
397            let error = (result.value - target).abs();
398            if error < EXACT_TOLERANCE {
399                if let Some(m) = make_match(candidate.symbols, target, error, table, context) {
400                    return Some(m);
401                }
402            }
403        }
404    }
405
406    // Check user constants with simple operations
407    for (idx, uc) in context.user_constants.iter().enumerate() {
408        if idx >= 16 {
409            break;
410        }
411        if let Some(sym) = Symbol::from_byte(128 + idx as u8) {
412            // Check 1/constant
413            if uc.value != 0.0 {
414                let recip_val = 1.0 / uc.value;
415                if (recip_val - target).abs() < EXACT_TOLERANCE {
416                    let symbols = [sym, Symbol::Recip];
417                    if passes_symbol_filters(&symbols, config) && uc.num_type >= config.min_num_type
418                    {
419                        if let Some(m) =
420                            make_match(&symbols, target, (recip_val - target).abs(), table, context)
421                        {
422                            return Some(m);
423                        }
424                    }
425                }
426            }
427            // Check sqrt(constant)
428            if uc.value > 0.0 {
429                let sqrt_val = uc.value.sqrt();
430                if (sqrt_val - target).abs() < EXACT_TOLERANCE {
431                    let symbols = [sym, Symbol::Sqrt];
432                    if passes_symbol_filters(&symbols, config) && uc.num_type >= config.min_num_type
433                    {
434                        if let Some(m) =
435                            make_match(&symbols, target, (sqrt_val - target).abs(), table, context)
436                        {
437                            return Some(m);
438                        }
439                    }
440                }
441            }
442        }
443    }
444
445    None
446}
447
448/// Create a Match from symbols representing the RHS value
449fn make_match(
450    symbols: &[Symbol],
451    target: f64,
452    error: f64,
453    table: &SymbolTable,
454    context: &EvalContext<'_>,
455) -> Option<Match> {
456    let lhs_expr = expr_from_symbols_with_table(&[Symbol::X], table);
457    let rhs_expr = expr_from_symbols_with_table(symbols, table);
458    let complexity = lhs_expr.complexity() + rhs_expr.complexity();
459
460    let lhs_eval = evaluate_with_context(&lhs_expr, target, context).ok()?;
461    let rhs_eval = evaluate_with_context(&rhs_expr, target, context).ok()?;
462
463    Some(Match {
464        lhs: EvaluatedExpr {
465            expr: lhs_expr,
466            value: lhs_eval.value,
467            derivative: lhs_eval.derivative,
468            num_type: lhs_eval.num_type,
469        },
470        rhs: EvaluatedExpr {
471            expr: rhs_expr,
472            value: rhs_eval.value,
473            derivative: 0.0,
474            num_type: rhs_eval.num_type,
475        },
476        x_value: target,
477        error,
478        complexity,
479    })
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    fn default_config() -> FastMatchConfig<'static> {
487        static EMPTY: std::sync::OnceLock<HashSet<u8>> = std::sync::OnceLock::new();
488        let empty = EMPTY.get_or_init(HashSet::new);
489        FastMatchConfig {
490            excluded_symbols: empty,
491            allowed_symbols: None,
492            min_num_type: NumType::Transcendental,
493        }
494    }
495
496    fn default_table() -> SymbolTable {
497        SymbolTable::new()
498    }
499
500    #[test]
501    fn test_pi_match() {
502        let m = find_fast_match(
503            std::f64::consts::PI,
504            &[],
505            &default_config(),
506            &default_table(),
507        );
508        assert!(m.is_some());
509        let m = m.unwrap();
510        assert!(m.error.abs() < 1e-14);
511        assert_eq!(m.rhs.expr.to_postfix(), "p");
512    }
513
514    #[test]
515    fn test_pi_excluded() {
516        let excluded: HashSet<u8> = vec![b'p'].into_iter().collect();
517        let config = FastMatchConfig {
518            excluded_symbols: &excluded,
519            allowed_symbols: None,
520            min_num_type: NumType::Transcendental,
521        };
522        let m = find_fast_match(std::f64::consts::PI, &[], &config, &default_table());
523        assert!(m.is_none(), "Should not find pi when it's excluded");
524    }
525
526    #[test]
527    fn test_pi_algebraic_only() {
528        static EMPTY: std::sync::OnceLock<HashSet<u8>> = std::sync::OnceLock::new();
529        let empty = EMPTY.get_or_init(HashSet::new);
530        let config = FastMatchConfig {
531            excluded_symbols: empty,
532            allowed_symbols: None,
533            min_num_type: NumType::Algebraic,
534        };
535        let m = find_fast_match(std::f64::consts::PI, &[], &config, &default_table());
536        assert!(
537            m.is_none(),
538            "Should not find pi when only algebraic allowed"
539        );
540    }
541
542    #[test]
543    fn test_sqrt2_algebraic_ok() {
544        static EMPTY: std::sync::OnceLock<HashSet<u8>> = std::sync::OnceLock::new();
545        let empty = EMPTY.get_or_init(HashSet::new);
546        let config = FastMatchConfig {
547            excluded_symbols: empty,
548            allowed_symbols: None,
549            min_num_type: NumType::Algebraic,
550        };
551        let m = find_fast_match(2.0_f64.sqrt(), &[], &config, &default_table());
552        assert!(m.is_some(), "sqrt(2) should be found with algebraic-only");
553    }
554
555    #[test]
556    fn test_e_match() {
557        let m = find_fast_match(
558            std::f64::consts::E,
559            &[],
560            &default_config(),
561            &default_table(),
562        );
563        assert!(m.is_some());
564        let m = m.unwrap();
565        assert!(m.error.abs() < 1e-14);
566        assert_eq!(m.rhs.expr.to_postfix(), "e");
567    }
568
569    #[test]
570    fn test_sqrt2_match() {
571        let m = find_fast_match(2.0_f64.sqrt(), &[], &default_config(), &default_table());
572        assert!(m.is_some());
573        let m = m.unwrap();
574        assert!(m.error.abs() < 1e-14);
575        assert_eq!(m.rhs.expr.to_postfix(), "2q");
576    }
577
578    #[test]
579    fn test_phi_match() {
580        let phi = (1.0 + 5.0_f64.sqrt()) / 2.0;
581        let m = find_fast_match(phi, &[], &default_config(), &default_table());
582        assert!(m.is_some());
583        let m = m.unwrap();
584        assert!(m.error.abs() < 1e-14);
585        assert_eq!(m.rhs.expr.to_postfix(), "f");
586    }
587
588    #[test]
589    fn test_integer_match() {
590        let m = find_fast_match(5.0, &[], &default_config(), &default_table());
591        assert!(m.is_some());
592        let m = m.unwrap();
593        assert!(m.error.abs() < 1e-14);
594        assert_eq!(m.rhs.expr.to_postfix(), "5");
595        assert_eq!(m.lhs.num_type, NumType::Integer);
596    }
597
598    #[test]
599    fn test_division_candidate_is_rational() {
600        assert_eq!(
601            get_num_type(&[Symbol::Two, Symbol::Three, Symbol::Div]),
602            NumType::Rational
603        );
604
605        let m = find_fast_match(2.0 / 3.0, &[], &default_config(), &default_table());
606        assert!(m.is_some());
607        let m = m.unwrap();
608        assert_eq!(m.rhs.expr.to_postfix(), "23/");
609        assert_eq!(m.rhs.num_type, NumType::Rational);
610    }
611
612    #[test]
613    fn test_no_match_for_random() {
614        // 2.506314 is not a simple constant
615        let m = find_fast_match(2.506314, &[], &default_config(), &default_table());
616        assert!(m.is_none());
617    }
618
619    #[test]
620    fn test_user_constant_match() {
621        let uc = UserConstant {
622            weight: 4,
623            name: "myconst".to_string(),
624            description: "Test constant".to_string(),
625            value: std::f64::consts::E,
626            num_type: NumType::Transcendental,
627        };
628        let m = find_fast_match(
629            std::f64::consts::E,
630            &[uc],
631            &default_config(),
632            &default_table(),
633        );
634        assert!(m.is_some());
635    }
636}