Skip to main content

henad_core/
network.rs

1//! A dynamic graph represented using CSR-like rows of neighbours, with an edge list.
2//!
3//! "Rows" are the adjacency lists of each node (i.e. a row in the adjacency matrix).
4//! All rows are stored in compressed sparse row (CSR) format.
5
6use std::ops::Range;
7
8/// Rows stored in compressed sparse row (CSR) format with slack specified by [`Csr::MIN_ROW`] and [`Csr::REBUILD_SLACK`].
9///
10/// When a row is full, [`Csr::relocate`] will be called.
11#[derive(Default)]
12struct Csr {
13    /// Start index of each row. Note that `offset[i+1]-offset[i]` is not a reliable way of finding the length of a row,
14    /// due to relocations and slack. Use `len[i]` instead.
15    offset: Vec<u32>,
16    /// Length (without slack) of each row.
17    len: Vec<u32>,
18    /// Capacity (with slack) of each row.
19    capacity: Vec<u32>,
20    /// Neighbour node of each entry.
21    neighbors: Vec<u32>,
22    /// Edge index of each entry. This is used in `Network` to find the corresponding edge in the edge list.
23    edges: Vec<u32>,
24    /// Number of stale entries caused by relocations.
25    stale_count: usize,
26}
27
28impl Csr {
29    /// Minimum row size to avoid frequent rebuilding on small graphs.
30    const MIN_ROW: usize = 4;
31
32    /// Fractional slack (1/[`Self::REBUILD_SLACK`]) to leave per row when rebuilding.
33    const REBUILD_SLACK: usize = 4;
34
35    /// Initialises a CSR with `n` empty rows.
36    fn with_rows(n: usize) -> Self {
37        Self {
38            offset: vec![0; n],
39            len: vec![0; n],
40            capacity: vec![0; n],
41            ..Self::default()
42        }
43    }
44
45    /// Returns the range of indices in `neighbors` and `edges` that correspond to row `i`.
46    #[inline]
47    fn span(&self, i: u32) -> Range<usize> {
48        let start = self.offset[i as usize] as usize;
49        start..start + self.len[i as usize] as usize
50    }
51
52    /// Returns the slice of neighbour nodes for row `i`.
53    #[inline]
54    fn row(&self, i: u32) -> &[u32] {
55        &self.neighbors[self.span(i)]
56    }
57
58    /// Returns the slice of edge indices for row `i`.
59    #[inline]
60    fn row_edges(&self, i: u32) -> &[u32] {
61        &self.edges[self.span(i)]
62    }
63
64    /// Adds a new empty row at the end of the CSR.
65    ///
66    /// Note that this does not allocate space for the row.
67    /// Upon the first push, it will be relocated to a new space with capacity [`Self::MIN_ROW`].
68    fn push_row(&mut self) {
69        self.offset.push(0);
70        self.len.push(0);
71        self.capacity.push(0);
72    }
73
74    /// Moves node `i`'s row to the end with double capacity, leaving the old space as stale.
75    fn relocate(&mut self, i: u32) {
76        let span = self.span(i);
77        let i = i as usize;
78        let new_capacity = (self.capacity[i] as usize * 2).max(Self::MIN_ROW);
79        let new_offset = self.neighbors.len();
80
81        self.neighbors.extend_from_within(span.clone());
82        self.edges.extend_from_within(span);
83        self.neighbors.resize(new_offset + new_capacity, 0);
84        self.edges.resize(new_offset + new_capacity, 0);
85
86        self.stale_count += self.capacity[i] as usize;
87        self.offset[i] = new_offset as u32;
88        self.capacity[i] = new_capacity as u32;
89    }
90
91    /// Appends `neighbor`, `edge` to node `i`'s rows.
92    fn push(&mut self, i: u32, neighbor: u32, edge: u32) {
93        if self.len[i as usize] == self.capacity[i as usize] {
94            self.relocate(i);
95        }
96        let offset = self.offset[i as usize] as usize + self.len[i as usize] as usize;
97        self.neighbors[offset] = neighbor;
98        self.edges[offset] = edge;
99        self.len[i as usize] += 1;
100    }
101
102    /// Drops the entry for `edge` from node `i`'s row.
103    ///
104    /// Returns whether the edge was found and successfully removed.
105    fn remove(&mut self, i: u32, edge: u32) -> bool {
106        let Range { start, end } = self.span(i);
107        let Some(offset) = self.edges[start..end].iter().position(|&e| e == edge) else {
108            return false;
109        };
110        let offset = start + offset;
111        self.neighbors[offset] = self.neighbors[end - 1];
112        self.edges[offset] = self.edges[end - 1];
113        self.len[i as usize] -= 1;
114        true
115    }
116
117    /// Replaces `old` with `new` in the edge list of node `i`.
118    fn renumber_edge(&mut self, i: u32, old: u32, new: u32) {
119        let span = self.span(i);
120        if let Some(slot) = self.edges[span].iter_mut().find(|e| **e == old) {
121            *slot = new;
122        }
123    }
124
125    /// Removes all entries from node `i`'s row, leaving the space as stale.
126    fn clear_row(&mut self, i: u32) {
127        self.stale_count += self.capacity[i as usize] as usize;
128        self.offset[i as usize] = 0;
129        self.len[i as usize] = 0;
130        self.capacity[i as usize] = 0;
131    }
132
133    /// Packs every row from scratch, dropping every stale entry.
134    ///
135    /// `entries` is an iterator over `(row, neighbor, edge)` tuples that describe every entry in the graph.
136    fn build(&mut self, n: usize, entries: impl Iterator<Item = (u32, u32, u32)> + Clone) {
137        self.offset.clear();
138        self.offset.resize(n, 0);
139        self.len.clear();
140        self.len.resize(n, 0);
141        self.capacity.clear();
142        self.capacity.resize(n, 0);
143
144        for (row, _, _) in entries.clone() {
145            self.len[row as usize] += 1;
146        }
147
148        let mut total = 0;
149        for i in 0..n {
150            let new_offset = total;
151            let new_capacity = Self::packed_capacity(self.len[i] as usize);
152            self.capacity[i] = new_capacity as u32;
153            self.offset[i] = new_offset as u32;
154            total += new_capacity;
155        }
156
157        self.len.fill(0);
158        self.neighbors.clear();
159        self.neighbors.resize(total, 0);
160        self.edges.clear();
161        self.edges.resize(total, 0);
162        self.stale_count = 0;
163
164        for (row, neighbor, edge) in entries {
165            let i = row as usize;
166            let offset = self.offset[i] as usize + self.len[i] as usize;
167            self.neighbors[offset] = neighbor;
168            self.edges[offset] = edge;
169            self.len[i] += 1;
170        }
171    }
172
173    /// Packs every row in node order, dropping every stale entry.
174    ///
175    /// Unlike [`Self::build`], this copies each row as it is instead of rebuilding from the edge list,
176    /// so the order of entries within a row is preserved.
177    fn repack(&mut self) {
178        let n = self.len.len();
179        let total: usize = self.len.iter().map(|&len| Self::packed_capacity(len as usize)).sum();
180        let mut neighbors = Vec::with_capacity(total);
181        let mut edges = Vec::with_capacity(total);
182        for i in 0..n {
183            let span = self.span(i as u32);
184            let new_offset = neighbors.len();
185            let new_capacity = Self::packed_capacity(span.len());
186            neighbors.extend_from_slice(&self.neighbors[span.clone()]);
187            edges.extend_from_slice(&self.edges[span]);
188            neighbors.resize(new_offset + new_capacity, 0);
189            edges.resize(new_offset + new_capacity, 0);
190            self.offset[i] = new_offset as u32;
191            self.capacity[i] = new_capacity as u32;
192        }
193        self.neighbors = neighbors;
194        self.edges = edges;
195        self.stale_count = 0;
196    }
197
198    /// Returns the capacity given to a row of `len` entries when rows are packed.
199    ///
200    /// This includes slack of 1/[`Self::REBUILD_SLACK`], and is at least [`Self::MIN_ROW`].
201    fn packed_capacity(len: usize) -> usize {
202        (len + len / Self::REBUILD_SLACK).max(Self::MIN_ROW)
203    }
204
205    /// Sets the CSR empty.
206    fn clear(&mut self) {
207        self.offset.clear();
208        self.len.clear();
209        self.capacity.clear();
210        self.neighbors.clear();
211        self.edges.clear();
212        self.stale_count = 0;
213    }
214
215    fn heap_bytes(&self) -> usize {
216        (self.offset.capacity()
217            + self.len.capacity()
218            + self.capacity.capacity()
219            + self.neighbors.capacity()
220            + self.edges.capacity())
221            * size_of::<u32>()
222    }
223}
224
225/// A network of nodes and edges.
226///
227/// Rows are stored in compressed sparse row (CSR) format, with an edge list for the view to draw.
228pub struct Network {
229    occupied: Vec<bool>,
230    /// Reusable indices of retired nodes
231    free: Vec<u32>,
232    node_count: usize,
233
234    src: Vec<u32>,
235    dst: Vec<u32>,
236    color: Vec<u8>,
237
238    /// By target. For undirected graphs, this stores both directions.
239    in_csr: Csr,
240    /// By source. For undirected graphs, rows are stored in [`Self::in_csr`] instead.
241    out_csr: Csr,
242
243    directed: bool,
244
245    /// Used to detect when a view of the graph is out of date.
246    version: u64,
247}
248
249/// Prints the graph's size, not its edges.
250impl std::fmt::Debug for Network {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        f.debug_struct("Network")
253            .field("node_count", &self.node_count)
254            .field("slot_count", &self.slot_count())
255            .field("edge_count", &self.edge_count())
256            .field("directed", &self.directed)
257            .field("version", &self.version)
258            .finish_non_exhaustive()
259    }
260}
261
262impl Network {
263    /// Creates a new `Network` with `nodes` slots and no edges. All slots are considered occupied with a node.
264    pub fn new(nodes: usize, directed: bool) -> Self {
265        Self {
266            occupied: vec![true; nodes],
267            free: Vec::new(),
268            node_count: nodes,
269            src: Vec::new(),
270            dst: Vec::new(),
271            color: Vec::new(),
272            in_csr: Csr::with_rows(nodes),
273            out_csr: Csr::with_rows(if directed { nodes } else { 0 }),
274            directed,
275            version: 0,
276        }
277    }
278
279    /// Number of total slots.
280    pub fn slot_count(&self) -> usize {
281        self.occupied.len()
282    }
283
284    /// Number of nodes.
285    pub fn node_count(&self) -> usize {
286        self.node_count
287    }
288
289    /// Whether slot `i` is occupied by a node.
290    pub fn contains_node(&self, i: u32) -> bool {
291        self.occupied.get(i as usize).copied().unwrap_or(false)
292    }
293
294    /// Whether the edges are directed.
295    pub fn directed(&self) -> bool {
296        self.directed
297    }
298
299    /// Version of the graph. It goes up whenever the edges, their colours or their direction change.
300    pub fn version(&self) -> u64 {
301        self.version
302    }
303
304    /// Spawns a new node and returns its index.
305    #[doc(hidden)]
306    pub fn spawn(&mut self) -> u32 {
307        self.node_count += 1;
308        if let Some(i) = self.free.pop() {
309            self.occupied[i as usize] = true;
310            return i;
311        }
312        let i = self.occupied.len() as u32;
313        self.occupied.push(true);
314        self.in_csr.push_row();
315        if self.directed {
316            self.out_csr.push_row();
317        }
318        i
319    }
320
321    /// Retires node in slot `i`.
322    #[doc(hidden)]
323    pub fn retire(&mut self, i: u32) {
324        if !self.contains_node(i) {
325            return;
326        }
327
328        // Collected before anything moves, then removed high index first so a swap never lands on
329        // one still to come.
330        let mut doomed: Vec<u32> = self.in_csr.row_edges(i).to_vec();
331        if self.directed {
332            doomed.extend_from_slice(self.out_csr.row_edges(i));
333        }
334        doomed.sort_unstable();
335        doomed.dedup();
336        for &e in doomed.iter().rev() {
337            self.remove_edge(e);
338        }
339
340        self.in_csr.clear_row(i);
341        if self.directed {
342            self.out_csr.clear_row(i);
343        }
344        self.occupied[i as usize] = false;
345        self.node_count -= 1;
346        self.free.push(i);
347        self.version += 1;
348    }
349
350    /// Number of edges.
351    pub fn edge_count(&self) -> usize {
352        self.src.len()
353    }
354
355    /// Returns the edge list as `(src, dst, color)`.
356    pub fn edges(&self) -> (&[u32], &[u32], &[u8]) {
357        (&self.src, &self.dst, &self.color)
358    }
359
360    /// Calls `f` with the edge list and a mutable slice of the edge colours, for recolouring edges in bulk.
361    ///
362    /// `f` returns whether it changed any colour. The version is incremented only if it did,
363    /// so a recolour that changes nothing does not make the view copy the edge list again.
364    pub fn update_colors(&mut self, f: impl FnOnce(&[u32], &[u32], &mut [u8]) -> bool) {
365        if f(&self.src, &self.dst, &mut self.color) {
366            self.version += 1;
367        }
368    }
369
370    /// Sets the color of edge `edge` to `color`.
371    pub fn set_edge_color(&mut self, edge: u32, color: u8) {
372        self.color[edge as usize] = color;
373        self.version += 1;
374    }
375
376    /// Creates a new edge from `a` to `b` with color `color`, returning its index in the edge list.
377    ///
378    /// Repeated edges are allowed.
379    ///
380    /// # Panics
381    ///
382    /// Panics on debug builds if `a == b`.
383    pub fn add_edge(&mut self, a: u32, b: u32, color: u8) -> u32 {
384        debug_assert!(a != b, "a self loop has no meaning for either row set");
385        let e = self.src.len() as u32;
386        self.src.push(a);
387        self.dst.push(b);
388        self.color.push(color);
389        if self.directed {
390            self.in_csr.push(b, a, e);
391            self.out_csr.push(a, b, e);
392        } else {
393            self.in_csr.push(b, a, e);
394            self.in_csr.push(a, b, e);
395        }
396        self.version += 1;
397        e
398    }
399
400    /// Removes edge `edge` from the graph.
401    pub fn remove_edge(&mut self, edge: u32) {
402        let e = edge as usize;
403        let (a, b) = (self.src[e], self.dst[e]);
404        if self.directed {
405            self.in_csr.remove(b, edge);
406            self.out_csr.remove(a, edge);
407        } else {
408            self.in_csr.remove(b, edge);
409            self.in_csr.remove(a, edge);
410        }
411
412        self.src.swap_remove(e);
413        self.dst.swap_remove(e);
414        self.color.swap_remove(e);
415
416        // The edge that moved into this slot is still in its endpoints' rows under its old index.
417        let moved = self.src.len() as u32;
418        if e < self.src.len() {
419            let (ma, mb) = (self.src[e], self.dst[e]);
420            if self.directed {
421                self.in_csr.renumber_edge(mb, moved, edge);
422                self.out_csr.renumber_edge(ma, moved, edge);
423            } else {
424                self.in_csr.renumber_edge(mb, moved, edge);
425                self.in_csr.renumber_edge(ma, moved, edge);
426            }
427        }
428        self.version += 1;
429    }
430
431    /// Returns the nodes that have an edge to `i`.
432    #[inline]
433    pub fn in_neighbors(&self, i: u32) -> &[u32] {
434        self.in_csr.row(i)
435    }
436
437    /// Returns the nodes that `i` has an edge to.
438    #[inline]
439    pub fn out_neighbors(&self, i: u32) -> &[u32] {
440        if self.directed {
441            self.out_csr.row(i)
442        } else {
443            self.in_csr.row(i)
444        }
445    }
446
447    /// Returns the degree of the node. On directed graphs, this is the sum of in-degree and out-degree.
448    #[inline]
449    pub fn degree(&self, i: u32) -> usize {
450        if self.directed {
451            self.in_csr.row(i).len() + self.out_csr.row(i).len()
452        } else {
453            self.in_csr.row(i).len()
454        }
455    }
456
457    /// Returns the index of an edge from `a` to `b` in the edge list, if there is one.
458    ///
459    /// On undirected graphs, an edge in either direction counts, and the shorter of the two rows is searched.
460    /// On directed graphs, the out-row of `a` is searched.
461    #[inline]
462    pub fn edge_between(&self, a: u32, b: u32) -> Option<u32> {
463        let (csr, a, b) = if self.directed {
464            (&self.out_csr, a, b)
465        } else if self.degree(a) <= self.degree(b) {
466            (&self.in_csr, a, b)
467        } else {
468            (&self.in_csr, b, a)
469        };
470        let offset = csr.row(a).iter().position(|&n| n == b)?;
471        Some(csr.row_edges(a)[offset])
472    }
473
474    /// Returns whether there is an edge from `a` to `b`.
475    #[inline]
476    pub fn has_edge(&self, a: u32, b: u32) -> bool {
477        self.edge_between(a, b).is_some()
478    }
479
480    #[doc(hidden)]
481    pub fn set_directed(&mut self, directed: bool) {
482        if directed == self.directed {
483            return;
484        }
485        self.directed = directed;
486        self.rebuild();
487        self.version += 1;
488    }
489
490    /// Returns whether the graph should be repacked to reclaim space from relocated rows.
491    ///
492    /// This is true when the number of stale entries exceeds 1/2 of occupied rows and is greater than `Csr::MIN_ROW`.
493    #[doc(hidden)]
494    pub fn should_repack(&self) -> bool {
495        let stale = self.in_csr.stale_count + self.out_csr.stale_count;
496        let slots = self.in_csr.neighbors.len() + self.out_csr.neighbors.len();
497        stale > slots / 2 && stale > Csr::MIN_ROW
498    }
499
500    /// Packs the rows, reclaiming the stale space left by relocations and retirements.
501    ///
502    /// The order of entries within each row is preserved.
503    #[doc(hidden)]
504    pub fn repack(&mut self) {
505        self.in_csr.repack();
506        self.out_csr.repack();
507    }
508
509    /// Rebuilds the CSR rows from the edge list, dropping any stale entries.
510    ///
511    /// This is needed when the direction changes. To only reclaim space, use [`Self::repack`], which is cheaper.
512    #[doc(hidden)]
513    pub fn rebuild(&mut self) {
514        let n = self.occupied.len();
515        let (src, dst) = (&self.src, &self.dst);
516        // Each edge as `(src, dst, index)`, which is already the shape of an out-row entry.
517        let edges = || src.iter().zip(dst).enumerate().map(|(e, (&a, &b))| (a, b, e as u32));
518        if self.directed {
519            self.in_csr.build(n, edges().map(|(a, b, e)| (b, a, e)));
520            self.out_csr.build(n, edges());
521        } else {
522            self.in_csr
523                .build(n, edges().flat_map(|(a, b, e)| [(b, a, e), (a, b, e)]));
524            self.out_csr.clear();
525        }
526    }
527
528    /// Heap memory held by the graph, in bytes.
529    pub fn heap_bytes(&self) -> usize {
530        self.occupied.capacity()
531            + self.free.capacity() * size_of::<u32>()
532            + (self.src.capacity() + self.dst.capacity()) * size_of::<u32>()
533            + self.color.capacity()
534            + self.in_csr.heap_bytes()
535            + self.out_csr.heap_bytes()
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::Network;
542    use crate::authoring::primitives::rng::{next_bits, xorshift64};
543
544    /// Neighbours of every node, computed from the edge list alone.
545    fn brute_force(net: &Network) -> (Vec<Vec<u32>>, Vec<Vec<u32>>) {
546        let n = net.slot_count();
547        let (src, dst, _) = net.edges();
548        let mut ins = vec![Vec::new(); n];
549        let mut outs = vec![Vec::new(); n];
550        for e in 0..src.len() {
551            let (a, b) = (src[e], dst[e]);
552            ins[b as usize].push(a);
553            if net.directed() {
554                outs[a as usize].push(b);
555            } else {
556                ins[a as usize].push(b);
557            }
558        }
559        if !net.directed() {
560            outs = ins.clone();
561        }
562        (ins, outs)
563    }
564
565    fn sorted(mut v: Vec<u32>) -> Vec<u32> {
566        v.sort_unstable();
567        v
568    }
569
570    /// Asserts that every node's rows match the edge list.
571    fn assert_rows_match(net: &Network, what: &str) {
572        let (ins, outs) = brute_force(net);
573        for i in 0..net.slot_count() as u32 {
574            assert_eq!(
575                sorted(net.in_neighbors(i).to_vec()),
576                sorted(ins[i as usize].clone()),
577                "{what}: node {i} in-row disagrees with the edge list"
578            );
579            assert_eq!(
580                sorted(net.out_neighbors(i).to_vec()),
581                sorted(outs[i as usize].clone()),
582                "{what}: node {i} out-row disagrees with the edge list"
583            );
584        }
585    }
586
587    #[test]
588    fn an_undirected_edge_lands_in_both_rows() {
589        let mut net = Network::new(4, false);
590        net.add_edge(0, 3, 0);
591        assert_eq!(net.in_neighbors(0), &[3]);
592        assert_eq!(net.in_neighbors(3), &[0]);
593        assert_eq!(net.out_neighbors(0), net.in_neighbors(0), "one row answers both ways");
594        assert!(net.has_edge(0, 3) && net.has_edge(3, 0));
595        assert_eq!(net.degree(0), 1);
596    }
597
598    #[test]
599    fn a_directed_edge_lands_in_one_row_each_way() {
600        let mut net = Network::new(4, true);
601        net.add_edge(0, 3, 0);
602        assert_eq!(net.out_neighbors(0), &[3]);
603        assert!(net.in_neighbors(0).is_empty(), "0 has no edge into it");
604        assert_eq!(net.in_neighbors(3), &[0]);
605        assert!(net.out_neighbors(3).is_empty());
606        assert!(net.has_edge(0, 3), "the edge runs 0 to 3");
607        assert!(!net.has_edge(3, 0), "and not the other way");
608        assert_eq!(net.degree(0), 1);
609    }
610
611    #[test]
612    fn a_row_survives_the_relocation_its_growth_forces() {
613        let mut net = Network::new(40, false);
614        // Well past `Csr::MIN_ROW`, forcing several relocations.
615        for b in 1..40u32 {
616            net.add_edge(0, b, 0);
617        }
618        assert_eq!(sorted(net.in_neighbors(0).to_vec()), (1..40).collect::<Vec<_>>());
619        assert_rows_match(&net, "after growth");
620    }
621
622    #[test]
623    fn removing_an_edge_renumbers_the_one_that_takes_its_place() {
624        let mut net = Network::new(6, false);
625        for (a, b) in [(0, 1), (2, 3), (4, 5)] {
626            net.add_edge(a, b, 0);
627        }
628        // Edge 2 moves into index 0, and its rows must follow.
629        net.remove_edge(0);
630        assert_eq!(net.edge_count(), 2);
631        assert_rows_match(&net, "after a middle removal");
632        net.remove_edge(0);
633        assert_rows_match(&net, "after a second removal");
634        assert_eq!(net.edge_count(), 1);
635    }
636
637    #[test]
638    fn retiring_a_node_takes_every_edge_touching_it() {
639        let mut net = Network::new(6, false);
640        for b in [1, 2, 3, 4] {
641            net.add_edge(0, b, 0);
642        }
643        net.add_edge(1, 2, 0);
644        net.retire(0);
645
646        assert!(!net.contains_node(0));
647        assert_eq!(net.node_count(), 5);
648        assert_eq!(net.edge_count(), 1, "only the edge clear of node 0 is left");
649        assert_rows_match(&net, "after a retirement");
650        for i in 1..6u32 {
651            assert!(
652                !net.in_neighbors(i).contains(&0),
653                "node {i} still lists the retired node"
654            );
655        }
656    }
657
658    #[test]
659    fn retiring_a_node_takes_its_edges_when_directed_too() {
660        let mut net = Network::new(6, true);
661        net.add_edge(0, 1, 0);
662        net.add_edge(2, 0, 0);
663        net.add_edge(3, 4, 0);
664        net.retire(0);
665        assert_eq!(net.edge_count(), 1);
666        assert_rows_match(&net, "after a directed retirement");
667    }
668
669    #[test]
670    fn a_retired_slot_is_the_next_one_handed_out() {
671        let mut net = Network::new(3, false);
672        net.retire(1);
673        net.retire(2);
674        assert_eq!(net.spawn(), 2, "newest free slot first");
675        assert_eq!(net.spawn(), 1);
676        assert_eq!(net.spawn(), 3, "then a fresh one");
677        assert_eq!(net.slot_count(), 4);
678        assert_eq!(net.node_count(), 4);
679    }
680
681    #[test]
682    fn a_reused_slot_starts_with_no_edges() {
683        let mut net = Network::new(4, false);
684        net.add_edge(1, 2, 0);
685        net.add_edge(1, 3, 0);
686        net.retire(1);
687        let reused = net.spawn();
688        assert_eq!(reused, 1);
689        assert!(
690            net.in_neighbors(reused).is_empty(),
691            "the old row came back with the slot"
692        );
693        net.add_edge(reused, 2, 0);
694        assert_rows_match(&net, "after reuse");
695    }
696
697    #[test]
698    fn a_rebuild_leaves_the_same_graph() {
699        let mut net = Network::new(20, false);
700        let mut rng = 0x51A7_u64;
701        for _ in 0..60 {
702            let a = next_bits(&mut rng) % 20;
703            let b = next_bits(&mut rng) % 20;
704            if a != b && !net.has_edge(a, b) {
705                net.add_edge(a, b, 0);
706            }
707        }
708        let before: Vec<Vec<u32>> = (0..20).map(|i| sorted(net.in_neighbors(i).to_vec())).collect();
709        net.rebuild();
710        let after: Vec<Vec<u32>> = (0..20).map(|i| sorted(net.in_neighbors(i).to_vec())).collect();
711        assert_eq!(before, after, "a repack changed the neighbours");
712        assert_rows_match(&net, "after a rebuild");
713    }
714
715    #[test]
716    fn flipping_the_direction_rebuilds_both_row_sets() {
717        let mut net = Network::new(5, false);
718        net.add_edge(0, 1, 0);
719        net.add_edge(1, 2, 0);
720        net.set_directed(true);
721
722        assert!(net.directed());
723        assert_eq!(net.out_neighbors(0), &[1]);
724        assert!(net.in_neighbors(0).is_empty(), "0 has no edge into it once directed");
725        assert_rows_match(&net, "after a flip to directed");
726
727        net.set_directed(false);
728        assert_rows_match(&net, "and back");
729        assert_eq!(sorted(net.in_neighbors(1).to_vec()), vec![0, 2]);
730    }
731
732    /// Rows still match the edge list after a long run of random changes.
733    #[test]
734    fn rows_track_the_edge_list_through_random_churn() {
735        for &directed in &[false, true] {
736            let mut net = Network::new(30, directed);
737            let mut rng = xorshift64(0xC0FF_EE01 ^ u64::from(directed));
738            for round in 0..400 {
739                match next_bits(&mut rng) % 10 {
740                    0..=5 => {
741                        let a = next_bits(&mut rng) % 30;
742                        let b = next_bits(&mut rng) % 30;
743                        if a != b && net.contains_node(a) && net.contains_node(b) && !net.has_edge(a, b) {
744                            net.add_edge(a, b, 0);
745                        }
746                    }
747                    6..=7 if net.edge_count() > 0 => {
748                        let e = next_bits(&mut rng) % net.edge_count() as u32;
749                        net.remove_edge(e);
750                    }
751                    8 => {
752                        let i = next_bits(&mut rng) % net.slot_count() as u32;
753                        net.retire(i);
754                    }
755                    _ => {
756                        net.spawn();
757                    }
758                }
759                assert_rows_match(&net, &format!("directed={directed} round={round}"));
760            }
761            assert!(
762                net.edge_count() > 0,
763                "the churn removed everything, so it proved little"
764            );
765        }
766    }
767
768    #[test]
769    fn edge_between_returns_the_index_in_the_edge_list() {
770        for directed in [false, true] {
771            let mut net = Network::new(6, directed);
772            let mut rng = 0xED6E_u64;
773            for _ in 0..20 {
774                let a = next_bits(&mut rng) % 6;
775                let b = next_bits(&mut rng) % 6;
776                if a != b && !net.has_edge(a, b) {
777                    net.add_edge(a, b, 0);
778                }
779            }
780            net.remove_edge(0);
781            let (src, dst, _) = net.edges();
782            for a in 0..6u32 {
783                for b in 0..6u32 {
784                    let listed = (0..src.len())
785                        .find(|&e| (src[e], dst[e]) == (a, b) || (!directed && (src[e], dst[e]) == (b, a)));
786                    assert_eq!(
787                        net.edge_between(a, b),
788                        listed.map(|e| e as u32),
789                        "directed={directed} edge_between({a}, {b})"
790                    );
791                }
792            }
793        }
794    }
795
796    #[test]
797    fn has_edge_agrees_with_the_edge_list() {
798        let mut net = Network::new(12, false);
799        let mut rng = 0xBEEF_u64;
800        for _ in 0..30 {
801            let a = next_bits(&mut rng) % 12;
802            let b = next_bits(&mut rng) % 12;
803            if a != b && !net.has_edge(a, b) {
804                net.add_edge(a, b, 0);
805            }
806        }
807        let (src, dst, _) = net.edges();
808        let joined: Vec<(u32, u32)> = src.iter().zip(dst).map(|(&a, &b)| (a, b)).collect();
809        for a in 0..12u32 {
810            for b in 0..12u32 {
811                let listed = joined.contains(&(a, b)) || joined.contains(&(b, a));
812                assert_eq!(net.has_edge(a, b), a != b && listed, "has_edge({a}, {b})");
813            }
814        }
815    }
816
817    #[test]
818    fn the_version_moves_for_every_change_the_view_can_see() {
819        let mut net = Network::new(4, false);
820        let start = net.version();
821        let e = net.add_edge(0, 1, 0);
822        assert!(net.version() > start, "an edge appeared");
823
824        let after_add = net.version();
825        net.set_edge_color(e, 3);
826        assert!(net.version() > after_add, "a colour changed");
827
828        let after_color = net.version();
829        net.remove_edge(e);
830        assert!(net.version() > after_color, "an edge went");
831
832        let after_remove = net.version();
833        net.set_directed(true);
834        assert!(net.version() > after_remove, "the direction changed");
835    }
836
837    /// Every publish recolours the edges. A recolour that changes nothing must leave the version alone,
838    /// otherwise every publish would copy the whole edge list again.
839    #[test]
840    fn a_recolour_moves_the_version_only_when_it_changed_something() {
841        let mut net = Network::new(3, false);
842        net.add_edge(0, 1, 0);
843        net.add_edge(1, 2, 0);
844
845        let before = net.version();
846        net.update_colors(|_, _, color| {
847            color.fill(0);
848            false
849        });
850        assert_eq!(net.version(), before, "an unchanged recolour moved the version");
851
852        net.update_colors(|src, _, color| {
853            for (c, &a) in color.iter_mut().zip(src) {
854                *c = a as u8;
855            }
856            true
857        });
858        assert!(net.version() > before, "a real recolour left the version behind");
859        assert_eq!(net.edges().2, &[0, 1]);
860    }
861
862    /// Growth alone never asks for a repack. Retiring most nodes does.
863    #[test]
864    fn retiring_most_of_the_graph_asks_for_a_repack() {
865        let mut net = Network::new(40, false);
866        let mut rng = 0x9A5B_u64;
867        for _ in 0..250 {
868            let a = next_bits(&mut rng) % 40;
869            let b = next_bits(&mut rng) % 40;
870            if a != b && !net.has_edge(a, b) {
871                net.add_edge(a, b, 0);
872            }
873        }
874        assert!(!net.should_repack(), "growth alone should not ask for one");
875
876        for i in 0..38u32 {
877            net.retire(i);
878        }
879        assert!(net.should_repack(), "38 cleared rows left nothing to reclaim");
880
881        net.repack();
882        assert!(!net.should_repack(), "the repack did not reclaim it");
883        assert_rows_match(&net, "after the repack");
884    }
885
886    /// A repack copies each row instead of rebuilding it, so the order within each row is preserved.
887    #[test]
888    fn a_repack_keeps_every_row_as_it_was() {
889        let mut net = Network::new(30, true);
890        let mut rng = 0x2E9A_u64;
891        for _ in 0..200 {
892            let a = next_bits(&mut rng) % 30;
893            let b = next_bits(&mut rng) % 30;
894            if a != b && !net.has_edge(a, b) {
895                net.add_edge(a, b, 0);
896            }
897        }
898        for e in 0..40 {
899            net.remove_edge(e);
900        }
901        net.retire(3);
902        let rows = |net: &Network| -> Vec<(Vec<u32>, Vec<u32>)> {
903            (0..30)
904                .map(|i| (net.in_neighbors(i).to_vec(), net.out_neighbors(i).to_vec()))
905                .collect()
906        };
907        let before = rows(&net);
908        net.repack();
909        assert_eq!(rows(&net), before, "a repack reordered or lost a row");
910        assert_rows_match(&net, "after a repack");
911
912        net.add_edge(3, 4, 0);
913        assert_rows_match(&net, "after a repack and an insert");
914    }
915}