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 let config = SearchConfig {
544 target,
545 max_matches,
546 stop_at_exact,
547 stop_below,
548 user_constants: gen_config.user_constants.clone(),
549 user_functions: gen_config.user_functions.clone(),
550 ..Default::default()
551 };
552
553 search_with_stats_and_config(gen_config, &config)
554}
555
556pub fn search_with_stats_and_config(
562 gen_config: &crate::gen::GenConfig,
563 config: &SearchConfig,
564) -> (Vec<Match>, SearchStats) {
565 use crate::gen::generate_all_with_limit_and_context;
566
567 const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
568 let context = SearchContext::new(config);
569
570 let gen_start = SearchTimer::start();
571
572 if let Some(generated) = generate_all_with_limit_and_context(
574 gen_config,
575 config.target,
576 &context.eval,
577 MAX_EXPRESSIONS_BEFORE_STREAMING,
578 ) {
579 let gen_time = gen_start.elapsed();
580
581 let mut db = ExprDatabase::new();
583 db.insert_rhs(generated.rhs);
584
585 let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
587
588 stats.gen_time = gen_time;
590 stats.lhs_count = generated.lhs.len();
591 stats.rhs_count = db.rhs_count();
592
593 (matches, stats)
594 } else {
595 search_streaming_with_config(gen_config, config)
597 }
598}
599
600pub fn search_adaptive(
634 base_config: &crate::gen::GenConfig,
635 search_config: &SearchConfig,
636 level: u32,
637) -> (Vec<Match>, SearchStats) {
638 use crate::expr::EvaluatedExpr;
639 use crate::gen::{quantize_value, LhsKey};
640 use std::collections::HashMap;
641
642 let gen_start = SearchTimer::start();
643 let context = SearchContext::new(search_config);
644 let mut lhs_map: HashMap<LhsKey, EvaluatedExpr> = HashMap::new();
648 let mut rhs_map: HashMap<i64, EvaluatedExpr> = HashMap::new();
649
650 let (std_lhs, std_rhs) = level_to_complexity(level);
653
654 let mut config = base_config.clone();
655 config.max_lhs_complexity = std_lhs.max(base_config.max_lhs_complexity);
656 config.max_rhs_complexity = std_rhs.max(base_config.max_rhs_complexity);
657
658 let generated = {
661 #[cfg(feature = "parallel")]
662 {
663 crate::gen::generate_all_parallel_with_context(
664 &config,
665 search_config.target,
666 &context.eval,
667 )
668 }
669 #[cfg(not(feature = "parallel"))]
670 {
671 crate::gen::generate_all_with_context(&config, search_config.target, &context.eval)
672 }
673 };
674
675 for expr in generated.lhs {
677 let key = (quantize_value(expr.value), quantize_value(expr.derivative));
678 lhs_map
679 .entry(key)
680 .and_modify(|existing| {
681 if expr.expr.complexity() < existing.expr.complexity() {
682 *existing = expr.clone();
683 }
684 })
685 .or_insert(expr);
686 }
687
688 for expr in generated.rhs {
690 let key = quantize_value(expr.value);
691 rhs_map
692 .entry(key)
693 .and_modify(|existing| {
694 if expr.expr.complexity() < existing.expr.complexity() {
695 *existing = expr.clone();
696 }
697 })
698 .or_insert(expr);
699 }
700
701 let all_lhs: Vec<EvaluatedExpr> = lhs_map.into_values().collect();
702 let all_rhs: Vec<EvaluatedExpr> = rhs_map.into_values().collect();
703
704 let gen_time = gen_start.elapsed();
705
706 let mut db = ExprDatabase::new();
708 db.insert_rhs(all_rhs);
709
710 let search_start = SearchTimer::start();
711 let (matches, match_stats) = db.find_matches_with_stats_and_context(&all_lhs, &context);
712 let search_time = search_start.elapsed();
713
714 let mut stats = SearchStats::new();
716 stats.gen_time = gen_time;
717 stats.search_time = search_time;
718 stats.lhs_count = all_lhs.len();
719 stats.rhs_count = db.rhs_count();
720 stats.lhs_tested = match_stats.lhs_tested;
721 stats.candidates_tested = match_stats.candidates_tested;
722 stats.newton_calls = match_stats.newton_calls;
723 stats.newton_success = match_stats.newton_success;
724 stats.pool_insertions = match_stats.pool_insertions;
725 stats.pool_rejections_error = match_stats.pool_rejections_error;
726 stats.pool_rejections_dedupe = match_stats.pool_rejections_dedupe;
727 stats.pool_evictions = match_stats.pool_evictions;
728 stats.pool_final_size = match_stats.pool_final_size;
729 stats.pool_best_error = match_stats.pool_best_error;
730 stats.early_exit = match_stats.early_exit;
731
732 (matches, stats)
733}
734
735#[allow(dead_code)]
767pub fn search_streaming(
768 target: f64,
769 gen_config: &crate::gen::GenConfig,
770 max_matches: usize,
771 stop_at_exact: bool,
772 stop_below: Option<f64>,
773) -> (Vec<Match>, SearchStats) {
774 let config = SearchConfig {
775 target,
776 max_matches,
777 stop_at_exact,
778 stop_below,
779 user_constants: gen_config.user_constants.clone(),
780 user_functions: gen_config.user_functions.clone(),
781 ..Default::default()
782 };
783
784 search_streaming_with_config(gen_config, &config)
785}
786
787pub fn search_streaming_with_config(
789 gen_config: &crate::gen::GenConfig,
790 search_config: &SearchConfig,
791) -> (Vec<Match>, SearchStats) {
792 use crate::gen::{generate_streaming_with_context, StreamingCallbacks};
793 use std::collections::HashMap;
794
795 let gen_start = SearchTimer::start();
796 let mut stats = SearchStats::new();
797 let context = SearchContext::new(search_config);
798
799 let initial_max_error = search_config.max_error.max(1e-12);
801
802 let mut pool = TopKPool::new_with_diagnostics(
804 search_config.max_matches,
805 initial_max_error,
806 search_config.show_db_adds,
807 search_config.ranking_mode,
808 );
809
810 let mut rhs_db = TieredExprDatabase::new();
812 let mut rhs_map: HashMap<i64, crate::expr::EvaluatedExpr> = HashMap::new();
813
814 let mut lhs_exprs: Vec<crate::expr::EvaluatedExpr> = Vec::new();
816
817 {
819 let mut callbacks = StreamingCallbacks {
820 on_rhs: &mut |expr| {
821 let key = crate::gen::quantize_value(expr.value);
824 rhs_map
825 .entry(key)
826 .and_modify(|existing| {
827 if expr.expr.complexity() < existing.expr.complexity() {
828 *existing = expr.clone();
829 }
830 })
831 .or_insert_with(|| expr.clone());
832 true
833 },
834 on_lhs: &mut |expr| {
835 lhs_exprs.push(expr.clone());
836 true
837 },
838 };
839
840 generate_streaming_with_context(
842 gen_config,
843 search_config.target,
844 &context.eval,
845 &mut callbacks,
846 );
847 }
848
849 for expr in rhs_map.into_values() {
851 rhs_db.insert(expr);
852 }
853 rhs_db.finalize();
854
855 stats.rhs_count = rhs_db.total_count();
856 stats.lhs_count = lhs_exprs.len();
857 stats.gen_time = gen_start.elapsed();
858
859 let search_start = SearchTimer::start();
861
862 lhs_exprs.sort_by_key(|e| e.expr.complexity());
864
865 let mut early_exit = false;
867
868 'outer: for lhs in &lhs_exprs {
869 if early_exit {
870 break;
871 }
872
873 if lhs.value.abs() < search_config.zero_value_threshold {
875 if search_config.show_pruned_range {
876 eprintln!(
877 " [pruned range] value={:.6e} reason=\"near-zero\" expr=\"{}\"",
878 lhs.value,
879 lhs.expr.to_infix()
880 );
881 }
882 continue;
883 }
884
885 if lhs.derivative.abs() < DEGENERATE_TEST_THRESHOLD {
887 let test_x = search_config.target + std::f64::consts::E;
888 if let Ok(test_result) =
892 crate::eval::evaluate_fast_with_context(&lhs.expr, test_x, &context.eval)
893 {
894 let value_unchanged =
895 (test_result.value - lhs.value).abs() < DEGENERATE_TEST_THRESHOLD;
896 let deriv_still_zero = test_result.derivative.abs() < DEGENERATE_TEST_THRESHOLD;
897 if deriv_still_zero || value_unchanged {
898 continue;
899 }
900 }
901
902 stats.lhs_tested += 1;
904 for rhs in rhs_db.iter_tiers_in_range(
905 lhs.value - DEGENERATE_RANGE_TOLERANCE,
906 lhs.value + DEGENERATE_RANGE_TOLERANCE,
907 ) {
908 if !search_config.rhs_symbol_allowed(&rhs.expr) {
909 continue;
910 }
911 stats.candidates_tested += 1;
912 if search_config.show_match_checks {
913 eprintln!(
914 " [match] checking lhs={:.6} rhs={:.6}",
915 lhs.value, rhs.value
916 );
917 }
918 let val_diff = (lhs.value - rhs.value).abs();
919 if val_diff < DEGENERATE_RANGE_TOLERANCE && pool.would_accept(0.0, true) {
920 let m = Match {
921 lhs: lhs.clone(),
922 rhs: rhs.clone(),
923 x_value: search_config.target,
924 error: 0.0,
925 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
926 };
927 pool.try_insert(m);
928 }
929 }
930 continue;
931 }
932
933 stats.lhs_tested += 1;
934
935 let search_radius = calculate_adaptive_search_radius(
937 lhs.derivative,
938 lhs.expr.complexity(),
939 pool.len(),
940 search_config.max_matches,
941 pool.best_error,
942 );
943 let low = lhs.value - search_radius;
944 let high = lhs.value + search_radius;
945
946 for rhs in rhs_db.iter_tiers_in_range(low, high) {
948 if !search_config.rhs_symbol_allowed(&rhs.expr) {
949 continue;
950 }
951 stats.candidates_tested += 1;
952 if search_config.show_match_checks {
953 eprintln!(
954 " [match] checking lhs={:.6} rhs={:.6}",
955 lhs.value, rhs.value
956 );
957 }
958
959 let val_diff = lhs.value - rhs.value;
961 let x_delta = -val_diff / lhs.derivative;
962 let coarse_error = x_delta.abs();
963
964 let is_potentially_exact = coarse_error < NEWTON_FINAL_TOLERANCE;
966 if !pool.would_accept_strict(coarse_error, is_potentially_exact) {
967 continue;
968 }
969
970 if !search_config.refine_with_newton {
971 let refined_x = search_config.target + x_delta;
972 let refined_error = x_delta;
973 let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
974
975 if pool.would_accept(refined_error.abs(), is_exact) {
976 let m = Match {
977 lhs: lhs.clone(),
978 rhs: rhs.clone(),
979 x_value: refined_x,
980 error: refined_error,
981 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
982 };
983
984 pool.try_insert(m);
985
986 if search_config.stop_at_exact && is_exact {
987 early_exit = true;
988 break 'outer;
989 }
990 if let Some(threshold) = search_config.stop_below {
991 if refined_error.abs() < threshold {
992 early_exit = true;
993 break 'outer;
994 }
995 }
996 }
997 continue;
998 }
999
1000 stats.newton_calls += 1;
1002 if let Some(refined_x) = newton_raphson_with_constants(
1003 &lhs.expr,
1004 rhs.value,
1005 search_config.target,
1006 search_config.newton_iterations,
1007 &context.eval,
1008 search_config.show_newton,
1009 search_config.derivative_margin,
1010 ) {
1011 stats.newton_success += 1;
1012 let refined_error = refined_x - search_config.target;
1013 let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
1014
1015 if pool.would_accept(refined_error.abs(), is_exact) {
1017 let m = Match {
1018 lhs: lhs.clone(),
1019 rhs: rhs.clone(),
1020 x_value: refined_x,
1021 error: refined_error,
1022 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1023 };
1024
1025 pool.try_insert(m);
1027
1028 if search_config.stop_at_exact && is_exact {
1030 early_exit = true;
1031 break 'outer;
1032 }
1033 if let Some(threshold) = search_config.stop_below {
1034 if refined_error.abs() < threshold {
1035 early_exit = true;
1036 break 'outer;
1037 }
1038 }
1039 }
1040 }
1041 }
1042 }
1043
1044 stats.pool_insertions = pool.stats.insertions;
1046 stats.pool_rejections_error = pool.stats.rejections_error;
1047 stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
1048 stats.pool_evictions = pool.stats.evictions;
1049 stats.pool_final_size = pool.len();
1050 stats.pool_best_error = pool.best_error;
1051 stats.search_time = search_start.elapsed();
1052 stats.early_exit = early_exit;
1053
1054 (pool.into_sorted(), stats)
1055}
1056
1057pub fn search_one_sided_with_stats_and_config(
1059 gen_config: &crate::gen::GenConfig,
1060 config: &SearchConfig,
1061) -> (Vec<Match>, SearchStats) {
1062 use crate::eval::evaluate_with_context;
1063 use crate::expr::Expression;
1064 use crate::gen::generate_all_with_context;
1065 use crate::symbol::Symbol;
1066
1067 let gen_start = SearchTimer::start();
1068 let context = SearchContext::new(config);
1069
1070 let mut rhs_only = gen_config.clone();
1071 rhs_only.generate_lhs = false;
1072 rhs_only.generate_rhs = true;
1073
1074 let generated = generate_all_with_context(&rhs_only, config.target, &context.eval);
1075 let gen_time = gen_start.elapsed();
1076
1077 let search_start = SearchTimer::start();
1078 let initial_max_error = config.max_error.max(1e-12);
1079 let mut pool = TopKPool::new_with_diagnostics(
1080 config.max_matches,
1081 initial_max_error,
1082 config.show_db_adds,
1083 config.ranking_mode,
1084 );
1085 let mut stats = SearchStats::new();
1086 let mut early_exit = false;
1087
1088 let mut lhs_expr = Expression::new();
1089 lhs_expr.push_with_table(Symbol::X, &gen_config.symbol_table);
1090 let lhs_eval = evaluate_with_context(&lhs_expr, config.target, &context.eval);
1091 let lhs_eval = match lhs_eval {
1092 Ok(v) => v,
1093 Err(_) => {
1094 stats.gen_time = gen_time;
1095 stats.search_time = search_start.elapsed();
1096 return (Vec::new(), stats);
1097 }
1098 };
1099 let lhs = EvaluatedExpr::new(
1100 lhs_expr,
1101 lhs_eval.value,
1102 lhs_eval.derivative,
1103 lhs_eval.num_type,
1104 );
1105
1106 stats.lhs_count = 1;
1107 stats.rhs_count = generated.rhs.len();
1108 stats.lhs_tested = 1;
1109
1110 for rhs in generated.rhs {
1111 if !config.rhs_symbol_allowed(&rhs.expr) {
1112 continue;
1113 }
1114 stats.candidates_tested += 1;
1115 if config.show_match_checks {
1116 eprintln!(
1117 " [match] checking lhs={:.6} rhs={:.6}",
1118 lhs.value, rhs.value
1119 );
1120 }
1121
1122 let error = rhs.value - config.target;
1123 let is_exact = error.abs() < EXACT_MATCH_TOLERANCE;
1124 if !pool.would_accept(error.abs(), is_exact) {
1125 continue;
1126 }
1127
1128 let m = Match {
1129 lhs: lhs.clone(),
1130 rhs: rhs.clone(),
1131 x_value: rhs.value,
1132 error,
1133 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1134 };
1135
1136 pool.try_insert(m);
1137
1138 if config.stop_at_exact && is_exact {
1139 early_exit = true;
1140 break;
1141 }
1142 if let Some(threshold) = config.stop_below {
1143 if error.abs() < threshold {
1144 early_exit = true;
1145 break;
1146 }
1147 }
1148 }
1149
1150 stats.pool_insertions = pool.stats.insertions;
1151 stats.pool_rejections_error = pool.stats.rejections_error;
1152 stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
1153 stats.pool_evictions = pool.stats.evictions;
1154 stats.pool_final_size = pool.len();
1155 stats.pool_best_error = pool.best_error;
1156 stats.gen_time = gen_time;
1157 stats.search_time = search_start.elapsed();
1158 stats.early_exit = early_exit;
1159
1160 (pool.into_sorted(), stats)
1161}
1162
1163#[cfg(feature = "parallel")]
1168#[allow(dead_code)]
1169pub fn search_parallel(
1170 target: f64,
1171 gen_config: &crate::gen::GenConfig,
1172 max_matches: usize,
1173) -> Vec<Match> {
1174 let (matches, _stats) = search_parallel_with_stats(target, gen_config, max_matches);
1175 matches
1176}
1177
1178#[cfg(feature = "parallel")]
1183#[allow(dead_code)]
1184pub fn search_parallel_with_stats(
1185 target: f64,
1186 gen_config: &crate::gen::GenConfig,
1187 max_matches: usize,
1188) -> (Vec<Match>, SearchStats) {
1189 search_parallel_with_stats_and_options(target, gen_config, max_matches, false, None)
1190}
1191
1192#[cfg(feature = "parallel")]
1194pub fn search_parallel_with_stats_and_options(
1195 target: f64,
1196 gen_config: &crate::gen::GenConfig,
1197 max_matches: usize,
1198 stop_at_exact: bool,
1199 stop_below: Option<f64>,
1200) -> (Vec<Match>, SearchStats) {
1201 let config = SearchConfig {
1202 target,
1203 max_matches,
1204 stop_at_exact,
1205 stop_below,
1206 user_constants: gen_config.user_constants.clone(),
1207 user_functions: gen_config.user_functions.clone(),
1208 ..Default::default()
1209 };
1210
1211 search_parallel_with_stats_and_config(gen_config, &config)
1212}
1213
1214#[cfg(feature = "parallel")]
1220pub fn search_parallel_with_stats_and_config(
1221 gen_config: &crate::gen::GenConfig,
1222 config: &SearchConfig,
1223) -> (Vec<Match>, SearchStats) {
1224 use crate::gen::generate_all_with_limit_and_context;
1225
1226 const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
1227 let context = SearchContext::new(config);
1228
1229 let gen_start = SearchTimer::start();
1230
1231 if let Some(generated) = generate_all_with_limit_and_context(
1233 gen_config,
1234 config.target,
1235 &context.eval,
1236 MAX_EXPRESSIONS_BEFORE_STREAMING,
1237 ) {
1238 let gen_time = gen_start.elapsed();
1239
1240 let mut db = ExprDatabase::new();
1242 db.insert_rhs(generated.rhs);
1243
1244 let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
1246
1247 stats.gen_time = gen_time;
1249 stats.lhs_count = generated.lhs.len();
1250 stats.rhs_count = db.rhs_count();
1251
1252 (matches, stats)
1253 } else {
1254 search_streaming_with_config(gen_config, config)
1256 }
1257}