use crate::eval::EvalContext;
use crate::expr::EvaluatedExpr;
use crate::pool::{RankingMode, TopKPool};
use crate::profile::UserConstant;
use crate::thresholds::{
DEGENERATE_DERIVATIVE, DEGENERATE_RANGE_TOLERANCE, DEGENERATE_TEST_THRESHOLD,
EXACT_MATCH_TOLERANCE, NEWTON_FINAL_TOLERANCE,
};
use std::collections::HashSet;
use std::time::Duration;
mod db;
mod newton;
#[cfg(test)]
mod tests;
use db::calculate_adaptive_search_radius;
pub use db::{ComplexityTier, ExprDatabase, TieredExprDatabase};
#[cfg(test)]
use newton::newton_raphson;
use newton::newton_raphson_with_constants;
#[derive(Clone, Copy, Debug)]
struct SearchTimer {
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
start_ms: f64,
#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
start: std::time::Instant,
}
impl SearchTimer {
#[inline]
fn start() -> Self {
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
{
Self {
start_ms: js_sys::Date::now(),
}
}
#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
{
Self {
start: std::time::Instant::now(),
}
}
}
#[inline]
fn elapsed(self) -> Duration {
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
{
let elapsed_ms = (js_sys::Date::now() - self.start_ms).max(0.0);
Duration::from_secs_f64(elapsed_ms / 1000.0)
}
#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
{
self.start.elapsed()
}
}
}
#[derive(Clone, Debug, Default)]
pub struct SearchStats {
pub gen_time: Duration,
pub search_time: Duration,
pub lhs_count: usize,
pub rhs_count: usize,
pub lhs_tested: usize,
pub candidates_tested: usize,
pub newton_calls: usize,
pub newton_success: usize,
pub pool_insertions: usize,
pub pool_rejections_error: usize,
pub pool_rejections_dedupe: usize,
pub pool_evictions: usize,
pub pool_final_size: usize,
pub pool_best_error: f64,
pub early_exit: bool,
}
impl SearchStats {
pub fn new() -> Self {
Self::default()
}
pub fn print(&self) {
println!();
println!(" === Search Statistics ===");
println!();
println!(" Generation:");
println!(
" Time: {:>10.3}ms",
self.gen_time.as_secs_f64() * 1000.0
);
println!(" LHS expressions: {:>10}", self.lhs_count);
println!(" RHS expressions: {:>10}", self.rhs_count);
println!();
println!(" Search:");
println!(
" Time: {:>10.3}ms",
self.search_time.as_secs_f64() * 1000.0
);
println!(" LHS tested: {:>10}", self.lhs_tested);
println!(" Candidates: {:>10}", self.candidates_tested);
println!(" Newton calls: {:>10}", self.newton_calls);
println!(
" Newton success: {:>10} ({:.1}%)",
self.newton_success,
if self.newton_calls > 0 {
100.0 * self.newton_success as f64 / self.newton_calls as f64
} else {
0.0
}
);
if self.early_exit {
println!(" Early exit: yes");
}
println!();
println!(" Pool:");
println!(" Insertions: {:>10}", self.pool_insertions);
println!(" Rejected (err): {:>10}", self.pool_rejections_error);
println!(" Rejected (dup): {:>10}", self.pool_rejections_dedupe);
println!(" Evictions: {:>10}", self.pool_evictions);
println!(" Final size: {:>10}", self.pool_final_size);
println!(" Best error: {:>14.2e}", self.pool_best_error);
}
}
#[inline]
pub fn level_to_complexity(level: u32) -> (u32, u32) {
const BASE_LHS: u32 = 10;
const BASE_RHS: u32 = 12;
const LEVEL_MULTIPLIER: u32 = 4;
let level_factor = LEVEL_MULTIPLIER.saturating_mul(level);
(
BASE_LHS.saturating_add(level_factor),
BASE_RHS.saturating_add(level_factor),
)
}
#[derive(Clone, Debug)]
pub struct Match {
pub lhs: EvaluatedExpr,
pub rhs: EvaluatedExpr,
pub x_value: f64,
pub error: f64,
pub complexity: u32,
}
impl Match {
#[cfg(test)]
pub fn display(&self, _target: f64) -> String {
let lhs_str = self.lhs.expr.to_infix();
let rhs_str = self.rhs.expr.to_infix();
let error_str = if self.error.abs() < EXACT_MATCH_TOLERANCE {
"('exact' match)".to_string()
} else {
let sign = if self.error >= 0.0 { "+" } else { "-" };
format!("for x = T {} {:.6e}", sign, self.error.abs())
};
format!(
"{:>24} = {:<24} {} {{{}}}",
lhs_str, rhs_str, error_str, self.complexity
)
}
}
#[derive(Clone, Debug)]
pub struct SearchConfig {
pub target: f64,
pub max_matches: usize,
pub max_error: f64,
pub stop_at_exact: bool,
pub stop_below: Option<f64>,
pub zero_value_threshold: f64,
pub newton_iterations: usize,
pub user_constants: Vec<UserConstant>,
pub user_functions: Vec<crate::udf::UserFunction>,
pub trig_argument_scale: f64,
pub refine_with_newton: bool,
pub rhs_allowed_symbols: Option<HashSet<u8>>,
pub rhs_excluded_symbols: Option<HashSet<u8>>,
pub show_newton: bool,
pub show_match_checks: bool,
#[allow(dead_code)]
pub show_pruned_arith: bool,
pub show_pruned_range: bool,
pub show_db_adds: bool,
#[allow(dead_code)]
pub match_all_digits: bool,
pub derivative_margin: f64,
pub ranking_mode: RankingMode,
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
target: 0.0,
max_matches: 100,
max_error: 1.0,
stop_at_exact: false,
stop_below: None,
zero_value_threshold: 1e-4,
newton_iterations: 15,
user_constants: Vec::new(),
user_functions: Vec::new(),
trig_argument_scale: crate::eval::DEFAULT_TRIG_ARGUMENT_SCALE,
refine_with_newton: true,
rhs_allowed_symbols: None,
rhs_excluded_symbols: None,
show_newton: false,
show_match_checks: false,
show_pruned_arith: false,
show_pruned_range: false,
show_db_adds: false,
match_all_digits: false,
derivative_margin: DEGENERATE_DERIVATIVE,
ranking_mode: RankingMode::Complexity,
}
}
}
impl SearchConfig {
pub fn context(&self) -> SearchContext<'_> {
SearchContext::new(self)
}
#[inline]
fn rhs_symbol_allowed(&self, rhs: &crate::expr::Expression) -> bool {
let symbols = rhs.symbols();
if let Some(allowed) = &self.rhs_allowed_symbols {
if symbols.iter().any(|s| !allowed.contains(&(*s as u8))) {
return false;
}
}
if let Some(excluded) = &self.rhs_excluded_symbols {
if symbols.iter().any(|s| excluded.contains(&(*s as u8))) {
return false;
}
}
true
}
}
#[derive(Clone, Copy, Debug)]
pub struct SearchContext<'a> {
pub config: &'a SearchConfig,
pub eval: EvalContext<'a>,
}
impl<'a> SearchContext<'a> {
pub fn new(config: &'a SearchConfig) -> Self {
Self {
config,
eval: EvalContext::from_slices(&config.user_constants, &config.user_functions)
.with_trig_argument_scale(config.trig_argument_scale),
}
}
}
#[allow(dead_code)]
pub fn search(target: f64, gen_config: &crate::gen::GenConfig, max_matches: usize) -> Vec<Match> {
let (matches, _stats) = search_with_stats(target, gen_config, max_matches);
matches
}
#[allow(dead_code)]
pub fn search_with_stats(
target: f64,
gen_config: &crate::gen::GenConfig,
max_matches: usize,
) -> (Vec<Match>, SearchStats) {
search_with_stats_and_options(target, gen_config, max_matches, false, None)
}
pub fn search_with_stats_and_options(
target: f64,
gen_config: &crate::gen::GenConfig,
max_matches: usize,
stop_at_exact: bool,
stop_below: Option<f64>,
) -> (Vec<Match>, SearchStats) {
if !target.is_finite() {
return (Vec::new(), SearchStats::default());
}
let config = SearchConfig {
target,
max_matches,
stop_at_exact,
stop_below,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
search_with_stats_and_config(gen_config, &config)
}
pub fn search_with_stats_and_config(
gen_config: &crate::gen::GenConfig,
config: &SearchConfig,
) -> (Vec<Match>, SearchStats) {
if !config.target.is_finite() {
return (Vec::new(), SearchStats::default());
}
use crate::gen::generate_all_with_limit_and_context;
const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
let context = SearchContext::new(config);
let gen_start = SearchTimer::start();
if let Some(generated) = generate_all_with_limit_and_context(
gen_config,
config.target,
&context.eval,
MAX_EXPRESSIONS_BEFORE_STREAMING,
) {
let gen_time = gen_start.elapsed();
let mut db = ExprDatabase::new();
db.insert_rhs(generated.rhs);
let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
stats.gen_time = gen_time;
stats.lhs_count = generated.lhs.len();
stats.rhs_count = db.rhs_count();
(matches, stats)
} else {
search_streaming_with_config(gen_config, config)
}
}
pub fn search_adaptive(
base_config: &crate::gen::GenConfig,
search_config: &SearchConfig,
level: u32,
) -> (Vec<Match>, SearchStats) {
use crate::expr::EvaluatedExpr;
use crate::gen::{quantize_value, LhsKey};
use std::collections::HashMap;
let gen_start = SearchTimer::start();
let context = SearchContext::new(search_config);
let mut lhs_map: HashMap<LhsKey, EvaluatedExpr> = HashMap::new();
let mut rhs_map: HashMap<i64, EvaluatedExpr> = HashMap::new();
let (std_lhs, std_rhs) = level_to_complexity(level);
let mut config = base_config.clone();
config.max_lhs_complexity = std_lhs.max(base_config.max_lhs_complexity);
config.max_rhs_complexity = std_rhs.max(base_config.max_rhs_complexity);
let generated = {
#[cfg(feature = "parallel")]
{
crate::gen::generate_all_parallel_with_context(
&config,
search_config.target,
&context.eval,
)
}
#[cfg(not(feature = "parallel"))]
{
crate::gen::generate_all_with_context(&config, search_config.target, &context.eval)
}
};
for expr in generated.lhs {
let key = (quantize_value(expr.value), quantize_value(expr.derivative));
lhs_map
.entry(key)
.and_modify(|existing| {
if expr.expr.complexity() < existing.expr.complexity() {
*existing = expr.clone();
}
})
.or_insert(expr);
}
for expr in generated.rhs {
let key = quantize_value(expr.value);
rhs_map
.entry(key)
.and_modify(|existing| {
if expr.expr.complexity() < existing.expr.complexity() {
*existing = expr.clone();
}
})
.or_insert(expr);
}
let all_lhs: Vec<EvaluatedExpr> = lhs_map.into_values().collect();
let all_rhs: Vec<EvaluatedExpr> = rhs_map.into_values().collect();
let gen_time = gen_start.elapsed();
let mut db = ExprDatabase::new();
db.insert_rhs(all_rhs);
let search_start = SearchTimer::start();
let (matches, match_stats) = db.find_matches_with_stats_and_context(&all_lhs, &context);
let search_time = search_start.elapsed();
let mut stats = SearchStats::new();
stats.gen_time = gen_time;
stats.search_time = search_time;
stats.lhs_count = all_lhs.len();
stats.rhs_count = db.rhs_count();
stats.lhs_tested = match_stats.lhs_tested;
stats.candidates_tested = match_stats.candidates_tested;
stats.newton_calls = match_stats.newton_calls;
stats.newton_success = match_stats.newton_success;
stats.pool_insertions = match_stats.pool_insertions;
stats.pool_rejections_error = match_stats.pool_rejections_error;
stats.pool_rejections_dedupe = match_stats.pool_rejections_dedupe;
stats.pool_evictions = match_stats.pool_evictions;
stats.pool_final_size = match_stats.pool_final_size;
stats.pool_best_error = match_stats.pool_best_error;
stats.early_exit = match_stats.early_exit;
(matches, stats)
}
#[allow(dead_code)]
pub fn search_streaming(
target: f64,
gen_config: &crate::gen::GenConfig,
max_matches: usize,
stop_at_exact: bool,
stop_below: Option<f64>,
) -> (Vec<Match>, SearchStats) {
let config = SearchConfig {
target,
max_matches,
stop_at_exact,
stop_below,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
search_streaming_with_config(gen_config, &config)
}
pub fn search_streaming_with_config(
gen_config: &crate::gen::GenConfig,
search_config: &SearchConfig,
) -> (Vec<Match>, SearchStats) {
use crate::gen::{generate_streaming_with_context, StreamingCallbacks};
use std::collections::HashMap;
let gen_start = SearchTimer::start();
let mut stats = SearchStats::new();
let context = SearchContext::new(search_config);
let initial_max_error = search_config.max_error.max(1e-12);
let mut pool = TopKPool::new_with_diagnostics(
search_config.max_matches,
initial_max_error,
search_config.show_db_adds,
search_config.ranking_mode,
);
let mut rhs_db = TieredExprDatabase::new();
let mut rhs_map: HashMap<i64, crate::expr::EvaluatedExpr> = HashMap::new();
let mut lhs_exprs: Vec<crate::expr::EvaluatedExpr> = Vec::new();
{
let mut callbacks = StreamingCallbacks {
on_rhs: &mut |expr| {
let key = crate::gen::quantize_value(expr.value);
rhs_map
.entry(key)
.and_modify(|existing| {
if expr.expr.complexity() < existing.expr.complexity() {
*existing = expr.clone();
}
})
.or_insert_with(|| expr.clone());
true
},
on_lhs: &mut |expr| {
lhs_exprs.push(expr.clone());
true
},
};
generate_streaming_with_context(
gen_config,
search_config.target,
&context.eval,
&mut callbacks,
);
}
for expr in rhs_map.into_values() {
rhs_db.insert(expr);
}
rhs_db.finalize();
stats.rhs_count = rhs_db.total_count();
stats.lhs_count = lhs_exprs.len();
stats.gen_time = gen_start.elapsed();
let search_start = SearchTimer::start();
lhs_exprs.sort_by_key(|e| e.expr.complexity());
let mut early_exit = false;
'outer: for lhs in &lhs_exprs {
if early_exit {
break;
}
if lhs.value.abs() < search_config.zero_value_threshold {
if search_config.show_pruned_range {
eprintln!(
" [pruned range] value={:.6e} reason=\"near-zero\" expr=\"{}\"",
lhs.value,
lhs.expr.to_infix()
);
}
continue;
}
if lhs.derivative.abs() < DEGENERATE_TEST_THRESHOLD {
let test_x = search_config.target + std::f64::consts::E;
if let Ok(test_result) =
crate::eval::evaluate_fast_with_context(&lhs.expr, test_x, &context.eval)
{
let value_unchanged =
(test_result.value - lhs.value).abs() < DEGENERATE_TEST_THRESHOLD;
let deriv_still_zero = test_result.derivative.abs() < DEGENERATE_TEST_THRESHOLD;
if deriv_still_zero || value_unchanged {
continue;
}
}
stats.lhs_tested += 1;
for rhs in rhs_db.iter_tiers_in_range(
lhs.value - DEGENERATE_RANGE_TOLERANCE,
lhs.value + DEGENERATE_RANGE_TOLERANCE,
) {
if !search_config.rhs_symbol_allowed(&rhs.expr) {
continue;
}
stats.candidates_tested += 1;
if search_config.show_match_checks {
eprintln!(
" [match] checking lhs={:.6} rhs={:.6}",
lhs.value, rhs.value
);
}
let val_diff = (lhs.value - rhs.value).abs();
if val_diff < DEGENERATE_RANGE_TOLERANCE && pool.would_accept(0.0, true) {
let m = Match {
lhs: lhs.clone(),
rhs: rhs.clone(),
x_value: search_config.target,
error: 0.0,
complexity: lhs.expr.complexity() + rhs.expr.complexity(),
};
pool.try_insert(m);
}
}
continue;
}
stats.lhs_tested += 1;
let search_radius = calculate_adaptive_search_radius(
lhs.derivative,
lhs.expr.complexity(),
pool.len(),
search_config.max_matches,
pool.best_error,
);
let low = lhs.value - search_radius;
let high = lhs.value + search_radius;
for rhs in rhs_db.iter_tiers_in_range(low, high) {
if !search_config.rhs_symbol_allowed(&rhs.expr) {
continue;
}
stats.candidates_tested += 1;
if search_config.show_match_checks {
eprintln!(
" [match] checking lhs={:.6} rhs={:.6}",
lhs.value, rhs.value
);
}
let val_diff = lhs.value - rhs.value;
let x_delta = -val_diff / lhs.derivative;
let coarse_error = x_delta.abs();
let is_potentially_exact = coarse_error < NEWTON_FINAL_TOLERANCE;
if !pool.would_accept_strict(coarse_error, is_potentially_exact) {
continue;
}
if !search_config.refine_with_newton {
let refined_x = search_config.target + x_delta;
let refined_error = x_delta;
let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
if pool.would_accept(refined_error.abs(), is_exact) {
let m = Match {
lhs: lhs.clone(),
rhs: rhs.clone(),
x_value: refined_x,
error: refined_error,
complexity: lhs.expr.complexity() + rhs.expr.complexity(),
};
pool.try_insert(m);
if search_config.stop_at_exact && is_exact {
early_exit = true;
break 'outer;
}
if let Some(threshold) = search_config.stop_below {
if refined_error.abs() < threshold {
early_exit = true;
break 'outer;
}
}
}
continue;
}
stats.newton_calls += 1;
if let Some(refined_x) = newton_raphson_with_constants(
&lhs.expr,
rhs.value,
search_config.target,
search_config.newton_iterations,
&context.eval,
search_config.show_newton,
search_config.derivative_margin,
) {
stats.newton_success += 1;
let refined_error = refined_x - search_config.target;
let is_exact = refined_error.abs() < EXACT_MATCH_TOLERANCE;
if pool.would_accept(refined_error.abs(), is_exact) {
let m = Match {
lhs: lhs.clone(),
rhs: rhs.clone(),
x_value: refined_x,
error: refined_error,
complexity: lhs.expr.complexity() + rhs.expr.complexity(),
};
pool.try_insert(m);
if search_config.stop_at_exact && is_exact {
early_exit = true;
break 'outer;
}
if let Some(threshold) = search_config.stop_below {
if refined_error.abs() < threshold {
early_exit = true;
break 'outer;
}
}
}
}
}
}
stats.pool_insertions = pool.stats.insertions;
stats.pool_rejections_error = pool.stats.rejections_error;
stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
stats.pool_evictions = pool.stats.evictions;
stats.pool_final_size = pool.len();
stats.pool_best_error = pool.best_error;
stats.search_time = search_start.elapsed();
stats.early_exit = early_exit;
(pool.into_sorted(), stats)
}
pub fn search_one_sided_with_stats_and_config(
gen_config: &crate::gen::GenConfig,
config: &SearchConfig,
) -> (Vec<Match>, SearchStats) {
use crate::eval::evaluate_with_context;
use crate::expr::Expression;
use crate::gen::generate_all_with_context;
use crate::symbol::Symbol;
let gen_start = SearchTimer::start();
let context = SearchContext::new(config);
let mut rhs_only = gen_config.clone();
rhs_only.generate_lhs = false;
rhs_only.generate_rhs = true;
let generated = generate_all_with_context(&rhs_only, config.target, &context.eval);
let gen_time = gen_start.elapsed();
let search_start = SearchTimer::start();
let initial_max_error = config.max_error.max(1e-12);
let mut pool = TopKPool::new_with_diagnostics(
config.max_matches,
initial_max_error,
config.show_db_adds,
config.ranking_mode,
);
let mut stats = SearchStats::new();
let mut early_exit = false;
let mut lhs_expr = Expression::new();
lhs_expr.push_with_table(Symbol::X, &gen_config.symbol_table);
let lhs_eval = evaluate_with_context(&lhs_expr, config.target, &context.eval);
let lhs_eval = match lhs_eval {
Ok(v) => v,
Err(_) => {
stats.gen_time = gen_time;
stats.search_time = search_start.elapsed();
return (Vec::new(), stats);
}
};
let lhs = EvaluatedExpr::new(
lhs_expr,
lhs_eval.value,
lhs_eval.derivative,
lhs_eval.num_type,
);
stats.lhs_count = 1;
stats.rhs_count = generated.rhs.len();
stats.lhs_tested = 1;
for rhs in generated.rhs {
if !config.rhs_symbol_allowed(&rhs.expr) {
continue;
}
stats.candidates_tested += 1;
if config.show_match_checks {
eprintln!(
" [match] checking lhs={:.6} rhs={:.6}",
lhs.value, rhs.value
);
}
let error = rhs.value - config.target;
let is_exact = error.abs() < EXACT_MATCH_TOLERANCE;
if !pool.would_accept(error.abs(), is_exact) {
continue;
}
let m = Match {
lhs: lhs.clone(),
rhs: rhs.clone(),
x_value: rhs.value,
error,
complexity: lhs.expr.complexity() + rhs.expr.complexity(),
};
pool.try_insert(m);
if config.stop_at_exact && is_exact {
early_exit = true;
break;
}
if let Some(threshold) = config.stop_below {
if error.abs() < threshold {
early_exit = true;
break;
}
}
}
stats.pool_insertions = pool.stats.insertions;
stats.pool_rejections_error = pool.stats.rejections_error;
stats.pool_rejections_dedupe = pool.stats.rejections_dedupe;
stats.pool_evictions = pool.stats.evictions;
stats.pool_final_size = pool.len();
stats.pool_best_error = pool.best_error;
stats.gen_time = gen_time;
stats.search_time = search_start.elapsed();
stats.early_exit = early_exit;
(pool.into_sorted(), stats)
}
#[cfg(feature = "parallel")]
#[allow(dead_code)]
pub fn search_parallel(
target: f64,
gen_config: &crate::gen::GenConfig,
max_matches: usize,
) -> Vec<Match> {
let (matches, _stats) = search_parallel_with_stats(target, gen_config, max_matches);
matches
}
#[cfg(feature = "parallel")]
#[allow(dead_code)]
pub fn search_parallel_with_stats(
target: f64,
gen_config: &crate::gen::GenConfig,
max_matches: usize,
) -> (Vec<Match>, SearchStats) {
search_parallel_with_stats_and_options(target, gen_config, max_matches, false, None)
}
#[cfg(feature = "parallel")]
pub fn search_parallel_with_stats_and_options(
target: f64,
gen_config: &crate::gen::GenConfig,
max_matches: usize,
stop_at_exact: bool,
stop_below: Option<f64>,
) -> (Vec<Match>, SearchStats) {
let config = SearchConfig {
target,
max_matches,
stop_at_exact,
stop_below,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
search_parallel_with_stats_and_config(gen_config, &config)
}
#[cfg(feature = "parallel")]
pub fn search_parallel_with_stats_and_config(
gen_config: &crate::gen::GenConfig,
config: &SearchConfig,
) -> (Vec<Match>, SearchStats) {
use crate::gen::generate_all_with_limit_and_context;
const MAX_EXPRESSIONS_BEFORE_STREAMING: usize = 2_000_000;
let context = SearchContext::new(config);
let gen_start = SearchTimer::start();
if let Some(generated) = generate_all_with_limit_and_context(
gen_config,
config.target,
&context.eval,
MAX_EXPRESSIONS_BEFORE_STREAMING,
) {
let gen_time = gen_start.elapsed();
let mut db = ExprDatabase::new();
db.insert_rhs(generated.rhs);
let (matches, mut stats) = db.find_matches_with_stats_and_context(&generated.lhs, &context);
stats.gen_time = gen_time;
stats.lhs_count = generated.lhs.len();
stats.rhs_count = db.rhs_count();
(matches, stats)
} else {
search_streaming_with_config(gen_config, config)
}
}