Skip to main content

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}