1use 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
15const EXACT_TOLERANCE: f64 = 1e-14;
17
18fn 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
27fn get_num_type(symbols: &[Symbol]) -> NumType {
30 use Symbol::*;
38
39 if symbols.len() == 1 {
41 return symbols[0].inherent_type();
42 }
43
44 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; }
53 if matches!(symbols[0], Pi | E | Gamma | Apery | Catalan) {
55 return NumType::Transcendental;
56 }
57 }
58 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 if symbols.len() == 3 && matches!(symbols[2], Div) {
71 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 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 for &sym in symbols {
93 if matches!(sym, Phi | Plastic) {
94 return NumType::Algebraic;
95 }
96 }
97
98 NumType::Transcendental
100}
101
102fn contains_excluded(symbols: &[Symbol], excluded: &HashSet<u8>) -> bool {
104 symbols.iter().any(|s| excluded.contains(&(*s as u8)))
105}
106
107struct FastCandidate {
109 symbols: &'static [Symbol],
111}
112
113fn get_constant_candidates() -> &'static [FastCandidate] {
115 static CANDIDATES: &[FastCandidate] = &[
116 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 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 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 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 FastCandidate {
209 symbols: &[Symbol::Two, Symbol::Ln],
210 },
211 FastCandidate {
212 symbols: &[Symbol::Pi, Symbol::Ln],
213 },
214 FastCandidate {
216 symbols: &[Symbol::E, Symbol::One, Symbol::Sub],
217 },
218 FastCandidate {
219 symbols: &[Symbol::E, Symbol::One, Symbol::Add],
220 },
221 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 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 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 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
266fn 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
277pub struct FastMatchConfig<'a> {
279 pub excluded_symbols: &'a HashSet<u8>,
281 pub allowed_symbols: Option<&'a HashSet<u8>>,
283 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
300pub 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
314pub fn find_fast_match_with_context(
316 target: f64,
317 context: &EvalContext<'_>,
318 config: &FastMatchConfig<'_>,
319 table: &SymbolTable,
320) -> Option<Match> {
321 if let Some((n, error)) = check_integer(target) {
323 if (1..=9).contains(&n) {
324 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 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 (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 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 let candidates = get_constant_candidates();
385 for candidate in candidates {
386 if !passes_symbol_filters(candidate.symbols, config) {
388 continue;
389 }
390 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 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 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 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
448fn 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 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}