Skip to main content

antecedent_data/
network.rs

1//! Unit-level tabular data with a fixed interference network.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::sync::Arc;
6
7use crate::{DataError, TableView, TabularData};
8
9/// Directed weighted edge from a source unit to an exposed target unit.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct NetworkEdge {
12    /// Source unit row.
13    pub from: u32,
14    /// Target unit row.
15    pub to: u32,
16    /// Non-negative exposure weight.
17    pub weight: f64,
18}
19
20/// Tabular unit data paired with a fixed, row-indexed network.
21#[derive(Clone, Debug)]
22pub struct NetworkData {
23    units: TabularData,
24    edges: Arc<[NetworkEdge]>,
25    incoming: Arc<[Arc<[NetworkEdge]>]>,
26}
27
28impl NetworkData {
29    /// Build a network, validating row indexes, weights, self-edges, and duplicates.
30    ///
31    /// # Errors
32    ///
33    /// [`DataError::InvalidArgument`] when an edge is invalid for the unit table.
34    pub fn try_new(
35        units: TabularData,
36        edges: impl Into<Arc<[NetworkEdge]>>,
37    ) -> Result<Self, DataError> {
38        let edges = edges.into();
39        let n = units.row_count();
40        let mut sorted = edges.to_vec();
41        sorted.sort_by_key(|edge| (edge.from, edge.to));
42        for (i, edge) in sorted.iter().enumerate() {
43            if edge.from as usize >= n || edge.to as usize >= n {
44                return Err(DataError::InvalidArgument {
45                    message: "network edge row index is outside the unit table".into(),
46                });
47            }
48            if edge.from == edge.to {
49                return Err(DataError::InvalidArgument {
50                    message: "network self-edges are not allowed".into(),
51                });
52            }
53            if !edge.weight.is_finite() || edge.weight < 0.0 {
54                return Err(DataError::InvalidArgument {
55                    message: "network weights must be finite and non-negative".into(),
56                });
57            }
58            if i > 0 && (sorted[i - 1].from, sorted[i - 1].to) == (edge.from, edge.to) {
59                return Err(DataError::InvalidArgument {
60                    message: "duplicate network edge".into(),
61                });
62            }
63        }
64        let edges: Arc<[NetworkEdge]> = sorted.into();
65        let mut incoming = vec![Vec::new(); n];
66        for edge in edges.iter().copied() {
67            incoming[edge.to as usize].push(edge);
68        }
69        let incoming =
70            incoming.into_iter().map(Arc::<[NetworkEdge]>::from).collect::<Vec<_>>().into();
71        Ok(Self { units, edges, incoming })
72    }
73
74    /// Borrow unit-level columns.
75    #[must_use]
76    pub const fn units(&self) -> &TabularData {
77        &self.units
78    }
79
80    /// Borrow all directed network edges.
81    #[must_use]
82    pub fn edges(&self) -> &[NetworkEdge] {
83        &self.edges
84    }
85
86    /// Incoming neighbors whose assignments define exposure for `unit`.
87    ///
88    /// # Errors
89    ///
90    /// [`DataError::InvalidArgument`] when `unit` is outside the table.
91    pub fn incoming(&self, unit: usize) -> Result<&[NetworkEdge], DataError> {
92        self.incoming.get(unit).map(AsRef::as_ref).ok_or(DataError::InvalidArgument {
93            message: "network unit index is out of range".into(),
94        })
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn empty_network_is_valid_and_has_no_incoming_edges() {
104        let values = [1.0, 2.0];
105        let table = TabularData::from_f64_columns([("y", values.as_slice())]).unwrap();
106        let network = NetworkData::try_new(table, []).unwrap();
107        assert!(network.incoming(0).unwrap().is_empty());
108    }
109}