igraph/misc/psumtree.rs
1//! Partial prefix-sum trees (`igraph_psumtree.h`).
2
3use crate::{
4 error::{Error, Result},
5 ffi::*,
6 igraph_call,
7};
8use std::mem::MaybeUninit;
9
10/// A partial prefix-sum tree (`igraph_psumtree_t`): a fixed number of items,
11/// each with a non-negative weight, supporting weight updates and
12/// *weighted sampling* in `O(log n)`.
13///
14/// It is the data structure igraph uses internally to draw vertices with
15/// probability proportional to changing weights (e.g. in preferential
16/// attachment models or in the [SIR simulation](crate::Graph::sir)).
17/// [`search`](PsumTree::search) maps a number in `[0, sum)` to the item
18/// whose cumulative weight interval contains it, so feeding it uniform
19/// random numbers samples items proportionally to their weights; this is
20/// what [`sample`](PsumTree::sample) does.
21///
22/// For one-off sampling from a fixed distribution, the functions of
23/// [`crate::rng`] are simpler; the tree pays off when weights change
24/// between draws.
25///
26/// Binds the `igraph_psumtree_*` functions, see the
27/// [igraph manual](https://igraph.org/c/html/latest/igraph-Data-structures.html#igraph_psumtree_init).
28///
29/// # Examples
30///
31/// ```
32/// use igraph::misc::PsumTree;
33///
34/// let mut tree = PsumTree::from_weights(&[1.0, 0.0, 3.0])?;
35/// assert_eq!(tree.sum(), 4.0);
36/// // [0, 1) -> item 0; [1, 4) -> item 2 (item 1 has no weight)
37/// assert_eq!(tree.search(0.5)?, 0);
38/// assert_eq!(tree.search(1.0)?, 2);
39/// tree.update(1, 10.0)?;
40/// assert_eq!(tree.search(1.0)?, 1);
41/// assert_eq!(tree.get(1), Some(10.0));
42/// # Ok::<(), igraph::Error>(())
43/// ```
44pub type PsumTree = igraph_psumtree_t;
45
46impl igraph_psumtree_t {
47 /// Creates a tree with `size` items, all of weight zero
48 /// (`igraph_psumtree_init`).
49 ///
50 /// # Errors
51 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `size`
52 /// is zero.
53 pub fn new(size: usize) -> Result<Self> {
54 if size == 0 {
55 return Err(Error::invalid(
56 "a prefix-sum tree must have at least one item",
57 ));
58 }
59 let size = igraph_int_t::try_from(size)
60 .map_err(|_| Error::invalid("too many items for a prefix-sum tree"))?;
61 crate::error::ensure_init();
62 let mut raw = MaybeUninit::<Self>::zeroed();
63 igraph_call!(igraph_psumtree_init(raw.as_mut_ptr(), size))?;
64 Ok(unsafe { raw.assume_init() })
65 }
66
67 /// Creates a tree whose items have the given weights.
68 ///
69 /// # Errors
70 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
71 /// `weights` is empty, or a weight is negative or not finite.
72 pub fn from_weights(weights: &[f64]) -> Result<Self> {
73 let mut tree = Self::new(weights.len())?;
74 for (i, &w) in weights.iter().enumerate() {
75 tree.update(i, w)?;
76 }
77 Ok(tree)
78 }
79
80 /// The number of items (`igraph_psumtree_size`).
81 pub fn len(&self) -> usize {
82 unsafe { igraph_psumtree_size(self) as usize }
83 }
84
85 /// Whether the tree has no items (never true for a successfully created tree).
86 pub fn is_empty(&self) -> bool {
87 self.len() == 0
88 }
89
90 /// The weight of item `index`, or `None` if out of bounds
91 /// (`igraph_psumtree_get`). `O(1)`.
92 pub fn get(&self, index: usize) -> Option<f64> {
93 (index < self.len()).then(|| unsafe { igraph_psumtree_get(self, index as igraph_int_t) })
94 }
95
96 /// The weights of all the items, in order.
97 pub fn weights(&self) -> Vec<f64> {
98 (0..self.len())
99 .map(|i| unsafe { igraph_psumtree_get(self, i as igraph_int_t) })
100 .collect()
101 }
102
103 /// The total weight of the items (`igraph_psumtree_sum`). `O(1)`.
104 pub fn sum(&self) -> f64 {
105 unsafe { igraph_psumtree_sum(self) }
106 }
107
108 /// Sets the weight of item `index` (`igraph_psumtree_update`). `O(log n)`.
109 ///
110 /// # Errors
111 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
112 /// `index` is out of bounds or `weight` is negative, infinite or NaN.
113 pub fn update(&mut self, index: usize, weight: f64) -> Result<()> {
114 if index >= self.len() {
115 return Err(Error::invalid(format!(
116 "item {index} out of bounds for a tree of {} items",
117 self.len()
118 )));
119 }
120 igraph_call!(igraph_psumtree_update(self, index as igraph_int_t, weight))
121 }
122
123 /// Resets all the weights to zero (`igraph_psumtree_reset`).
124 pub fn reset(&mut self) {
125 unsafe { igraph_psumtree_reset(self) }
126 }
127
128 /// Finds the item whose cumulative weight interval contains `value`
129 /// (`igraph_psumtree_search`). `O(log n)`.
130 ///
131 /// Precisely, it returns the lowest index `i` such that the total weight
132 /// of the items before `i` is `<= value` and adding the weight of `i`
133 /// makes it `> value`. Items of weight zero are therefore never
134 /// returned.
135 ///
136 /// # Errors
137 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) unless
138 /// `0 <= value < self.sum()` (in particular when all weights are zero).
139 pub fn search(&self, value: f64) -> Result<usize> {
140 let sum = self.sum();
141 if !(value >= 0.0 && value < sum) {
142 return Err(Error::invalid(format!(
143 "search value {value} is outside of [0, {sum})"
144 )));
145 }
146 let mut idx: igraph_int_t = 0;
147 igraph_call!(igraph_psumtree_search(self, &mut idx, value))?;
148 // Guard against round-off landing on the zero padding of the tree.
149 let idx = usize::try_from(idx).unwrap_or(usize::MAX);
150 if idx >= self.len() {
151 return Err(Error::invalid(format!(
152 "search value {value} is too close to the total weight {sum}"
153 )));
154 }
155 Ok(idx)
156 }
157
158 /// Draws an item with probability proportional to its weight, using the
159 /// thread's default random number generator; `None` if all weights are
160 /// zero. `O(log n)`.
161 ///
162 /// # Examples
163 ///
164 /// ```
165 /// use igraph::{misc::PsumTree, prelude::*};
166 ///
167 /// rng::seed(3)?;
168 /// let tree = PsumTree::from_weights(&[0.0, 1.0, 0.0])?;
169 /// assert!((0..100).all(|_| tree.sample() == Some(1)));
170 /// # Ok::<(), igraph::Error>(())
171 /// ```
172 pub fn sample(&self) -> Option<usize> {
173 let sum = self.sum();
174 if sum.is_nan() || sum <= 0.0 {
175 return None;
176 }
177 // Retry in the (extremely unlikely) case of round-off at the upper end.
178 for _ in 0..64 {
179 if let Ok(i) = self.search(crate::rng::uniform(0.0, sum)) {
180 return Some(i);
181 }
182 }
183 (0..self.len())
184 .rev()
185 .find(|&i| self.get(i).is_some_and(|w| w > 0.0))
186 }
187}
188
189impl Drop for igraph_psumtree_t {
190 fn drop(&mut self) {
191 // `igraph_psumtree_destroy` frees the inner vector and nulls its
192 // storage pointer, so the drop glue of the `v` field is then a no-op.
193 if !self.v.stor_begin.is_null() {
194 unsafe { igraph_psumtree_destroy(self) };
195 }
196 }
197}
198
199impl Clone for igraph_psumtree_t {
200 fn clone(&self) -> Self {
201 Self {
202 v: self.v.clone(),
203 size: self.size,
204 offset: self.offset,
205 }
206 }
207}