Skip to main content

systile/
graph.rs

1//! `TensorGraph` — a directed weighted graph whose algorithms are matrix powers.
2//!
3//! The adjacency matrix is stored as a [`PaddedTileLattice`], and classic graph
4//! questions are answered by raising it to a power over the right semiring
5//! ([`crate::semiring`]):
6//!
7//! - **Reachability / transitive closure**: boolean matrix powers.
8//! - **All-pairs shortest paths**: tropical (min-plus) matrix powers.
9//! - **Walk counting**: ordinary matrix powers.
10//!
11//! Each "power" is computed by repeated squaring, so a graph of `n` nodes needs
12//! only `⌈log₂ n⌉` dense matmuls — turning a traversal into a handful of GEMMs.
13
14use crate::geometry::Geometry;
15use crate::lattice::PaddedTileLattice;
16use crate::semiring::{semiring_matmul, Boolean, Counting, Semiring, Tropical};
17
18/// A directed, weighted graph backed by a dense adjacency lattice.
19#[derive(Clone)]
20pub struct TensorGraph {
21    n: usize,
22    geom: Geometry,
23    edges: Vec<(usize, usize, f32)>,
24}
25
26impl TensorGraph {
27    /// Create an `n`-node graph with no edges.
28    pub fn new(n: usize) -> Self {
29        TensorGraph {
30            n,
31            geom: Geometry::TPU_V,
32            edges: Vec::new(),
33        }
34    }
35
36    /// Number of nodes.
37    #[inline]
38    pub fn nodes(&self) -> usize {
39        self.n
40    }
41
42    /// Number of edges added.
43    #[inline]
44    pub fn edge_count(&self) -> usize {
45        self.edges.len()
46    }
47
48    /// Add a weighted directed edge `u → v`.
49    pub fn add_edge(&mut self, u: usize, v: usize, weight: f32) {
50        assert!(u < self.n && v < self.n, "endpoints must be in range");
51        self.edges.push((u, v, weight));
52    }
53
54    /// Add an unweighted directed edge `u → v` (weight `1`).
55    pub fn add_edge_unweighted(&mut self, u: usize, v: usize) {
56        self.add_edge(u, v, 1.0);
57    }
58
59    /// Build the boolean adjacency lattice (`1` where an edge exists), with the
60    /// diagonal set so the closure is reflexive.
61    fn reflexive_boolean(&self) -> PaddedTileLattice<f32> {
62        let mut dense = vec![0.0f32; self.n * self.n];
63        for i in 0..self.n {
64            dense[i * self.n + i] = 1.0;
65        }
66        for &(u, v, _) in &self.edges {
67            dense[u * self.n + v] = 1.0;
68        }
69        PaddedTileLattice::from_dense(self.n, self.n, &dense, self.geom).unwrap()
70    }
71
72    /// Build the tropical adjacency lattice: edge weights off-diagonal, `0` on the
73    /// diagonal, `+∞` where there is no edge.
74    fn tropical_adjacency(&self) -> PaddedTileLattice<f32> {
75        let mut dense = vec![f32::INFINITY; self.n * self.n];
76        for i in 0..self.n {
77            dense[i * self.n + i] = 0.0;
78        }
79        for &(u, v, w) in &self.edges {
80            let slot = &mut dense[u * self.n + v];
81            *slot = slot.min(w);
82        }
83        PaddedTileLattice::from_dense(self.n, self.n, &dense, self.geom).unwrap()
84    }
85
86    /// Build the `0/1` adjacency lattice with no self-loops, for walk counting.
87    fn plain_adjacency(&self) -> PaddedTileLattice<f32> {
88        let mut dense = vec![0.0f32; self.n * self.n];
89        for &(u, v, _) in &self.edges {
90            dense[u * self.n + v] = 1.0;
91        }
92        PaddedTileLattice::from_dense(self.n, self.n, &dense, self.geom).unwrap()
93    }
94
95    /// Number of squarings needed to reach any path length up to `n`.
96    fn squarings(&self) -> u32 {
97        // ⌈log₂(max(1, n))⌉
98        let mut steps = 0;
99        let mut reach = 1usize;
100        while reach < self.n.max(1) {
101            reach *= 2;
102            steps += 1;
103        }
104        steps
105    }
106
107    /// Raise `m` to a closure power over semiring `S` by repeated squaring.
108    fn close<S: Semiring>(&self, mut m: PaddedTileLattice<f32>) -> PaddedTileLattice<f32> {
109        for _ in 0..self.squarings() {
110            m = semiring_matmul::<S>(&m, &m).unwrap();
111        }
112        m
113    }
114
115    /// The reflexive-transitive closure: `reachable[u][v]` is `1` iff `v` is
116    /// reachable from `u`. Computed as boolean matrix powers.
117    pub fn reachability(&self) -> PaddedTileLattice<f32> {
118        self.close::<Boolean>(self.reflexive_boolean())
119    }
120
121    /// True if `v` is reachable from `u` (including `u == v`).
122    pub fn reachable(&self, u: usize, v: usize) -> bool {
123        *self.reachability().get(u, v).unwrap() != 0.0
124    }
125
126    /// All-pairs shortest path distances, computed as tropical matrix powers.
127    /// Unreachable pairs hold `+∞`.
128    pub fn shortest_paths(&self) -> PaddedTileLattice<f32> {
129        self.close::<Tropical>(self.tropical_adjacency())
130    }
131
132    /// The shortest-path distance from `u` to `v`, or `None` if unreachable.
133    pub fn distance(&self, u: usize, v: usize) -> Option<f32> {
134        let d = *self.shortest_paths().get(u, v).unwrap();
135        if d.is_finite() {
136            Some(d)
137        } else {
138            None
139        }
140    }
141
142    /// The number of distinct walks of exactly `k` edges from `u` to `v`, as the
143    /// `k`-th ordinary matrix power of the adjacency.
144    pub fn walk_counts(&self, k: usize) -> PaddedTileLattice<f32> {
145        let adj = self.plain_adjacency();
146        if k == 0 {
147            // The 0th power is the identity.
148            let mut dense = vec![0.0f32; self.n * self.n];
149            for i in 0..self.n {
150                dense[i * self.n + i] = 1.0;
151            }
152            return PaddedTileLattice::from_dense(self.n, self.n, &dense, self.geom).unwrap();
153        }
154        let mut acc = adj.clone();
155        for _ in 1..k {
156            acc = semiring_matmul::<Counting>(&acc, &adj).unwrap();
157        }
158        acc
159    }
160}
161
162impl core::fmt::Debug for TensorGraph {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        write!(
165            f,
166            "TensorGraph {{ nodes: {}, edges: {} }}",
167            self.n,
168            self.edges.len()
169        )
170    }
171}