use crate::extractor::extract_features;
use crate::model::{Model, Tree};
const MAX_TRAVERSAL_DEPTH: usize = 1000;
pub fn predict_tree(tree: &Tree, features: &[f32]) -> f32 {
let mut node = 0i32;
for _ in 0..MAX_TRAVERSAL_DEPTH {
let node_idx = node as usize;
let left = match tree.left.get(node_idx) {
Some(&l) => l,
None => return 0.5,
};
if left == -1 {
return tree.value.get(node_idx).copied().unwrap_or(0.5);
}
let feature_idx = tree.feature.get(node_idx).copied().unwrap_or(0);
let threshold = tree.threshold.get(node_idx).copied().unwrap_or(0.0);
let feature_val = features.get(feature_idx as usize).copied().unwrap_or(0.0);
let next = if feature_val <= threshold {
left
} else {
tree.right.get(node_idx).copied().unwrap_or(-1)
};
if next < 0 {
return tree.value.get(node_idx).copied().unwrap_or(0.5);
}
node = next;
}
0.5
}
#[derive(Debug, Clone)]
pub struct PathStep {
pub feature_idx: i32,
pub threshold: f32,
pub feature_val: f32,
pub went_left: bool,
}
pub fn predict_tree_with_path(tree: &Tree, features: &[f32]) -> (f32, Vec<PathStep>) {
let mut node = 0i32;
let mut path = Vec::new();
for _ in 0..MAX_TRAVERSAL_DEPTH {
let node_idx = node as usize;
let left = match tree.left.get(node_idx) {
Some(&l) => l,
None => return (0.5, path),
};
if left == -1 {
return (tree.value.get(node_idx).copied().unwrap_or(0.5), path);
}
let feature_idx = tree.feature.get(node_idx).copied().unwrap_or(0);
let threshold = tree.threshold.get(node_idx).copied().unwrap_or(0.0);
let feature_val = features.get(feature_idx as usize).copied().unwrap_or(0.0);
let went_left = feature_val <= threshold;
let next = if went_left {
left
} else {
tree.right.get(node_idx).copied().unwrap_or(-1)
};
path.push(PathStep {
feature_idx,
threshold,
feature_val,
went_left,
});
if next < 0 {
return (tree.value.get(node_idx).copied().unwrap_or(0.5), path);
}
node = next;
}
(0.5, path)
}
pub fn predict_url(url: &str, model: &Model) -> f32 {
if model.trees.is_empty() {
panic!("predict_url: model contains no trees — cannot produce a meaningful prediction");
}
let features = extract_features(
url,
model.n_features,
model.n_manual_features,
model.ngram_range,
);
let mut sum = 0.0;
for tree in &model.trees {
sum += predict_tree(tree, &features);
}
sum / model.trees.len() as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_predict_tree_single_node() {
let tree = Tree {
left: vec![-1],
right: vec![-1],
feature: vec![0],
threshold: vec![0.0],
value: vec![0.5],
};
let features = vec![1.0];
assert_eq!(predict_tree(&tree, &features), 0.5);
}
#[test]
fn test_predict_tree_with_split() {
let tree = Tree {
left: vec![1, -1, -1],
right: vec![2, -1, -1],
feature: vec![0, 0, 0],
threshold: vec![1.0, 0.0, 0.0],
value: vec![0.0, 0.2, 0.8],
};
let features_left = vec![0.5];
assert_eq!(predict_tree(&tree, &features_left), 0.2);
let features_right = vec![1.5];
assert_eq!(predict_tree(&tree, &features_right), 0.8);
}
#[test]
fn test_predict_tree_cycle_returns_neutral() {
let tree = Tree {
left: vec![1, 0],
right: vec![-1, -1],
feature: vec![0, 0],
threshold: vec![1.0, 1.0],
value: vec![0.0, 0.0],
};
let features = vec![0.5];
let score = predict_tree(&tree, &features);
assert_eq!(score, 0.5, "Cyclic tree should return neutral 0.5");
}
#[test]
fn test_predict_tree_out_of_bounds_returns_neutral() {
let tree = Tree {
left: vec![5],
right: vec![-1],
feature: vec![0],
threshold: vec![1.0],
value: vec![0.0],
};
let features = vec![0.5];
let score = predict_tree(&tree, &features);
assert_eq!(score, 0.5, "Out-of-bounds node should return neutral 0.5");
}
#[test]
#[should_panic(expected = "model contains no trees")]
fn test_predict_url_empty_trees_panics() {
let model = Model {
n_features: 10,
n_manual_features: 5,
ngram_range: [2, 3],
trees: vec![],
};
let _ = predict_url("http://example.com", &model);
}
#[test]
fn test_predict_tree_with_path_single_node() {
let tree = Tree {
left: vec![-1],
right: vec![-1],
feature: vec![0],
threshold: vec![0.0],
value: vec![0.5],
};
let features = vec![1.0];
let (score, path) = predict_tree_with_path(&tree, &features);
assert_eq!(score, 0.5);
assert!(path.is_empty(), "Single leaf node should have empty path");
}
#[test]
fn test_predict_tree_with_path_left_branch() {
let tree = Tree {
left: vec![1, -1, -1],
right: vec![2, -1, -1],
feature: vec![0, 0, 0],
threshold: vec![1.0, 0.0, 0.0],
value: vec![0.0, 0.2, 0.8],
};
let features = vec![0.5]; let (score, path) = predict_tree_with_path(&tree, &features);
assert_eq!(score, 0.2);
assert_eq!(path.len(), 1);
assert_eq!(path[0].feature_idx, 0);
assert_eq!(path[0].feature_val, 0.5);
assert_eq!(path[0].threshold, 1.0);
assert!(path[0].went_left);
}
#[test]
fn test_predict_tree_with_path_right_branch() {
let tree = Tree {
left: vec![1, -1, -1],
right: vec![2, -1, -1],
feature: vec![0, 0, 0],
threshold: vec![1.0, 0.0, 0.0],
value: vec![0.0, 0.2, 0.8],
};
let features = vec![1.5]; let (score, path) = predict_tree_with_path(&tree, &features);
assert_eq!(score, 0.8);
assert_eq!(path.len(), 1);
assert!(!path[0].went_left);
}
#[test]
fn test_predict_tree_with_path_multi_depth() {
let tree = Tree {
left: vec![1, 3, -1, -1],
right: vec![2, -1, -1, -1],
feature: vec![0, 1, 0, 0],
threshold: vec![0.5, 0.5, 0.0, 0.0],
value: vec![0.0, 0.0, 0.8, 0.3],
};
let features = vec![0.3, 0.4];
let (score, path) = predict_tree_with_path(&tree, &features);
assert_eq!(score, 0.3);
assert_eq!(path.len(), 2);
assert_eq!(path[0].feature_idx, 0);
assert!(path[0].went_left);
assert_eq!(path[1].feature_idx, 1);
assert!(path[1].went_left);
}
}