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
24pub(crate) fn should_skip_degenerate_lhs(
26 lhs: &EvaluatedExpr,
27 target: f64,
28 eval: &EvalContext<'_>,
29) -> bool {
30 if lhs.derivative.abs() >= DEGENERATE_TEST_THRESHOLD {
31 return false;
32 }
33 let test_x = target + std::f64::consts::E;
34 match crate::eval::evaluate_fast_with_context(&lhs.expr, test_x, eval) {
35 Ok(test_result) => {
36 let value_unchanged = (test_result.value - lhs.value).abs() < DEGENERATE_TEST_THRESHOLD;
37 let deriv_still_zero = test_result.derivative.abs() < DEGENERATE_TEST_THRESHOLD;
38 deriv_still_zero || value_unchanged
39 }
40 Err(_) => true,
41 }
42}
43#[cfg(test)]
44use newton::newton_raphson;
45use newton::newton_raphson_with_constants;
46
47#[derive(Clone, Copy, Debug)]
48struct SearchTimer {
49 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
50 start_ms: f64,
51 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
52 start: std::time::Instant,
53}
54
55impl SearchTimer {
56 #[inline]
57 fn start() -> Self {
58 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
59 {
60 Self {
61 start_ms: js_sys::Date::now(),
62 }
63 }
64
65 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
66 {
67 Self {
68 start: std::time::Instant::now(),
69 }
70 }
71 }
72
73 #[inline]
74 fn elapsed(self) -> Duration {
75 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
76 {
77 let elapsed_ms = (js_sys::Date::now() - self.start_ms).max(0.0);
78 Duration::from_secs_f64(elapsed_ms / 1000.0)
79 }
80
81 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
82 {
83 self.start.elapsed()
84 }
85 }
86}
87
88#[derive(Clone, Debug, Default)]
90pub struct SearchStats {
91 pub gen_time: Duration,
93 pub search_time: Duration,
95 pub lhs_count: usize,
97 pub rhs_count: usize,
99 pub lhs_tested: usize,
101 pub candidates_tested: usize,
103 pub candidate_window_total: usize,
105 pub candidate_window_max: usize,
107 pub candidate_window_max_lhs_postfix: Option<String>,
109 pub candidate_window_max_lhs_value: Option<f64>,
111 pub candidate_window_max_lhs_derivative: Option<f64>,
113 pub candidate_window_max_lhs_complexity: Option<u32>,
115 pub strict_gate_rejections: usize,
117 pub newton_calls: usize,
119 pub newton_success: usize,
121 pub pool_insertions: usize,
123 pub pool_rejections_error: usize,
125 pub pool_rejections_dedupe: usize,
127 pub pool_evictions: usize,
129 pub pool_final_size: usize,
131 pub pool_best_error: f64,
133 pub early_exit: bool,
135}
136
137impl SearchStats {
138 pub fn new() -> Self {
139 Self::default()
140 }
141
142 #[inline]
143 pub fn record_candidate_window(&mut self, lhs: &EvaluatedExpr, width: usize) {
144 self.candidate_window_total = self.candidate_window_total.saturating_add(width);
145 if width > self.candidate_window_max {
146 self.candidate_window_max = width;
147 self.candidate_window_max_lhs_postfix = Some(lhs.expr.to_postfix());
148 self.candidate_window_max_lhs_value = Some(lhs.value);
149 self.candidate_window_max_lhs_derivative = Some(lhs.derivative);
150 self.candidate_window_max_lhs_complexity = Some(lhs.expr.complexity());
151 }
152 }
153
154 #[cfg(feature = "parallel")]
160 pub fn merge_search_counters(&mut self, other: &SearchStats) {
161 self.lhs_tested = self.lhs_tested.saturating_add(other.lhs_tested);
162 self.candidates_tested = self
163 .candidates_tested
164 .saturating_add(other.candidates_tested);
165 self.candidate_window_total = self
166 .candidate_window_total
167 .saturating_add(other.candidate_window_total);
168 if other.candidate_window_max > self.candidate_window_max {
169 self.candidate_window_max = other.candidate_window_max;
170 self.candidate_window_max_lhs_postfix = other.candidate_window_max_lhs_postfix.clone();
171 self.candidate_window_max_lhs_value = other.candidate_window_max_lhs_value;
172 self.candidate_window_max_lhs_derivative = other.candidate_window_max_lhs_derivative;
173 self.candidate_window_max_lhs_complexity = other.candidate_window_max_lhs_complexity;
174 }
175 self.strict_gate_rejections = self
176 .strict_gate_rejections
177 .saturating_add(other.strict_gate_rejections);
178 self.newton_calls = self.newton_calls.saturating_add(other.newton_calls);
179 self.newton_success = self.newton_success.saturating_add(other.newton_success);
180 self.early_exit |= other.early_exit;
181 }
182
183 #[inline]
184 pub fn candidate_window_avg(&self) -> f64 {
185 if self.lhs_tested == 0 {
186 0.0
187 } else {
188 self.candidate_window_total as f64 / self.lhs_tested as f64
189 }
190 }
191
192 #[inline]
193 pub fn candidates_per_pool_insertion(&self) -> f64 {
194 if self.pool_insertions == 0 {
195 0.0
196 } else {
197 self.candidates_tested as f64 / self.pool_insertions as f64
198 }
199 }
200
201 #[inline]
202 pub fn newton_success_rate(&self) -> f64 {
203 if self.newton_calls == 0 {
204 0.0
205 } else {
206 self.newton_success as f64 / self.newton_calls as f64
207 }
208 }
209
210 #[inline]
211 pub fn pool_attempts(&self) -> usize {
212 self.pool_insertions
213 .saturating_add(self.pool_rejections_error)
214 .saturating_add(self.pool_rejections_dedupe)
215 }
216
217 #[inline]
218 pub fn pool_acceptance_rate(&self) -> f64 {
219 let attempts = self.pool_attempts();
220 if attempts == 0 {
221 0.0
222 } else {
223 self.pool_insertions as f64 / attempts as f64
224 }
225 }
226
227 pub fn print(&self) {
229 println!();
230 println!(" === Search Statistics ===");
231 println!();
232 println!(" Generation:");
233 println!(
234 " Time: {:>10.3}ms",
235 self.gen_time.as_secs_f64() * 1000.0
236 );
237 println!(" LHS expressions: {:>10}", self.lhs_count);
238 println!(" RHS expressions: {:>10}", self.rhs_count);
239 println!();
240 println!(" Search:");
241 println!(
242 " Time: {:>10.3}ms",
243 self.search_time.as_secs_f64() * 1000.0
244 );
245 println!(" LHS tested: {:>10}", self.lhs_tested);
246 println!(" Candidates: {:>10}", self.candidates_tested);
247 println!(" Window avg: {:>10.2}", self.candidate_window_avg());
248 println!(" Window max: {:>10}", self.candidate_window_max);
249 if let Some(lhs) = &self.candidate_window_max_lhs_postfix {
250 println!(" Window max lhs: {:>10}", lhs);
251 }
252 println!(" Gate rejects: {:>10}", self.strict_gate_rejections);
253 println!(
254 " Cand/insert: {:>10.2}",
255 self.candidates_per_pool_insertion()
256 );
257 println!(" Newton calls: {:>10}", self.newton_calls);
258 println!(
259 " Newton success: {:>10} ({:.1}%)",
260 self.newton_success,
261 100.0 * self.newton_success_rate()
262 );
263 if self.early_exit {
264 println!(" Early exit: yes");
265 }
266 println!();
267 println!(" Pool:");
268 println!(" Insertions: {:>10}", self.pool_insertions);
269 println!(" Rejected (err): {:>10}", self.pool_rejections_error);
270 println!(" Rejected (dup): {:>10}", self.pool_rejections_dedupe);
271 println!(" Evictions: {:>10}", self.pool_evictions);
272 println!(
273 " Acceptance: {:>9.1}%",
274 100.0 * self.pool_acceptance_rate()
275 );
276 println!(" Final size: {:>10}", self.pool_final_size);
277 println!(" Best error: {:>14.2e}", self.pool_best_error);
278 }
279}
280
281#[inline]
311pub fn level_to_complexity(level: u32) -> (u32, u32) {
312 const BASE_LHS: u32 = 10;
313 const BASE_RHS: u32 = 12;
314 const LEVEL_MULTIPLIER: u32 = 4;
315
316 let level_factor = LEVEL_MULTIPLIER.saturating_mul(level);
319 (
320 BASE_LHS.saturating_add(level_factor),
321 BASE_RHS.saturating_add(level_factor),
322 )
323}
324
325#[derive(Clone, Debug)]
327pub struct Match {
328 pub lhs: EvaluatedExpr,
330 pub rhs: EvaluatedExpr,
332 pub x_value: f64,
334 pub error: f64,
336 pub complexity: u32,
338}
339
340impl Match {
341 #[cfg(test)]
343 pub fn display(&self, _target: f64) -> String {
344 let lhs_str = self.lhs.expr.to_infix();
345 let rhs_str = self.rhs.expr.to_infix();
346
347 let error_str = if self.error.abs() < EXACT_MATCH_TOLERANCE {
348 "('exact' match)".to_string()
349 } else {
350 let sign = if self.error >= 0.0 { "+" } else { "-" };
351 format!("for x = T {} {:.6e}", sign, self.error.abs())
352 };
353
354 format!(
355 "{:>24} = {:<24} {} {{{}}}",
356 lhs_str, rhs_str, error_str, self.complexity
357 )
358 }
359}
360
361#[derive(Clone, Debug)]
393pub struct SearchConfig {
394 pub target: f64,
401
402 pub max_matches: usize,
409
410 pub max_error: f64,
417
418 pub stop_at_exact: bool,
425
426 pub stop_below: Option<f64>,
433
434 pub zero_value_threshold: f64,
442
443 pub newton_iterations: usize,
450
451 pub user_constants: Vec<UserConstant>,
458
459 pub user_functions: Vec<crate::udf::UserFunction>,
466
467 pub trig_argument_scale: f64,
472
473 pub refine_with_newton: bool,
480
481 pub rhs_allowed_symbols: Option<HashSet<u8>>,
489
490 pub rhs_excluded_symbols: Option<HashSet<u8>>,
498
499 pub show_newton: bool,
506
507 pub show_match_checks: bool,
514
515 #[allow(dead_code)]
522 pub show_pruned_arith: bool,
523
524 pub show_pruned_range: bool,
531
532 pub show_db_adds: bool,
539
540 #[allow(dead_code)]
548 pub match_all_digits: bool,
549
550 pub derivative_margin: f64,
558
559 pub ranking_mode: RankingMode,
567}
568
569impl Default for SearchConfig {
570 fn default() -> Self {
571 Self {
572 target: 0.0,
573 max_matches: 100,
574 max_error: 1.0,
575 stop_at_exact: false,
576 stop_below: None,
577 zero_value_threshold: 1e-4,
578 newton_iterations: 15,
579 user_constants: Vec::new(),
580 user_functions: Vec::new(),
581 trig_argument_scale: crate::eval::DEFAULT_TRIG_ARGUMENT_SCALE,
582 refine_with_newton: true,
583 rhs_allowed_symbols: None,
584 rhs_excluded_symbols: None,
585 show_newton: false,
586 show_match_checks: false,
587 show_pruned_arith: false,
588 show_pruned_range: false,
589 show_db_adds: false,
590 match_all_digits: false,
591 derivative_margin: DEGENERATE_DERIVATIVE,
592 ranking_mode: RankingMode::Complexity,
593 }
594 }
595}
596
597impl SearchConfig {
598 pub fn context(&self) -> SearchContext<'_> {
600 SearchContext::new(self)
601 }
602
603 #[inline]
604 fn rhs_symbol_allowed(&self, rhs: &crate::expr::Expression) -> bool {
605 let symbols = rhs.symbols();
606
607 if let Some(allowed) = &self.rhs_allowed_symbols {
608 if symbols.iter().any(|s| !allowed.contains(&(*s as u8))) {
609 return false;
610 }
611 }
612
613 if let Some(excluded) = &self.rhs_excluded_symbols {
614 if symbols.iter().any(|s| excluded.contains(&(*s as u8))) {
615 return false;
616 }
617 }
618
619 true
620 }
621}
622
623#[derive(Clone, Copy, Debug)]
625pub struct SearchContext<'a> {
626 pub config: &'a SearchConfig,
628 pub eval: EvalContext<'a>,
630}
631
632impl<'a> SearchContext<'a> {
633 pub fn new(config: &'a SearchConfig) -> Self {
634 Self {
635 config,
636 eval: EvalContext::from_slices(&config.user_constants, &config.user_functions)
637 .with_trig_argument_scale(config.trig_argument_scale),
638 }
639 }
640}
641
642#[allow(dead_code)]
647pub fn search(target: f64, gen_config: &crate::gen::GenConfig, max_matches: usize) -> Vec<Match> {
648 let (matches, _stats) = search_with_stats(target, gen_config, max_matches);
649 matches
650}
651
652#[allow(dead_code)]
657pub fn search_with_stats(
658 target: f64,
659 gen_config: &crate::gen::GenConfig,
660 max_matches: usize,
661) -> (Vec<Match>, SearchStats) {
662 search_with_stats_and_options(target, gen_config, max_matches, false, None)
663}
664
665pub fn search_with_stats_and_options(
667 target: f64,
668 gen_config: &crate::gen::GenConfig,
669 max_matches: usize,
670 stop_at_exact: bool,
671 stop_below: Option<f64>,
672) -> (Vec<Match>, SearchStats) {
673 if !target.is_finite() {
674 return (Vec::new(), SearchStats::default());
675 }
676 let config = SearchConfig {
677 target,
678 max_matches,
679 stop_at_exact,
680 stop_below,
681 user_constants: gen_config.user_constants.clone(),
682 user_functions: gen_config.user_functions.clone(),
683 ..Default::default()
684 };
685
686 search_with_stats_and_config(gen_config, &config)
687}
688
689pub fn search_with_stats_and_config(
695 gen_config: &crate::gen::GenConfig,
696 config: &SearchConfig,
697) -> (Vec<Match>, SearchStats) {
698 if !config.target.is_finite() {
699 return (Vec::new(), SearchStats::default());
700 }
701
702 use crate::gen::generate_all_with_limit_and_context;
703
704 const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
705 let context = SearchContext::new(config);
706
707 let gen_start = SearchTimer::start();
708
709 if let Some(generated) = generate_all_with_limit_and_context(
711 gen_config,
712 config.target,
713 &context.eval,
714 MAX_EXPRESSIONS_BEFORE_STREAMING,
715 ) {
716 let gen_time = gen_start.elapsed();
717
718 let mut db = ExprDatabase::new();
720 db.insert_rhs(generated.rhs);
721
722 let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
724
725 stats.gen_time = gen_time;
727 stats.lhs_count = generated.lhs.len();
728 stats.rhs_count = db.rhs_count();
729
730 (matches, stats)
731 } else {
732 search_streaming_with_config(gen_config, config)
734 }
735}
736
737pub fn search_adaptive(
771 base_config: &crate::gen::GenConfig,
772 search_config: &SearchConfig,
773 level: u32,
774) -> (Vec<Match>, SearchStats) {
775 use crate::expr::EvaluatedExpr;
776 use crate::gen::{quantize_value, LhsKey};
777 use std::collections::HashMap;
778
779 if !search_config.target.is_finite() {
780 return (Vec::new(), SearchStats::default());
781 }
782
783 let gen_start = SearchTimer::start();
784 let context = SearchContext::new(search_config);
785 let mut lhs_map: HashMap<LhsKey, EvaluatedExpr> = HashMap::new();
789 let mut rhs_map: HashMap<i64, EvaluatedExpr> = HashMap::new();
790
791 let target_count = adaptive_target_count(level);
794
795 let mut lhs_c: u32 = 1;
802 let mut rhs_c: u32 = 1;
803 const MAX_ADAPTIVE_COMPLEXITY: u32 = 60;
804
805 loop {
806 let mut config = base_config.clone();
807 config.max_lhs_complexity = lhs_c;
811 config.max_rhs_complexity = rhs_c;
812
813 let generated = {
814 #[cfg(feature = "parallel")]
815 {
816 crate::gen::generate_all_parallel_with_context(
817 &config,
818 search_config.target,
819 &context.eval,
820 )
821 }
822 #[cfg(not(feature = "parallel"))]
823 {
824 crate::gen::generate_all_with_context(&config, search_config.target, &context.eval)
825 }
826 };
827
828 for expr in generated.lhs {
829 let key = (quantize_value(expr.value), quantize_value(expr.derivative));
830 lhs_map
831 .entry(key)
832 .and_modify(|existing| {
833 if expr.expr.complexity() < existing.expr.complexity() {
834 *existing = expr.clone();
835 }
836 })
837 .or_insert(expr);
838 }
839 for expr in generated.rhs {
840 let key = quantize_value(expr.value);
841 rhs_map
842 .entry(key)
843 .and_modify(|existing| {
844 if expr.expr.complexity() < existing.expr.complexity() {
845 *existing = expr.clone();
846 }
847 })
848 .or_insert(expr);
849 }
850
851 if lhs_map.len() + rhs_map.len() >= target_count {
852 break;
853 }
854 if lhs_c >= MAX_ADAPTIVE_COMPLEXITY && rhs_c >= MAX_ADAPTIVE_COMPLEXITY {
855 break;
856 }
857 if lhs_map.len() <= rhs_map.len() && lhs_c < MAX_ADAPTIVE_COMPLEXITY {
859 lhs_c += 1;
860 } else if rhs_c < MAX_ADAPTIVE_COMPLEXITY {
861 rhs_c += 1;
862 } else {
863 break;
864 }
865 }
866
867 let all_lhs: Vec<EvaluatedExpr> = lhs_map.into_values().collect();
868 let all_rhs: Vec<EvaluatedExpr> = rhs_map.into_values().collect();
869
870 let gen_time = gen_start.elapsed();
871
872 let mut db = ExprDatabase::new();
874 db.insert_rhs(all_rhs);
875
876 let search_start = SearchTimer::start();
877 let (matches, mut stats) = db.find_matches_with_stats_and_context(&all_lhs, &context);
878 let search_time = search_start.elapsed();
879
880 stats.gen_time = gen_time;
882 stats.search_time = search_time;
883 stats.lhs_count = all_lhs.len();
884 stats.rhs_count = db.rhs_count();
885
886 (matches, stats)
887}
888
889#[inline]
890fn adaptive_target_count(level: u32) -> usize {
891 2000_usize.saturating_mul(4_usize.saturating_pow(level.saturating_add(2)))
892}
893
894#[allow(dead_code)]
926pub fn search_streaming(
927 target: f64,
928 gen_config: &crate::gen::GenConfig,
929 max_matches: usize,
930 stop_at_exact: bool,
931 stop_below: Option<f64>,
932) -> (Vec<Match>, SearchStats) {
933 let config = SearchConfig {
934 target,
935 max_matches,
936 stop_at_exact,
937 stop_below,
938 user_constants: gen_config.user_constants.clone(),
939 user_functions: gen_config.user_functions.clone(),
940 ..Default::default()
941 };
942
943 search_streaming_with_config(gen_config, &config)
944}
945
946fn stop_condition_met(m: &Match, search_config: &SearchConfig) -> bool {
947 (search_config.stop_at_exact && m.error.abs() < EXACT_MATCH_TOLERANCE)
948 || search_config
949 .stop_below
950 .is_some_and(|threshold| m.error.abs() < threshold)
951}
952
953fn prefer_streaming_stop_match(candidate: &Match, current: &Match) -> bool {
954 use std::cmp::Ordering;
955
956 candidate
957 .lhs
958 .expr
959 .complexity()
960 .cmp(¤t.lhs.expr.complexity())
961 .then_with(|| {
962 candidate
963 .rhs
964 .expr
965 .complexity()
966 .cmp(¤t.rhs.expr.complexity())
967 })
968 .then_with(|| {
969 candidate
970 .error
971 .abs()
972 .partial_cmp(¤t.error.abs())
973 .unwrap_or(Ordering::Equal)
974 })
975 .then_with(|| {
976 candidate
977 .lhs
978 .expr
979 .to_postfix()
980 .cmp(¤t.lhs.expr.to_postfix())
981 })
982 .then_with(|| {
983 candidate
984 .rhs
985 .expr
986 .to_postfix()
987 .cmp(¤t.rhs.expr.to_postfix())
988 })
989 .is_lt()
990}
991
992fn merge_priority_match(
993 mut matches: Vec<Match>,
994 priority: Match,
995 max_matches: usize,
996) -> Vec<Match> {
997 if let Some(idx) = matches
998 .iter()
999 .position(|m| m.lhs.expr == priority.lhs.expr && m.rhs.expr == priority.rhs.expr)
1000 {
1001 matches.remove(idx);
1002 }
1003 matches.insert(0, priority);
1004 matches.truncate(max_matches);
1005 matches
1006}
1007
1008pub fn search_streaming_with_config(
1014 gen_config: &crate::gen::GenConfig,
1015 search_config: &SearchConfig,
1016) -> (Vec<Match>, SearchStats) {
1017 use crate::gen::{generate_streaming_with_context, StreamingCallbacks};
1018 use std::collections::HashMap;
1019
1020 if !search_config.target.is_finite() {
1021 return (Vec::new(), SearchStats::default());
1022 }
1023
1024 let gen_start = SearchTimer::start();
1025 let mut stats = SearchStats::new();
1026 let context = SearchContext::new(search_config);
1027
1028 let initial_max_error = search_config.max_error.max(1e-12);
1029 let mut pool = TopKPool::new_with_diagnostics(
1030 search_config.max_matches,
1031 initial_max_error,
1032 search_config.show_db_adds,
1033 search_config.ranking_mode,
1034 );
1035
1036 let mut rhs_db = TieredExprDatabase::new();
1042 let mut rhs_map: HashMap<i64, crate::expr::EvaluatedExpr> = HashMap::new();
1043
1044 {
1045 let mut callbacks = StreamingCallbacks {
1046 on_rhs: &mut |expr| {
1047 let key = crate::gen::quantize_value(expr.value);
1048 rhs_map
1049 .entry(key)
1050 .and_modify(|existing| {
1051 if expr.expr.complexity() < existing.expr.complexity() {
1052 *existing = expr.clone();
1053 }
1054 })
1055 .or_insert_with(|| expr.clone());
1056 true
1057 },
1058 on_lhs: &mut |_| true,
1059 };
1060 generate_streaming_with_context(
1061 gen_config,
1062 search_config.target,
1063 &context.eval,
1064 &mut callbacks,
1065 );
1066 }
1067
1068 for expr in rhs_map.into_values() {
1069 rhs_db.insert(expr);
1070 }
1071 rhs_db.finalize();
1072 stats.rhs_count = rhs_db.total_count();
1073 stats.gen_time = gen_start.elapsed();
1074
1075 let mut lhs_gen_config = gen_config.clone();
1086 lhs_gen_config.generate_rhs = false;
1087
1088 let search_start = SearchTimer::start();
1089 let mut best_stop_match: Option<Match> = None;
1090
1091 {
1092 let mut callbacks = StreamingCallbacks {
1093 on_rhs: &mut |_| true,
1094 on_lhs: &mut |lhs| {
1095 stats.lhs_count += 1;
1096
1097 if lhs.value.abs() < search_config.zero_value_threshold {
1099 if search_config.show_pruned_range {
1100 eprintln!(
1101 " [pruned range] value={:.6e} reason=\"near-zero\" expr=\"{}\"",
1102 lhs.value,
1103 lhs.expr.to_infix()
1104 );
1105 }
1106 return true;
1107 }
1108
1109 if should_skip_degenerate_lhs(lhs, search_config.target, &context.eval) {
1111 return true;
1112 }
1113 if lhs.derivative.abs() < DEGENERATE_TEST_THRESHOLD {
1114 stats.lhs_tested += 1;
1115 stats.record_candidate_window(
1116 lhs,
1117 rhs_db.count_in_range(
1118 lhs.value - DEGENERATE_RANGE_TOLERANCE,
1119 lhs.value + DEGENERATE_RANGE_TOLERANCE,
1120 ),
1121 );
1122 for rhs in rhs_db.iter_tiers_in_range(
1123 lhs.value - DEGENERATE_RANGE_TOLERANCE,
1124 lhs.value + DEGENERATE_RANGE_TOLERANCE,
1125 ) {
1126 if !search_config.rhs_symbol_allowed(&rhs.expr) {
1127 continue;
1128 }
1129 stats.candidates_tested += 1;
1130 if search_config.show_match_checks {
1131 eprintln!(
1132 " [match] checking lhs={:.6} rhs={:.6}",
1133 lhs.value, rhs.value
1134 );
1135 }
1136 let val_diff = (lhs.value - rhs.value).abs();
1137 if val_diff < DEGENERATE_RANGE_TOLERANCE && pool.would_accept(0.0, true) {
1138 let m = Match {
1139 lhs: lhs.clone(),
1140 rhs: rhs.clone(),
1141 x_value: search_config.target,
1142 error: 0.0,
1143 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1144 };
1145 if stop_condition_met(&m, search_config)
1146 && best_stop_match
1147 .as_ref()
1148 .is_none_or(|current| prefer_streaming_stop_match(&m, current))
1149 {
1150 best_stop_match = Some(m.clone());
1151 }
1152 pool.try_insert(m);
1153 }
1154 }
1155 return true;
1156 }
1157
1158 stats.lhs_tested += 1;
1159
1160 let search_radius = calculate_adaptive_search_radius(
1161 lhs.derivative,
1162 lhs.expr.complexity(),
1163 pool.len(),
1164 search_config.max_matches,
1165 pool.best_error,
1166 pool.accept_error,
1167 );
1168 let low = lhs.value - search_radius;
1169 let high = lhs.value + search_radius;
1170 stats.record_candidate_window(lhs, rhs_db.count_in_range(low, high));
1171
1172 for rhs in rhs_db.iter_tiers_in_range(low, high) {
1173 if !search_config.rhs_symbol_allowed(&rhs.expr) {
1174 continue;
1175 }
1176 stats.candidates_tested += 1;
1177 if search_config.show_match_checks {
1178 eprintln!(
1179 " [match] checking lhs={:.6} rhs={:.6}",
1180 lhs.value, rhs.value
1181 );
1182 }
1183
1184 let val_diff = lhs.value - rhs.value;
1185 let x_delta = -val_diff / lhs.derivative;
1186 let coarse_error = x_delta.abs();
1187
1188 let is_potentially_exact = coarse_error < NEWTON_FINAL_TOLERANCE;
1189 if !pool.would_accept_strict(coarse_error, is_potentially_exact) {
1190 stats.strict_gate_rejections += 1;
1191 continue;
1192 }
1193
1194 if !search_config.refine_with_newton {
1195 let refined_x = search_config.target + x_delta;
1196 let refined_error = x_delta;
1197 let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
1198
1199 if pool.would_accept(refined_error.abs(), is_exact) {
1200 let m = Match {
1201 lhs: lhs.clone(),
1202 rhs: rhs.clone(),
1203 x_value: refined_x,
1204 error: refined_error,
1205 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1206 };
1207 if stop_condition_met(&m, search_config)
1208 && best_stop_match
1209 .as_ref()
1210 .is_none_or(|current| prefer_streaming_stop_match(&m, current))
1211 {
1212 best_stop_match = Some(m.clone());
1213 }
1214 pool.try_insert(m);
1215 }
1216 continue;
1217 }
1218
1219 stats.newton_calls += 1;
1220 let initial_guess = search_config.target + x_delta;
1221 if let Some(refined_x) = newton_raphson_with_constants(
1222 &lhs.expr,
1223 rhs.value,
1224 initial_guess,
1225 search_config.newton_iterations,
1226 &context.eval,
1227 search_config.show_newton,
1228 search_config.derivative_margin,
1229 ) {
1230 stats.newton_success += 1;
1231 let refined_error = refined_x - search_config.target;
1232 let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
1233
1234 if pool.would_accept(refined_error.abs(), is_exact) {
1235 let m = Match {
1236 lhs: lhs.clone(),
1237 rhs: rhs.clone(),
1238 x_value: refined_x,
1239 error: refined_error,
1240 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1241 };
1242 if stop_condition_met(&m, search_config)
1243 && best_stop_match
1244 .as_ref()
1245 .is_none_or(|current| prefer_streaming_stop_match(&m, current))
1246 {
1247 best_stop_match = Some(m.clone());
1248 }
1249 pool.try_insert(m);
1250 }
1251 }
1252 }
1253 true
1254 },
1255 };
1256 generate_streaming_with_context(
1257 &lhs_gen_config,
1258 search_config.target,
1259 &context.eval,
1260 &mut callbacks,
1261 );
1262 }
1263
1264 stats.pool_insertions = pool.stats.insertions;
1265 stats.pool_rejections_error = pool.stats.rejections_error;
1266 stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
1267 stats.pool_evictions = pool.stats.evictions;
1268 stats.pool_final_size = pool.len();
1269 stats.pool_best_error = pool.best_error;
1270 stats.search_time = search_start.elapsed();
1271 stats.early_exit = false;
1272
1273 let matches = pool.into_sorted();
1274 let matches = if let Some(priority_match) = best_stop_match {
1275 merge_priority_match(matches, priority_match, search_config.max_matches)
1276 } else {
1277 matches
1278 };
1279
1280 (matches, stats)
1281}
1282
1283pub fn search_one_sided_with_stats_and_config(
1285 gen_config: &crate::gen::GenConfig,
1286 config: &SearchConfig,
1287) -> (Vec<Match>, SearchStats) {
1288 use crate::eval::evaluate_with_context;
1289 use crate::expr::Expression;
1290 use crate::gen::generate_all_with_context;
1291 use crate::symbol::Symbol;
1292
1293 let gen_start = SearchTimer::start();
1294 let context = SearchContext::new(config);
1295
1296 let mut rhs_only = gen_config.clone();
1297 rhs_only.generate_lhs = false;
1298 rhs_only.generate_rhs = true;
1299
1300 let generated = generate_all_with_context(&rhs_only, config.target, &context.eval);
1301 let gen_time = gen_start.elapsed();
1302
1303 let search_start = SearchTimer::start();
1304 let initial_max_error = config.max_error.max(1e-12);
1305 let mut pool = TopKPool::new_with_diagnostics(
1306 config.max_matches,
1307 initial_max_error,
1308 config.show_db_adds,
1309 config.ranking_mode,
1310 );
1311 let mut stats = SearchStats::new();
1312 let mut early_exit = false;
1313
1314 let mut lhs_expr = Expression::new();
1315 lhs_expr.push_with_table(Symbol::X, &gen_config.symbol_table);
1316 let lhs_eval = evaluate_with_context(&lhs_expr, config.target, &context.eval);
1317 let lhs_eval = match lhs_eval {
1318 Ok(v) => v,
1319 Err(_) => {
1320 stats.gen_time = gen_time;
1321 stats.search_time = search_start.elapsed();
1322 return (Vec::new(), stats);
1323 }
1324 };
1325 let lhs = EvaluatedExpr::new(
1326 lhs_expr,
1327 lhs_eval.value,
1328 lhs_eval.derivative,
1329 lhs_eval.num_type,
1330 );
1331
1332 stats.lhs_count = 1;
1333 stats.rhs_count = generated.rhs.len();
1334 stats.lhs_tested = 1;
1335
1336 for rhs in generated.rhs {
1337 if !config.rhs_symbol_allowed(&rhs.expr) {
1338 continue;
1339 }
1340 stats.candidates_tested += 1;
1341 if config.show_match_checks {
1342 eprintln!(
1343 " [match] checking lhs={:.6} rhs={:.6}",
1344 lhs.value, rhs.value
1345 );
1346 }
1347
1348 let error = rhs.value - config.target;
1349 let is_exact = error.abs() < EXACT_MATCH_TOLERANCE;
1350 if !pool.would_accept(error.abs(), is_exact) {
1351 continue;
1352 }
1353
1354 let m = Match {
1355 lhs: lhs.clone(),
1356 rhs: rhs.clone(),
1357 x_value: rhs.value,
1358 error,
1359 complexity: lhs.expr.complexity() + rhs.expr.complexity(),
1360 };
1361
1362 pool.try_insert(m);
1363
1364 if config.stop_at_exact && is_exact {
1365 early_exit = true;
1366 break;
1367 }
1368 if let Some(threshold) = config.stop_below {
1369 if error.abs() < threshold {
1370 early_exit = true;
1371 break;
1372 }
1373 }
1374 }
1375
1376 stats.pool_insertions = pool.stats.insertions;
1377 stats.pool_rejections_error = pool.stats.rejections_error;
1378 stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
1379 stats.pool_evictions = pool.stats.evictions;
1380 stats.pool_final_size = pool.len();
1381 stats.pool_best_error = pool.best_error;
1382 stats.gen_time = gen_time;
1383 stats.search_time = search_start.elapsed();
1384 stats.early_exit = early_exit;
1385
1386 (pool.into_sorted(), stats)
1387}
1388
1389#[cfg(feature = "parallel")]
1395#[allow(dead_code)]
1396pub fn search_parallel(
1397 target: f64,
1398 gen_config: &crate::gen::GenConfig,
1399 max_matches: usize,
1400) -> Vec<Match> {
1401 let (matches, _stats) = search_parallel_with_stats(target, gen_config, max_matches);
1402 matches
1403}
1404
1405#[cfg(feature = "parallel")]
1410#[allow(dead_code)]
1411pub fn search_parallel_with_stats(
1412 target: f64,
1413 gen_config: &crate::gen::GenConfig,
1414 max_matches: usize,
1415) -> (Vec<Match>, SearchStats) {
1416 search_parallel_with_stats_and_options(target, gen_config, max_matches, false, None)
1417}
1418
1419#[cfg(feature = "parallel")]
1421pub fn search_parallel_with_stats_and_options(
1422 target: f64,
1423 gen_config: &crate::gen::GenConfig,
1424 max_matches: usize,
1425 stop_at_exact: bool,
1426 stop_below: Option<f64>,
1427) -> (Vec<Match>, SearchStats) {
1428 let config = SearchConfig {
1429 target,
1430 max_matches,
1431 stop_at_exact,
1432 stop_below,
1433 user_constants: gen_config.user_constants.clone(),
1434 user_functions: gen_config.user_functions.clone(),
1435 ..Default::default()
1436 };
1437
1438 search_parallel_with_stats_and_config(gen_config, &config)
1439}
1440
1441#[cfg(feature = "parallel")]
1449pub fn search_parallel_with_stats_and_config(
1450 gen_config: &crate::gen::GenConfig,
1451 config: &SearchConfig,
1452) -> (Vec<Match>, SearchStats) {
1453 use crate::gen::{
1454 count_expressions_with_limit_and_context, generate_all_parallel_with_context,
1455 };
1456
1457 if !config.target.is_finite() {
1458 return (Vec::new(), SearchStats::default());
1459 }
1460
1461 if config.stop_at_exact || config.stop_below.is_some() {
1466 return search_with_stats_and_config(gen_config, config);
1467 }
1468
1469 const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
1470 let context = SearchContext::new(config);
1471
1472 if count_expressions_with_limit_and_context(
1475 gen_config,
1476 config.target,
1477 &context.eval,
1478 MAX_EXPRESSIONS_BEFORE_STREAMING,
1479 )
1480 .is_none()
1481 {
1482 return search_streaming_with_config(gen_config, config);
1483 }
1484
1485 let gen_start = SearchTimer::start();
1487 let generated = generate_all_parallel_with_context(gen_config, config.target, &context.eval);
1488 let gen_time = gen_start.elapsed();
1489
1490 let mut db = ExprDatabase::new();
1491 db.insert_rhs(generated.rhs);
1492
1493 let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
1494 stats.gen_time = gen_time;
1495 stats.lhs_count = generated.lhs.len();
1496 stats.rhs_count = db.rhs_count();
1497
1498 (matches, stats)
1499}
1500
1501#[cfg(feature = "parallel")]
1510pub fn search_turbo_with_stats_and_config(
1511 gen_config: &crate::gen::GenConfig,
1512 config: &SearchConfig,
1513) -> (Vec<Match>, SearchStats) {
1514 use crate::gen::{
1515 count_expressions_with_limit_and_context,
1516 generate_all_preserving_lhs_with_limit_and_context, generate_all_with_limit_and_context,
1517 };
1518
1519 if !config.target.is_finite() {
1520 return (Vec::new(), SearchStats::default());
1521 }
1522
1523 if config.stop_at_exact || config.stop_below.is_some() {
1530 return search_parallel_with_stats_and_config(gen_config, config);
1531 }
1532
1533 const SERIAL_BATCH_LIMIT: usize = 2_000_000;
1538 const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 64_000_000;
1539 let context = SearchContext::new(config);
1540
1541 let gen_start = SearchTimer::start();
1542 let generated = if count_expressions_with_limit_and_context(
1543 gen_config,
1544 config.target,
1545 &context.eval,
1546 SERIAL_BATCH_LIMIT,
1547 )
1548 .is_some()
1549 {
1550 generate_all_with_limit_and_context(
1551 gen_config,
1552 config.target,
1553 &context.eval,
1554 SERIAL_BATCH_LIMIT,
1555 )
1556 .expect("preflight count established serial batch generation is within limit")
1557 } else {
1558 let Some(generated) = generate_all_preserving_lhs_with_limit_and_context(
1559 gen_config,
1560 config.target,
1561 &context.eval,
1562 MAX_EXPRESSIONS_BEFORE_STREAMING,
1563 ) else {
1564 return search_streaming_with_config(gen_config, config);
1565 };
1566 generated
1567 };
1568 let gen_time = gen_start.elapsed();
1569
1570 let mut db = ExprDatabase::new();
1571 db.insert_rhs(generated.rhs);
1572
1573 let (matches, mut stats) =
1574 db.find_matches_turbo_with_stats_and_context(&generated.lhs, &context);
1575 stats.gen_time = gen_time;
1576 stats.lhs_count = generated.lhs.len();
1577 stats.rhs_count = db.rhs_count();
1578
1579 (matches, stats)
1580}