oxihuman_core/
decision_tree.rs1#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10#[allow(dead_code)]
12pub type ConditionFn = fn(&HashMap<String, bool>) -> bool;
13
14#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub enum DecisionNode {
18 Branch {
20 condition_key: String,
21 true_branch: usize,
22 false_branch: usize,
23 },
24 Leaf(String),
26}
27
28#[allow(dead_code)]
30#[derive(Debug, Clone, Default)]
31pub struct DecisionTree {
32 nodes: Vec<DecisionNode>,
33}
34
35#[allow(dead_code)]
37pub fn new_decision_tree() -> DecisionTree {
38 DecisionTree::default()
39}
40
41#[allow(dead_code)]
43pub fn dt_add_leaf(tree: &mut DecisionTree, action: &str) -> usize {
44 let idx = tree.nodes.len();
45 tree.nodes.push(DecisionNode::Leaf(action.to_string()));
46 idx
47}
48
49#[allow(dead_code)]
51pub fn dt_add_branch(
52 tree: &mut DecisionTree,
53 condition_key: &str,
54 true_branch: usize,
55 false_branch: usize,
56) -> usize {
57 let idx = tree.nodes.len();
58 tree.nodes.push(DecisionNode::Branch {
59 condition_key: condition_key.to_string(),
60 true_branch,
61 false_branch,
62 });
63 idx
64}
65
66#[allow(dead_code)]
69pub fn dt_evaluate(
70 tree: &DecisionTree,
71 root: usize,
72 context: &HashMap<String, bool>,
73) -> Option<String> {
74 if tree.nodes.is_empty() {
75 return None;
76 }
77 let mut current = root;
78 loop {
79 match tree.nodes.get(current)? {
80 DecisionNode::Leaf(action) => return Some(action.clone()),
81 DecisionNode::Branch {
82 condition_key,
83 true_branch,
84 false_branch,
85 } => {
86 let cond = context.get(condition_key).copied().unwrap_or(false);
87 current = if cond { *true_branch } else { *false_branch };
88 }
89 }
90 }
91}
92
93#[allow(dead_code)]
95pub fn dt_node_count(tree: &DecisionTree) -> usize {
96 tree.nodes.len()
97}
98
99#[allow(dead_code)]
101pub fn dt_leaf_count(tree: &DecisionTree) -> usize {
102 tree.nodes
103 .iter()
104 .filter(|n| matches!(n, DecisionNode::Leaf(_)))
105 .count()
106}
107
108#[allow(dead_code)]
110pub fn dt_branch_count(tree: &DecisionTree) -> usize {
111 tree.nodes
112 .iter()
113 .filter(|n| matches!(n, DecisionNode::Branch { .. }))
114 .count()
115}
116
117#[allow(dead_code)]
119pub fn dt_clear(tree: &mut DecisionTree) {
120 tree.nodes.clear();
121}
122
123#[allow(dead_code)]
125pub fn dt_leaf_action(tree: &DecisionTree, idx: usize) -> Option<&str> {
126 match tree.nodes.get(idx)? {
127 DecisionNode::Leaf(a) => Some(a.as_str()),
128 _ => None,
129 }
130}
131
132#[allow(dead_code)]
134pub fn dt_all_actions(tree: &DecisionTree) -> Vec<String> {
135 tree.nodes
136 .iter()
137 .filter_map(|n| {
138 if let DecisionNode::Leaf(a) = n {
139 Some(a.clone())
140 } else {
141 None
142 }
143 })
144 .collect()
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn make_simple_tree() -> (DecisionTree, usize) {
152 let mut tree = new_decision_tree();
153 let leaf_a = dt_add_leaf(&mut tree, "action_a");
154 let leaf_b = dt_add_leaf(&mut tree, "action_b");
155 let root = dt_add_branch(&mut tree, "is_hot", leaf_a, leaf_b);
156 (tree, root)
157 }
158
159 #[test]
160 fn test_empty_tree() {
161 let tree = new_decision_tree();
162 assert_eq!(dt_node_count(&tree), 0);
163 assert_eq!(dt_leaf_count(&tree), 0);
164 }
165
166 #[test]
167 fn test_add_leaf() {
168 let mut tree = new_decision_tree();
169 let idx = dt_add_leaf(&mut tree, "fire");
170 assert_eq!(idx, 0);
171 assert_eq!(dt_node_count(&tree), 1);
172 assert_eq!(dt_leaf_count(&tree), 1);
173 }
174
175 #[test]
176 fn test_add_branch() {
177 let mut tree = new_decision_tree();
178 let a = dt_add_leaf(&mut tree, "a");
179 let b = dt_add_leaf(&mut tree, "b");
180 let root = dt_add_branch(&mut tree, "cond", a, b);
181 assert_eq!(dt_branch_count(&tree), 1);
182 assert_eq!(root, 2);
183 }
184
185 #[test]
186 fn test_evaluate_true_branch() {
187 let (tree, root) = make_simple_tree();
188 let mut ctx = HashMap::new();
189 ctx.insert("is_hot".to_string(), true);
190 let result = dt_evaluate(&tree, root, &ctx);
191 assert_eq!(result, Some("action_a".to_string()));
192 }
193
194 #[test]
195 fn test_evaluate_false_branch() {
196 let (tree, root) = make_simple_tree();
197 let mut ctx = HashMap::new();
198 ctx.insert("is_hot".to_string(), false);
199 let result = dt_evaluate(&tree, root, &ctx);
200 assert_eq!(result, Some("action_b".to_string()));
201 }
202
203 #[test]
204 fn test_evaluate_missing_key_defaults_false() {
205 let (tree, root) = make_simple_tree();
206 let ctx = HashMap::new();
207 let result = dt_evaluate(&tree, root, &ctx);
208 assert_eq!(result, Some("action_b".to_string()));
209 }
210
211 #[test]
212 fn test_leaf_action() {
213 let mut tree = new_decision_tree();
214 let idx = dt_add_leaf(&mut tree, "run");
215 assert_eq!(dt_leaf_action(&tree, idx), Some("run"));
216 }
217
218 #[test]
219 fn test_all_actions() {
220 let (tree, _) = make_simple_tree();
221 let actions = dt_all_actions(&tree);
222 assert_eq!(actions.len(), 2);
223 assert!(actions.contains(&"action_a".to_string()));
224 assert!(actions.contains(&"action_b".to_string()));
225 }
226
227 #[test]
228 fn test_clear() {
229 let (mut tree, _) = make_simple_tree();
230 dt_clear(&mut tree);
231 assert_eq!(dt_node_count(&tree), 0);
232 }
233
234 #[test]
235 fn test_evaluate_empty_returns_none() {
236 let tree = new_decision_tree();
237 let ctx = HashMap::new();
238 assert!(dt_evaluate(&tree, 0, &ctx).is_none());
239 }
240}