1use crate::eval::EvalContext;
6use crate::expr::EvaluatedExpr;
7use crate::pool::{RankingMode, TopKPool};
8use crate::profile::UserConstant;
9use crate::thresholds::{
10 DEGENERATE_DERIVATIVE, DEGENERATE_RANGE_TOLERANCE, DEGENERATE_TEST_THRESHOLD,
11 EXACT_MATCH_TOLERANCE, NEWTON_FINAL_TOLERANCE,
12};
13use std::collections::HashSet;
14use std::time::Duration;
15
16mod db;
17mod newton;
18#[cfg(test)]
19mod tests;
20
21use db::calculate_adaptive_search_radius;
22pub use db::{ComplexityTier, ExprDatabase, TieredExprDatabase};
23#[cfg(test)]
24use newton::newton_raphson;
25use newton::newton_raphson_with_constants;
26
27#[derive(Clone, Copy, Debug)]
28struct SearchTimer {
29 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
30 start_ms: f64,
31 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
32 start: std::time::Instant,
33}
34
35impl SearchTimer {
36 #[inline]
37 fn start() -> Self {
38 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
39 {
40 Self {
41 start_ms: js_sys::Date::now(),
42 }
43 }
44
45 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
46 {
47 Self {
48 start: std::time::Instant::now(),
49 }
50 }
51 }
52
53 #[inline]
54 fn elapsed(self) -> Duration {
55 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
56 {
57 let elapsed_ms = (js_sys::Date::now() - self.start_ms).max(0.0);
58 Duration::from_secs_f64(elapsed_ms / 1000.0)
59 }
60
61 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
62 {
63 self.start.elapsed()
64 }
65 }
66}
67
68#[derive(Clone, Debug, Default)]
70pub struct SearchStats {
71 pub gen_time: Duration,
73 pub search_time: Duration,
75 pub lhs_count: usize,
77 pub rhs_count: usize,
79 pub lhs_tested: usize,
81 pub candidates_tested: usize,
83 pub newton_calls: usize,
85 pub newton_success: usize,
87 pub pool_insertions: usize,
89 pub pool_rejections_error: usize,
91 pub pool_rejections_dedupe: usize,
93 pub pool_evictions: usize,
95 pub pool_final_size: usize,
97 pub pool_best_error: f64,
99 pub early_exit: bool,
101}
102
103impl SearchStats {
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn print(&self) {
110 println!();
111 println!(" === Search Statistics ===");
112 println!();
113 println!(" Generation:");
114 println!(
115 " Time: {:>10.3}ms",
116 self.gen_time.as_secs_f64() * 1000.0
117 );
118 println!(" LHS expressions: {:>10}", self.lhs_count);
119 println!(" RHS expressions: {:>10}", self.rhs_count);
120 println!();
121 println!(" Search:");
122 println!(
123 " Time: {:>10.3}ms",
124 self.search_time.as_secs_f64() * 1000.0
125 );
126 println!(" LHS tested: {:>10}", self.lhs_tested);
127 println!(" Candidates: {:>10}", self.candidates_tested);
128 println!(" Newton calls: {:>10}", self.newton_calls);
129 println!(
130 " Newton success: {:>10} ({:.1}%)",
131 self.newton_success,
132 if self.newton_calls > 0 {
133 100.0 * self.newton_success as f64 / self.newton_calls as f64
134 } else {
135 0.0
136 }
137 );
138 if self.early_exit {
139 println!(" Early exit: yes");
140 }
141 println!();
142 println!(" Pool:");
143 println!(" Insertions: {:>10}", self.pool_insertions);
144 println!(" Rejected (err): {:>10}", self.pool_rejections_error);
145 println!(" Rejected (dup): {:>10}", self.pool_rejections_dedupe);
146 println!(" Evictions: {:>10}", self.pool_evictions);
147 println!(" Final size: {:>10}", self.pool_final_size);
148 println!(" Best error: {:>14.2e}", self.pool_best_error);
149 }
150}
151
152#[inline]
181pub fn level_to_complexity(level: u32) -> (u32, u32) {
182 const BASE_LHS: u32 = 10;
183 const BASE_RHS: u32 = 12;
184 const LEVEL_MULTIPLIER: u32 = 4;
185
186 let level_factor = LEVEL_MULTIPLIER.saturating_mul(level);
189 (
190 BASE_LHS.saturating_add(level_factor),
191 BASE_RHS.saturating_add(level_factor),
192 )
193}
194
195#[derive(Clone, Debug)]
197pub struct Match {
198 pub lhs: EvaluatedExpr,
200 pub rhs: EvaluatedExpr,
202 pub x_value: f64,
204 pub error: f64,
206 pub complexity: u32,
208}
209
210impl Match {
211 #[cfg(test)]
213 pub fn display(&self, _target: f64) -> String {
214 let lhs_str = self.lhs.expr.to_infix();
215 let rhs_str = self.rhs.expr.to_infix();
216
217 let error_str = if self.error.abs() < EXACT_MATCH_TOLERANCE {
218 "('exact' match)".to_string()
219 } else {
220 let sign = if self.error >= 0.0 { "+" } else { "-" };
221 format!("for x = T {} {:.6e}", sign, self.error.abs())
222 };
223
224 format!(
225 "{:>24} = {:<24} {} {{{}}}",
226 lhs_str, rhs_str, error_str, self.complexity
227 )
228 }
229}
230
231#[derive(Clone, Debug)]
263pub struct SearchConfig {
264 pub target: f64,
271
272 pub max_matches: usize,
279
280 pub max_error: f64,
287
288 pub stop_at_exact: bool,
295
296 pub stop_below: Option<f64>,
303
304 pub zero_value_threshold: f64,
312
313 pub newton_iterations: usize,
320
321 pub user_constants: Vec<UserConstant>,
328
329 pub user_functions: Vec<crate::udf::UserFunction>,
336
337 pub trig_argument_scale: f64,
342
343 pub refine_with_newton: bool,
350
351 pub rhs_allowed_symbols: Option<HashSet<u8>>,
359
360 pub rhs_excluded_symbols: Option<HashSet<u8>>,
368
369 pub show_newton: bool,
376
377 pub show_match_checks: bool,
384
385 #[allow(dead_code)]
392 pub show_pruned_arith: bool,
393
394 pub show_pruned_range: bool,
401
402 pub show_db_adds: bool,
409
410 #[allow(dead_code)]
418 pub match_all_digits: bool,
419
420 pub derivative_margin: f64,
428
429 pub ranking_mode: RankingMode,
437}
438
439impl Default for SearchConfig {
440 fn default() -> Self {
441 Self {
442 target: 0.0,
443 max_matches: 100,
444 max_error: 1.0,
445 stop_at_exact: false,
446 stop_below: None,
447 zero_value_threshold: 1e-4,
448 newton_iterations: 15,
449 user_constants: Vec::new(),
450 user_functions: Vec::new(),
451 trig_argument_scale: crate::eval::DEFAULT_TRIG_ARGUMENT_SCALE,
452 refine_with_newton: true,
453 rhs_allowed_symbols: None,
454 rhs_excluded_symbols: None,
455 show_newton: false,
456 show_match_checks: false,
457 show_pruned_arith: false,
458 show_pruned_range: false,
459 show_db_adds: false,
460 match_all_digits: false,
461 derivative_margin: DEGENERATE_DERIVATIVE,
462 ranking_mode: RankingMode::Complexity,
463 }
464 }
465}
466
467impl SearchConfig {
468 pub fn context(&self) -> SearchContext<'_> {
470 SearchContext::new(self)
471 }
472
473 #[inline]
474 fn rhs_symbol_allowed(&self, rhs: &crate::expr::Expression) -> bool {
475 let symbols = rhs.symbols();
476
477 if let Some(allowed) = &self.rhs_allowed_symbols {
478 if symbols.iter().any(|s| !allowed.contains(&(*s as u8))) {
479 return false;
480 }
481 }
482
483 if let Some(excluded) = &self.rhs_excluded_symbols {
484 if symbols.iter().any(|s| excluded.contains(&(*s as u8))) {
485 return false;
486 }
487 }
488
489 true
490 }
491}
492
493#[derive(Clone, Copy, Debug)]
495pub struct SearchContext<'a> {
496 pub config: &'a SearchConfig,
498 pub eval: EvalContext<'a>,
500}
501
502impl<'a> SearchContext<'a> {
503 pub fn new(config: &'a SearchConfig) -> Self {
504 Self {
505 config,
506 eval: EvalContext::from_slices(&config.user_constants, &config.user_functions)
507 .with_trig_argument_scale(config.trig_argument_scale),
508 }
509 }
510}
511
512#[allow(dead_code)]
517pub fn search(target: f64, gen_config: &crate::gen::GenConfig, max_matches: usize) -> Vec<Match> {
518 let (matches, _stats) = search_with_stats(target, gen_config, max_matches);
519 matches
520}
521
522#[allow(dead_code)]
527pub fn search_with_stats(
528 target: f64,
529 gen_config: &crate::gen::GenConfig,
530 max_matches: usize,
531) -> (Vec<Match>, SearchStats) {
532 search_with_stats_and_options(target, gen_config, max_matches, false, None)
533}
534
535pub fn search_with_stats_and_options(
537 target: f64,
538 gen_config: &crate::gen::GenConfig,
539 max_matches: usize,
540 stop_at_exact: bool,
541 stop_below: Option<f64>,
542) -> (Vec<Match>, SearchStats) {
543 if !target.is_finite() {
544 return (Vec::new(), SearchStats::default());
545 }
546 let config = SearchConfig {
547 target,
548 max_matches,
549 stop_at_exact,
550 stop_below,
551 user_constants: gen_config.user_constants.clone(),
552 user_functions: gen_config.user_functions.clone(),
553 ..Default::default()
554 };
555
556 search_with_stats_and_config(gen_config, &config)
557}
558
559pub fn search_with_stats_and_config(
565 gen_config: &crate::gen::GenConfig,
566 config: &SearchConfig,
567) -> (Vec<Match>, SearchStats) {
568 if !config.target.is_finite() {
569 return (Vec::new(), SearchStats::default());
570 }
571
572 use crate::gen::generate_all_with_limit_and_context;
573
574 const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
575 let context = SearchContext::new(config);
576
577 let gen_start = SearchTimer::start();
578
579 if let Some(generated) = generate_all_with_limit_and_context(
581 gen_config,
582 config.target,
583 &context.eval,
584 MAX_EXPRESSIONS_BEFORE_STREAMING,
585 ) {
586 let gen_time = gen_start.elapsed();
587
588 let mut db = ExprDatabase::new();
590 db.insert_rhs(generated.rhs);
591
592 let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
594
595 stats.gen_time = gen_time;
597 stats.lhs_count = generated.lhs.len();
598 stats.rhs_count = db.rhs_count();
599
600 (matches, stats)
601 } else {
602 search_streaming_with_config(gen_config, config)
604 }
605}
606
607pub fn search_adaptive(
641 base_config: &crate::gen::GenConfig,
642 search_config: &SearchConfig,
643 level: u32,
644) -> (Vec<Match>, SearchStats) {
645 use crate::expr::EvaluatedExpr;
646 use crate::gen::{quantize_value, LhsKey};
647 use std::collections::HashMap;
648
649 let gen_start = SearchTimer::start();
650 let context = SearchContext::new(search_config);
651 let mut lhs_map: HashMap<LhsKey, EvaluatedExpr> = HashMap::new();
655 let mut rhs_map: HashMap<i64, EvaluatedExpr> = HashMap::new();
656
657 let (std_lhs, std_rhs) = level_to_complexity(level);
660
661 let mut config = base_config.clone();
662 config.max_lhs_complexity = std_lhs.max(base_config.max_lhs_complexity);
663 config.max_rhs_complexity = std_rhs.max(base_config.max_rhs_complexity);
664
665 let generated = {
668 #[cfg(feature = "parallel")]
669 {
670 crate::gen::generate_all_parallel_with_context(
671 &config,
672 search_config.target,
673 &context.eval,
674 )
675 }
676 #[cfg(not(feature = "parallel"))]
677 {
678 crate::gen::generate_all_with_context(&config, search_config.target, &context.eval)
679 }
680 };
681
682 for expr in generated.lhs {
684 let key = (quantize_value(expr.value), quantize_value(expr.derivative));
685 lhs_map
686 .entry(key)
687 .and_modify(|existing| {
688 if expr.expr.complexity() < existing.expr.complexity() {
689 *existing = expr.clone();
690 }
691 })
692 .or_insert(expr);
693 }
694
695 for expr in generated.rhs {
697 let key = quantize_value(expr.value);
698 rhs_map
699 .entry(key)
700 .and_modify(|existing| {
701 if expr.expr.complexity() < existing.expr.complexity() {
702 *existing = expr.clone();
703 }
704 })
705 .or_insert(expr);
706 }
707
708 let all_lhs: Vec<EvaluatedExpr> = lhs_map.into_values().collect();
709 let all_rhs: Vec<EvaluatedExpr> = rhs_map.into_values().collect();
710
711 let gen_time = gen_start.elapsed();
712
713 let mut db = ExprDatabase::new();
715 db.insert_rhs(all_rhs);
716
717 let search_start = SearchTimer::start();
718 let (matches, match_stats) = db.find_matches_with_stats_and_context(&all_lhs, &context);
719 let search_time = search_start.elapsed();
720
721 let mut stats = SearchStats::new();
723 stats.gen_time = gen_time;
724 stats.search_time = search_time;
725 stats.lhs_count = all_lhs.len();
726 stats.rhs_count = db.rhs_count();
727 stats.lhs_tested = match_stats.lhs_tested;
728 stats.candidates_tested = match_stats.candidates_tested;
729 stats.newton_calls = match_stats.newton_calls;
730 stats.newton_success = match_stats.newton_success;
731 stats.pool_insertions = match_stats.pool_insertions;
732 stats.pool_rejections_error = match_stats.pool_rejections_error;
733 stats.pool_rejections_dedupe = match_stats.pool_rejections_dedupe;
734 stats.pool_evictions = match_stats.pool_evictions;
735 stats.pool_final_size = match_stats.pool_final_size;
736 stats.pool_best_error = match_stats.pool_best_error;
737 stats.early_exit = match_stats.early_exit;
738
739 (matches, stats)
740}
741
742#[allow(dead_code)]
774pub fn search_streaming(
775 target: f64,
776 gen_config: &crate::gen::GenConfig,
777 max_matches: usize,
778 stop_at_exact: bool,
779 stop_below: Option<f64>,
780) -> (Vec<Match>, SearchStats) {
781 let config = SearchConfig {
782 target,
783 max_matches,
784 stop_at_exact,
785 stop_below,
786 user_constants: gen_config.user_constants.clone(),
787 user_functions: gen_config.user_functions.clone(),
788 ..Default::default()
789 };
790
791 search_streaming_with_config(gen_config, &config)
792}
793
794pub fn search_streaming_with_config(
796 gen_config: &crate::gen::GenConfig,
797 search_config: &SearchConfig,
798) -> (Vec<Match>, SearchStats) {
799 use crate::gen::{generate_streaming_with_context, StreamingCallbacks};
800 use std::collections::HashMap;
801
802 let gen_start = SearchTimer::start();
803 let mut stats = SearchStats::new();
804 let context = SearchContext::new(search_config);
805
806 let initial_max_error = search_config.max_error.max(1e-12);
808
809 let mut pool = TopKPool::new_with_diagnostics(
811 search_config.max_matches,
812 initial_max_error,
813 search_config.show_db_adds,
814 search_config.ranking_mode,
815 );
816
817 let mut rhs_db = TieredExprDatabase::new();
819 let mut rhs_map: HashMap<i64, crate::expr::EvaluatedExpr> = HashMap::new();
820
821 let mut lhs_exprs: Vec<crate::expr::EvaluatedExpr> = Vec::new();
823
824 {
826 let mut callbacks = StreamingCallbacks {
827 on_rhs: &mut |expr| {
828 let key = crate::gen::quantize_value(expr.value);
831 rhs_map
832 .entry(key)
833 .and_modify(|existing| {
834 if expr.expr.complexity() < existing.expr.complexity() {
835 *existing = expr.clone();
836 }
837 })
838 .or_insert_with(|| expr.clone());
839 true
840 },
841 on_lhs: &mut |expr| {
842 lhs_exprs.push(expr.clone());
843 true
844 },
845 };
846
847 generate_streaming_with_context(
849 gen_config,
850 search_config.target,
851 &context.eval,
852 &mut callbacks,
853 );
854 }
855
856 for expr in rhs_map.into_values() {
858 rhs_db.insert(expr);
859 }
860 rhs_db.finalize();
861
862 stats.rhs_count = rhs_db.total_count();
863 stats.lhs_count = lhs_exprs.len();
864 stats.gen_time = gen_start.elapsed();
865
866 let search_start = SearchTimer::start();
868
869 lhs_exprs.sort_by_key(|e| e.expr.complexity());
871
872 let mut early_exit = false;
874
875 'outer: for lhs in &lhs_exprs {
876 if early_exit {
877 break;
878 }
879
880 if lhs.value.abs() < search_config.zero_value_threshold {
882 if search_config.show_pruned_range {
883 eprintln!(
884 " [pruned range] value={:.6e} reason=\"near-zero\" expr=\"{}\"",
885 lhs.value,
886 lhs.expr.to_infix()
887 );
888 }
889 continue;
890 }
891
892 if lhs.derivative.abs() < DEGENERATE_TEST_THRESHOLD {
894 let test_x = search_config.target + std::f64::consts::E;
895 if let Ok(test_result) =
899 crate::eval::evaluate_fast_with_context(&lhs.expr, test_x, &context.eval)
900 {
901 let value_unchanged =
902 (test_result.value - lhs.value).abs() < DEGENERATE_TEST_THRESHOLD;
903 let deriv_still_zero = test_result.derivative.abs() < DEGENERATE_TEST_THRESHOLD;
904 if deriv_still_zero || value_unchanged {
905 continue;
906 }
907 }
908
909 stats.lhs_tested += 1;
911 for rhs in rhs_db.iter_tiers_in_range(
912 lhs.value - DEGENERATE_RANGE_TOLERANCE,
913 lhs.value + DEGENERATE_RANGE_TOLERANCE,
914 ) {
915 if !search_config.rhs_symbol_allowed(&rhs.expr) {
916 continue;
917 }
918 stats.candidates_tested += 1;
919 if search_config.show_match_checks {
920 eprintln!(
921 " [match] checking lhs={:.6} rhs={:.6}",
922 lhs.value, rhs.value
923 );
924 }
925 let val_diff = (lhs.value - rhs.value).abs();
926 if val_diff < DEGENERATE_RANGE_TOLERANCE && pool.would_accept(0.0, true) {
927 let m = Match {
928 lhs: lhs.clone(),
929 rhs: rhs.clone(),
930 x_value: search_config.target,
931 error: 0.0,
932 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
933 };
934 pool.try_insert(m);
935 }
936 }
937 continue;
938 }
939
940 stats.lhs_tested += 1;
941
942 let search_radius = calculate_adaptive_search_radius(
944 lhs.derivative,
945 lhs.expr.complexity(),
946 pool.len(),
947 search_config.max_matches,
948 pool.best_error,
949 );
950 let low = lhs.value - search_radius;
951 let high = lhs.value + search_radius;
952
953 for rhs in rhs_db.iter_tiers_in_range(low, high) {
955 if !search_config.rhs_symbol_allowed(&rhs.expr) {
956 continue;
957 }
958 stats.candidates_tested += 1;
959 if search_config.show_match_checks {
960 eprintln!(
961 " [match] checking lhs={:.6} rhs={:.6}",
962 lhs.value, rhs.value
963 );
964 }
965
966 let val_diff = lhs.value - rhs.value;
968 let x_delta = -val_diff / lhs.derivative;
969 let coarse_error = x_delta.abs();
970
971 let is_potentially_exact = coarse_error < NEWTON_FINAL_TOLERANCE;
973 if !pool.would_accept_strict(coarse_error, is_potentially_exact) {
974 continue;
975 }
976
977 if !search_config.refine_with_newton {
978 let refined_x = search_config.target + x_delta;
979 let refined_error = x_delta;
980 let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
981
982 if pool.would_accept(refined_error.abs(), is_exact) {
983 let m = Match {
984 lhs: lhs.clone(),
985 rhs: rhs.clone(),
986 x_value: refined_x,
987 error: refined_error,
988 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
989 };
990
991 pool.try_insert(m);
992
993 if search_config.stop_at_exact && is_exact {
994 early_exit = true;
995 break 'outer;
996 }
997 if let Some(threshold) = search_config.stop_below {
998 if refined_error.abs() < threshold {
999 early_exit = true;
1000 break 'outer;
1001 }
1002 }
1003 }
1004 continue;
1005 }
1006
1007 stats.newton_calls += 1;
1009 if let Some(refined_x) = newton_raphson_with_constants(
1010 &lhs.expr,
1011 rhs.value,
1012 search_config.target,
1013 search_config.newton_iterations,
1014 &context.eval,
1015 search_config.show_newton,
1016 search_config.derivative_margin,
1017 ) {
1018 stats.newton_success += 1;
1019 let refined_error = refined_x - search_config.target;
1020 let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
1021
1022 if pool.would_accept(refined_error.abs(), is_exact) {
1024 let m = Match {
1025 lhs: lhs.clone(),
1026 rhs: rhs.clone(),
1027 x_value: refined_x,
1028 error: refined_error,
1029 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1030 };
1031
1032 pool.try_insert(m);
1034
1035 if search_config.stop_at_exact && is_exact {
1037 early_exit = true;
1038 break 'outer;
1039 }
1040 if let Some(threshold) = search_config.stop_below {
1041 if refined_error.abs() < threshold {
1042 early_exit = true;
1043 break 'outer;
1044 }
1045 }
1046 }
1047 }
1048 }
1049 }
1050
1051 stats.pool_insertions = pool.stats.insertions;
1053 stats.pool_rejections_error = pool.stats.rejections_error;
1054 stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
1055 stats.pool_evictions = pool.stats.evictions;
1056 stats.pool_final_size = pool.len();
1057 stats.pool_best_error = pool.best_error;
1058 stats.search_time = search_start.elapsed();
1059 stats.early_exit = early_exit;
1060
1061 (pool.into_sorted(), stats)
1062}
1063
1064pub fn search_one_sided_with_stats_and_config(
1066 gen_config: &crate::gen::GenConfig,
1067 config: &SearchConfig,
1068) -> (Vec<Match>, SearchStats) {
1069 use crate::eval::evaluate_with_context;
1070 use crate::expr::Expression;
1071 use crate::gen::generate_all_with_context;
1072 use crate::symbol::Symbol;
1073
1074 let gen_start = SearchTimer::start();
1075 let context = SearchContext::new(config);
1076
1077 let mut rhs_only = gen_config.clone();
1078 rhs_only.generate_lhs = false;
1079 rhs_only.generate_rhs = true;
1080
1081 let generated = generate_all_with_context(&rhs_only, config.target, &context.eval);
1082 let gen_time = gen_start.elapsed();
1083
1084 let search_start = SearchTimer::start();
1085 let initial_max_error = config.max_error.max(1e-12);
1086 let mut pool = TopKPool::new_with_diagnostics(
1087 config.max_matches,
1088 initial_max_error,
1089 config.show_db_adds,
1090 config.ranking_mode,
1091 );
1092 let mut stats = SearchStats::new();
1093 let mut early_exit = false;
1094
1095 let mut lhs_expr = Expression::new();
1096 lhs_expr.push_with_table(Symbol::X, &gen_config.symbol_table);
1097 let lhs_eval = evaluate_with_context(&lhs_expr, config.target, &context.eval);
1098 let lhs_eval = match lhs_eval {
1099 Ok(v) => v,
1100 Err(_) => {
1101 stats.gen_time = gen_time;
1102 stats.search_time = search_start.elapsed();
1103 return (Vec::new(), stats);
1104 }
1105 };
1106 let lhs = EvaluatedExpr::new(
1107 lhs_expr,
1108 lhs_eval.value,
1109 lhs_eval.derivative,
1110 lhs_eval.num_type,
1111 );
1112
1113 stats.lhs_count = 1;
1114 stats.rhs_count = generated.rhs.len();
1115 stats.lhs_tested = 1;
1116
1117 for rhs in generated.rhs {
1118 if !config.rhs_symbol_allowed(&rhs.expr) {
1119 continue;
1120 }
1121 stats.candidates_tested += 1;
1122 if config.show_match_checks {
1123 eprintln!(
1124 " [match] checking lhs={:.6} rhs={:.6}",
1125 lhs.value, rhs.value
1126 );
1127 }
1128
1129 let error = rhs.value - config.target;
1130 let is_exact = error.abs() < EXACT_MATCH_TOLERANCE;
1131 if !pool.would_accept(error.abs(), is_exact) {
1132 continue;
1133 }
1134
1135 let m = Match {
1136 lhs: lhs.clone(),
1137 rhs: rhs.clone(),
1138 x_value: rhs.value,
1139 error,
1140 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1141 };
1142
1143 pool.try_insert(m);
1144
1145 if config.stop_at_exact && is_exact {
1146 early_exit = true;
1147 break;
1148 }
1149 if let Some(threshold) = config.stop_below {
1150 if error.abs() < threshold {
1151 early_exit = true;
1152 break;
1153 }
1154 }
1155 }
1156
1157 stats.pool_insertions = pool.stats.insertions;
1158 stats.pool_rejections_error = pool.stats.rejections_error;
1159 stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
1160 stats.pool_evictions = pool.stats.evictions;
1161 stats.pool_final_size = pool.len();
1162 stats.pool_best_error = pool.best_error;
1163 stats.gen_time = gen_time;
1164 stats.search_time = search_start.elapsed();
1165 stats.early_exit = early_exit;
1166
1167 (pool.into_sorted(), stats)
1168}
1169
1170#[cfg(feature = "parallel")]
1175#[allow(dead_code)]
1176pub fn search_parallel(
1177 target: f64,
1178 gen_config: &crate::gen::GenConfig,
1179 max_matches: usize,
1180) -> Vec<Match> {
1181 let (matches, _stats) = search_parallel_with_stats(target, gen_config, max_matches);
1182 matches
1183}
1184
1185#[cfg(feature = "parallel")]
1190#[allow(dead_code)]
1191pub fn search_parallel_with_stats(
1192 target: f64,
1193 gen_config: &crate::gen::GenConfig,
1194 max_matches: usize,
1195) -> (Vec<Match>, SearchStats) {
1196 search_parallel_with_stats_and_options(target, gen_config, max_matches, false, None)
1197}
1198
1199#[cfg(feature = "parallel")]
1201pub fn search_parallel_with_stats_and_options(
1202 target: f64,
1203 gen_config: &crate::gen::GenConfig,
1204 max_matches: usize,
1205 stop_at_exact: bool,
1206 stop_below: Option<f64>,
1207) -> (Vec<Match>, SearchStats) {
1208 let config = SearchConfig {
1209 target,
1210 max_matches,
1211 stop_at_exact,
1212 stop_below,
1213 user_constants: gen_config.user_constants.clone(),
1214 user_functions: gen_config.user_functions.clone(),
1215 ..Default::default()
1216 };
1217
1218 search_parallel_with_stats_and_config(gen_config, &config)
1219}
1220
1221#[cfg(feature = "parallel")]
1227pub fn search_parallel_with_stats_and_config(
1228 gen_config: &crate::gen::GenConfig,
1229 config: &SearchConfig,
1230) -> (Vec<Match>, SearchStats) {
1231 use crate::gen::generate_all_with_limit_and_context;
1232
1233 const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
1234 let context = SearchContext::new(config);
1235
1236 let gen_start = SearchTimer::start();
1237
1238 if let Some(generated) = generate_all_with_limit_and_context(
1240 gen_config,
1241 config.target,
1242 &context.eval,
1243 MAX_EXPRESSIONS_BEFORE_STREAMING,
1244 ) {
1245 let gen_time = gen_start.elapsed();
1246
1247 let mut db = ExprDatabase::new();
1249 db.insert_rhs(generated.rhs);
1250
1251 let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
1253
1254 stats.gen_time = gen_time;
1256 stats.lhs_count = generated.lhs.len();
1257 stats.rhs_count = db.rhs_count();
1258
1259 (matches, stats)
1260 } else {
1261 search_streaming_with_config(gen_config, config)
1263 }
1264}