use crate::tree::EmlTree;
use super::constants::bake_params_into_lowered;
use super::topology::{dedupe_by_semantics, enumerate_topologies_gated};
use super::{DiscoveredFormula, SharedFormula, SymRegEngine};
pub(super) fn run_shared_topology(
engine: &SymRegEngine,
inputs: &[Vec<f64>],
targets_multi: &[Vec<f64>],
num_vars: usize,
) -> Vec<SharedFormula> {
let n_outputs = targets_multi.len();
if n_outputs == 0 || inputs.is_empty() {
return Vec::new();
}
let config = &engine.config;
let const_leaf = if config.enable_const_leaf {
Some(config.const_leaf_init)
} else {
None
};
let raw_topologies = enumerate_topologies_gated(config.max_depth, num_vars, const_leaf);
let topologies: Vec<EmlTree> = dedupe_by_semantics(raw_topologies);
let complexity_penalty = config.complexity_penalty;
let mut shared_results: Vec<SharedFormula> = topologies
.iter()
.enumerate()
.filter_map(|(topo_idx, tree)| {
fit_shared_topology(
engine,
tree,
topo_idx,
inputs,
targets_multi,
complexity_penalty,
n_outputs,
)
})
.collect();
shared_results.sort_by(|a, b| {
a.total_score
.partial_cmp(&b.total_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
let top_k = (config.num_restarts * 5).max(1);
shared_results.truncate(top_k);
shared_results
}
fn fit_shared_topology(
engine: &SymRegEngine,
tree: &EmlTree,
topo_idx: usize,
inputs: &[Vec<f64>],
targets_multi: &[Vec<f64>],
complexity_penalty: f64,
n_outputs: usize,
) -> Option<SharedFormula> {
let mut per_output_params: Vec<Vec<f64>> = Vec::with_capacity(n_outputs);
let mut per_output_mse: Vec<f64> = Vec::with_capacity(n_outputs);
let mut total_mse = 0.0_f64;
let mut any_feasible = false;
for (out_idx, target) in targets_multi.iter().enumerate() {
let per_output_topo_idx = topo_idx * n_outputs + out_idx;
match engine.optimize_topology(tree, inputs, target, per_output_topo_idx) {
Some(df) => {
per_output_mse.push(df.mse);
total_mse += df.mse;
per_output_params.push(df.params);
any_feasible = true;
}
None => {
per_output_mse.push(f64::INFINITY);
total_mse = f64::INFINITY;
per_output_params.push(Vec::new());
}
}
}
if !any_feasible || !total_mse.is_finite() {
return None;
}
let node_count = tree.size() as f64;
let total_score = total_mse + complexity_penalty * node_count;
let pretty_per_output: Vec<String> = per_output_params
.iter()
.map(|params| {
if params.is_empty() {
"Infeasible".to_string()
} else {
bake_params_into_lowered(tree, params)
.simplify()
.to_pretty()
}
})
.collect();
Some(SharedFormula {
eml_tree: tree.clone(),
per_output_params,
per_output_mse,
total_score,
pretty_per_output,
})
}
pub(super) fn shared_to_multi_result(
shared: Vec<SharedFormula>,
complexity_penalty: f64,
n_outputs: usize,
) -> Vec<Vec<DiscoveredFormula>> {
if n_outputs == 0 || shared.is_empty() {
return vec![Vec::new(); n_outputs];
}
let mut per_output: Vec<Vec<DiscoveredFormula>> = (0..n_outputs)
.map(|_| Vec::with_capacity(shared.len()))
.collect();
for sf in &shared {
for (out_idx, (params, &mse)) in sf
.per_output_params
.iter()
.zip(sf.per_output_mse.iter())
.enumerate()
{
if !mse.is_finite() || out_idx >= n_outputs {
continue;
}
let complexity = sf.eml_tree.size();
let score = mse + complexity_penalty * complexity as f64;
let pretty = sf
.pretty_per_output
.get(out_idx)
.cloned()
.unwrap_or_default();
let n = 1_f64;
let k = params.len() as f64;
let rss_per_n = mse.max(f64::MIN_POSITIVE);
let aic = n * rss_per_n.ln() + 2.0 * k;
let bic = n * rss_per_n.ln() + k * n.max(1.0_f64).ln();
let df = DiscoveredFormula {
eml_tree: sf.eml_tree.clone(),
mse,
complexity,
score,
pretty,
params: params.clone(),
cv_mse: None,
aic,
bic,
param_intervals: None,
};
per_output[out_idx].push(df);
}
}
for output_list in &mut per_output {
output_list.sort_by(|a, b| {
a.score
.partial_cmp(&b.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
per_output
}