thrust_rl/buffer/replay/
sum_tree.rs1#[derive(Debug, Clone)]
38pub struct SumTree {
39 capacity: usize,
41 nodes: Vec<f32>,
44}
45
46impl SumTree {
47 pub fn new(capacity: usize) -> Self {
55 assert!(capacity > 0, "SumTree capacity must be > 0");
56 Self { capacity, nodes: vec![0.0; 2 * capacity - 1] }
57 }
58
59 #[inline]
61 pub fn total(&self) -> f32 {
62 self.nodes[0]
63 }
64
65 #[inline]
67 pub fn capacity(&self) -> usize {
68 self.capacity
69 }
70
71 #[inline]
73 fn leaf_offset(&self) -> usize {
74 self.capacity - 1
75 }
76
77 pub fn max_leaf(&self) -> f32 {
83 let offset = self.leaf_offset();
84 let mut m: f32 = 0.0;
85 for &v in &self.nodes[offset..offset + self.capacity] {
86 if v > m {
87 m = v;
88 }
89 }
90 m
91 }
92
93 pub fn leaf_priority(&self, leaf_idx: usize) -> f32 {
98 assert!(
99 leaf_idx < self.capacity,
100 "SumTree::leaf_priority: leaf_idx ({}) >= capacity ({})",
101 leaf_idx,
102 self.capacity
103 );
104 self.nodes[self.leaf_offset() + leaf_idx]
105 }
106
107 pub fn update(&mut self, leaf_idx: usize, priority: f32) {
114 assert!(
115 leaf_idx < self.capacity,
116 "SumTree::update: leaf_idx ({}) >= capacity ({})",
117 leaf_idx,
118 self.capacity
119 );
120 assert!(
121 priority.is_finite() && priority >= 0.0,
122 "SumTree::update: priority must be finite and non-negative, got {}",
123 priority
124 );
125
126 let node_idx = self.leaf_offset() + leaf_idx;
127 let delta = priority - self.nodes[node_idx];
128 self.nodes[node_idx] = priority;
129
130 let mut idx = node_idx;
132 while idx > 0 {
133 idx = (idx - 1) / 2;
134 self.nodes[idx] += delta;
135 }
136 }
137
138 pub fn find(&self, mut p: f32) -> (usize, f32) {
151 let total = self.total();
152 assert!(total > 0.0, "SumTree::find: total priority is zero; nothing to sample from");
153
154 if p < 0.0 {
158 p = 0.0;
159 }
160 if p > total {
161 p = total;
162 }
163
164 let mut idx = 0usize;
165 let leaf_start = self.leaf_offset();
166 while idx < leaf_start {
167 let left = 2 * idx + 1;
168 let right = left + 1;
169 let left_val = self.nodes[left];
170 if p <= left_val {
171 idx = left;
172 } else {
173 p -= left_val;
174 idx = right;
175 }
176 }
177 let leaf_idx = idx - leaf_start;
178 (leaf_idx, self.nodes[idx])
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_new_zero_total() {
188 let tree = SumTree::new(8);
189 assert_eq!(tree.capacity(), 8);
190 assert_eq!(tree.total(), 0.0);
191 assert_eq!(tree.max_leaf(), 0.0);
192 }
193
194 #[test]
195 fn test_update_propagates_to_root() {
196 let mut tree = SumTree::new(4);
197 tree.update(0, 1.0);
198 tree.update(1, 2.0);
199 tree.update(2, 3.0);
200 tree.update(3, 4.0);
201 assert!((tree.total() - 10.0).abs() < 1e-6);
202 assert!((tree.max_leaf() - 4.0).abs() < 1e-6);
203 assert!((tree.leaf_priority(0) - 1.0).abs() < 1e-6);
204 assert!((tree.leaf_priority(3) - 4.0).abs() < 1e-6);
205 }
206
207 #[test]
208 fn test_update_in_place_changes_total() {
209 let mut tree = SumTree::new(4);
210 tree.update(0, 1.0);
211 tree.update(1, 2.0);
212 tree.update(2, 3.0);
213 tree.update(3, 4.0);
214 tree.update(1, 5.0);
216 assert!((tree.total() - 13.0).abs() < 1e-6);
217 assert!((tree.leaf_priority(1) - 5.0).abs() < 1e-6);
218 }
219
220 #[test]
221 fn test_find_walks_to_correct_leaf() {
222 let mut tree = SumTree::new(4);
228 tree.update(0, 1.0);
229 tree.update(1, 2.0);
230 tree.update(2, 3.0);
231 tree.update(3, 4.0);
232
233 assert_eq!(tree.find(0.5).0, 0);
234 assert_eq!(tree.find(1.0).0, 0);
235 assert_eq!(tree.find(1.5).0, 1);
236 assert_eq!(tree.find(3.0).0, 1);
237 assert_eq!(tree.find(3.1).0, 2);
238 assert_eq!(tree.find(6.0).0, 2);
239 assert_eq!(tree.find(6.5).0, 3);
240 assert_eq!(tree.find(10.0).0, 3);
241 }
242
243 #[test]
244 fn test_find_skips_zero_priority_leaves() {
245 let mut tree = SumTree::new(4);
248 tree.update(1, 5.0);
249 tree.update(3, 3.0);
250
251 let total = tree.total();
252 assert!((total - 8.0).abs() < 1e-6);
253
254 for p in [0.1, 1.0, 4.9, 5.0] {
255 assert_eq!(tree.find(p).0, 1, "p={} should hit leaf 1", p);
256 }
257 for p in [5.1, 6.0, 8.0] {
258 assert_eq!(tree.find(p).0, 3, "p={} should hit leaf 3", p);
259 }
260 }
261
262 #[test]
263 fn test_find_clamps_overshoot() {
264 let mut tree = SumTree::new(4);
268 tree.update(0, 1.0);
269 tree.update(1, 1.0);
270 let (leaf, _) = tree.find(2.0 + 1e-3);
271 assert!(leaf <= 1, "expected leaf ≤ 1 (rightmost non-zero), got {}", leaf);
272 }
273
274 #[test]
275 fn test_max_leaf_tracks_updates() {
276 let mut tree = SumTree::new(4);
277 assert_eq!(tree.max_leaf(), 0.0);
278 tree.update(0, 1.0);
279 tree.update(1, 2.5);
280 tree.update(2, 0.5);
281 assert!((tree.max_leaf() - 2.5).abs() < 1e-6);
282 tree.update(1, 0.1);
283 assert!((tree.max_leaf() - 1.0).abs() < 1e-6);
284 }
285
286 #[test]
287 #[should_panic(expected = "capacity must be > 0")]
288 fn test_zero_capacity_panics() {
289 let _ = SumTree::new(0);
290 }
291
292 #[test]
293 #[should_panic(expected = "leaf_idx")]
294 fn test_update_out_of_range_panics() {
295 let mut tree = SumTree::new(4);
296 tree.update(4, 1.0);
297 }
298
299 #[test]
300 #[should_panic(expected = "priority must be finite")]
301 fn test_update_nan_panics() {
302 let mut tree = SumTree::new(4);
303 tree.update(0, f32::NAN);
304 }
305
306 #[test]
307 #[should_panic(expected = "priority must be finite")]
308 fn test_update_negative_panics() {
309 let mut tree = SumTree::new(4);
310 tree.update(0, -1.0);
311 }
312
313 #[test]
314 #[should_panic(expected = "total priority is zero")]
315 fn test_find_zero_total_panics() {
316 let tree = SumTree::new(4);
317 let _ = tree.find(0.5);
318 }
319}