lean_ctx/core/
free_energy_budget.rs1#[derive(Debug, Clone)]
17pub struct ColumnBudgetRequest {
18 pub column_id: String,
19 pub saliency_score: f64,
20 pub estimated_tokens: usize,
21 pub minimum_tokens: usize,
22}
23
24#[derive(Debug, Clone)]
26pub struct ColumnBudgetAllocation {
27 pub column_id: String,
28 pub allocated_tokens: usize,
29 pub fraction: f64,
30}
31
32pub fn allocate_budget(
38 total_budget: usize,
39 requests: &[ColumnBudgetRequest],
40 floor_fraction: f64,
41) -> Vec<ColumnBudgetAllocation> {
42 if requests.is_empty() || total_budget == 0 {
43 return Vec::new();
44 }
45
46 let floor = (total_budget as f64 * floor_fraction.clamp(0.0, 0.5)) as usize;
47 let total_floor = floor * requests.len();
48 let distributable = total_budget.saturating_sub(total_floor);
49
50 let efficiencies: Vec<f64> = requests
51 .iter()
52 .map(|r| {
53 let cost = r.estimated_tokens.max(1) as f64;
54 r.saliency_score / cost
55 })
56 .collect();
57
58 let total_efficiency: f64 = efficiencies.iter().sum();
59
60 requests
61 .iter()
62 .enumerate()
63 .map(|(i, req)| {
64 let proportional = if total_efficiency > 0.0 {
65 (efficiencies[i] / total_efficiency * distributable as f64) as usize
66 } else {
67 distributable / requests.len()
68 };
69
70 let allocated = (floor + proportional)
71 .max(req.minimum_tokens)
72 .min(total_budget);
73 let fraction = allocated as f64 / total_budget as f64;
74
75 ColumnBudgetAllocation {
76 column_id: req.column_id.clone(),
77 allocated_tokens: allocated,
78 fraction,
79 }
80 })
81 .collect()
82}
83
84pub fn free_energy(
87 requests: &[ColumnBudgetRequest],
88 allocations: &[ColumnBudgetAllocation],
89) -> f64 {
90 if requests.is_empty() {
91 return 0.0;
92 }
93
94 let total_requested: f64 = requests.iter().map(|r| r.estimated_tokens as f64).sum();
95 let total_allocated: f64 = allocations.iter().map(|a| a.allocated_tokens as f64).sum();
96
97 if total_requested == 0.0 {
98 return 0.0;
99 }
100
101 ((total_requested - total_allocated) / total_requested).max(0.0)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn request(id: &str, saliency: f64, tokens: usize) -> ColumnBudgetRequest {
109 ColumnBudgetRequest {
110 column_id: id.into(),
111 saliency_score: saliency,
112 estimated_tokens: tokens,
113 minimum_tokens: 0,
114 }
115 }
116
117 #[test]
118 fn allocate_empty_returns_empty() {
119 assert!(allocate_budget(1000, &[], 0.05).is_empty());
120 }
121
122 #[test]
123 fn allocate_single_column_gets_all() {
124 let reqs = vec![request("fs", 1.0, 500)];
125 let allocs = allocate_budget(1000, &reqs, 0.05);
126
127 assert_eq!(allocs.len(), 1);
128 assert!(allocs[0].allocated_tokens >= 950);
129 }
130
131 #[test]
132 fn high_saliency_gets_more_budget() {
133 let reqs = vec![
134 request("important", 0.9, 500),
135 request("unimportant", 0.1, 500),
136 ];
137 let allocs = allocate_budget(1000, &reqs, 0.05);
138
139 assert!(allocs[0].allocated_tokens > allocs[1].allocated_tokens);
140 }
141
142 #[test]
143 fn efficient_column_gets_more_budget() {
144 let reqs = vec![
145 request("efficient", 0.5, 100), request("expensive", 0.5, 10000), ];
148 let allocs = allocate_budget(2000, &reqs, 0.05);
149
150 assert!(allocs[0].allocated_tokens > allocs[1].allocated_tokens);
151 }
152
153 #[test]
154 fn floor_ensures_minimum_allocation() {
155 let reqs = vec![request("dominant", 0.99, 100), request("tiny", 0.01, 100)];
156 let allocs = allocate_budget(1000, &reqs, 0.1);
157
158 assert!(allocs[1].allocated_tokens >= 100);
159 }
160
161 #[test]
162 fn free_energy_zero_when_fully_satisfied() {
163 let reqs = vec![request("a", 1.0, 500)];
164 let allocs = vec![ColumnBudgetAllocation {
165 column_id: "a".into(),
166 allocated_tokens: 500,
167 fraction: 1.0,
168 }];
169 assert!((free_energy(&reqs, &allocs)).abs() < f64::EPSILON);
170 }
171
172 #[test]
173 fn free_energy_positive_when_under_budget() {
174 let reqs = vec![request("a", 1.0, 1000)];
175 let allocs = vec![ColumnBudgetAllocation {
176 column_id: "a".into(),
177 allocated_tokens: 500,
178 fraction: 0.5,
179 }];
180 let fe = free_energy(&reqs, &allocs);
181 assert!(fe > 0.0);
182 assert!((fe - 0.5).abs() < f64::EPSILON);
183 }
184}