use crate::dag::builder::DagBuilder;
use crate::dag::node::DagNodeId;
use crate::dag::symbol::{OpKind, SymbolKind};
#[derive(Debug, Clone, Copy)]
pub struct CostWeights {
pub terminal: f64,
pub add_sub: f64,
pub mul: f64,
pub div: f64,
pub neg: f64,
pub pow_int: f64,
pub sqrt: f64,
pub pow_general: f64,
pub function: f64,
pub modulo: f64,
}
impl Default for CostWeights {
fn default() -> Self {
Self {
terminal: 1.0,
add_sub: 1.0,
mul: 2.0,
div: 8.0,
neg: 1.0,
pow_int: 4.0,
sqrt: 12.0,
pow_general: 40.0,
function: 8.0,
modulo: 20.0,
}
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn node_cost<S: ::std::hash::BuildHasher>(
builder: &DagBuilder,
id: DagNodeId,
child_costs: &std::collections::HashMap<u32, f64, S>,
weights: &CostWeights,
) -> f64 {
let Some(node) = builder.arena().get(id) else {
return weights.terminal;
};
let children_total: f64 = node
.children
.as_slice()
.iter()
.map(|&c| child_costs.get(&c.0).copied().unwrap_or(weights.terminal))
.sum();
let own = match &node.kind {
SymbolKind::Variable(_) | SymbolKind::Constant(_) => weights.terminal,
SymbolKind::Operator(op) => match op {
OpKind::Add | OpKind::Sub => weights.add_sub,
OpKind::Mul => weights.mul,
OpKind::Div => weights.div,
OpKind::Neg => weights.neg,
OpKind::Mod => weights.modulo,
OpKind::Pow => {
let is_int_pow = node.children.as_slice().get(1).is_some_and(|&exp_id| {
builder.arena().get(exp_id).is_some_and(|exp_node| {
if let SymbolKind::Constant(v) = exp_node.kind {
let n = v as i64;
(2..=16).contains(&n) && (n as f64 - v).abs() < 1e-9
} else {
false
}
})
});
if is_int_pow {
weights.pow_int
} else {
weights.pow_general
}
}
},
SymbolKind::Function(_) => weights.function,
};
let own = if matches!(&node.kind, SymbolKind::Operator(OpKind::Pow)) {
let is_half_pow = node.children.as_slice().get(1).is_some_and(|&exp_id| {
builder.arena().get(exp_id).is_some_and(|exp_node| {
matches!(exp_node.kind, SymbolKind::Constant(v) if (v - 0.5).abs() < 1e-9)
})
});
if is_half_pow { weights.sqrt } else { own }
} else {
own
};
own + children_total
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn constant_cost_is_terminal() {
let mut b = DagBuilder::new();
let c = b.constant(3.14);
let costs = HashMap::new();
let w = CostWeights::default();
assert!((node_cost(&b, c, &costs, &w) - w.terminal).abs() < 1e-9);
}
#[test]
fn add_cost_includes_children() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let s = b.add(x, y);
let costs = HashMap::new();
let w = CostWeights::default();
let expected = w.add_sub + w.terminal + w.terminal;
assert!((node_cost(&b, s, &costs, &w) - expected).abs() < 1e-9);
}
#[test]
fn div_costs_more_than_mul() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let d = b.div(x, y);
let m = b.mul(x, y);
let costs = HashMap::new();
let w = CostWeights::default();
assert!(node_cost(&b, d, &costs, &w) > node_cost(&b, m, &costs, &w));
}
#[test]
fn int_pow_costs_less_than_general_pow() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let c2 = b.constant(2.0);
let c15 = b.constant(15.5); let p_int = b.pow(x, c2);
let p_gen = b.pow(x, c15);
let costs = HashMap::new();
let w = CostWeights::default();
assert!(node_cost(&b, p_int, &costs, &w) < node_cost(&b, p_gen, &costs, &w));
}
#[test]
fn custom_weights_change_ordering() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let d = b.div(x, y);
let m = b.mul(x, y);
let costs = HashMap::new();
let w = CostWeights {
div: 2.0,
mul: 2.0,
..CostWeights::default()
};
assert_eq!(
(node_cost(&b, d, &costs, &w) - node_cost(&b, m, &costs, &w)).abs() < 1e-9,
true,
"div and mul should have equal cost with equal weights"
);
}
}