1use crate::edge::Directedness;
10use crate::error::GraphError;
11use crate::graph::Graph;
12use crate::intring::IntRing;
13use sim_lib_discrete_algebra::{
14 AlgebraLimits, BoolRing, Matrix, MinPlus, SparseEntry, SparseMatrix,
15};
16use std::collections::HashMap;
17
18type ResolveResult =
20 Result<(HashMap<(usize, usize), i64>, Vec<Option<(usize, usize)>>), GraphError>;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum MultiedgePolicy {
25 ErrorOnMultiedge,
27 KeepFirst,
29 KeepLast,
31 MinWeight,
33 SumWeight,
35 CountEdges,
37}
38
39impl MultiedgePolicy {
40 fn label(self) -> &'static str {
41 match self {
42 MultiedgePolicy::ErrorOnMultiedge => "error-on-multiedge",
43 MultiedgePolicy::KeepFirst => "keep-first",
44 MultiedgePolicy::KeepLast => "keep-last",
45 MultiedgePolicy::MinWeight => "min-weight",
46 MultiedgePolicy::SumWeight => "sum-weight",
47 MultiedgePolicy::CountEdges => "count-edges",
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct GraphMatrixMap {
55 pub node_to_row: Vec<usize>,
57 pub row_to_node: Vec<usize>,
59 pub edge_to_entry: Vec<Option<(usize, usize)>>,
61 pub policy: String,
63}
64
65fn identity_map(n: usize, edge_count: usize, policy: &str) -> GraphMatrixMap {
66 GraphMatrixMap {
67 node_to_row: (0..n).collect(),
68 row_to_node: (0..n).collect(),
69 edge_to_entry: vec![None; edge_count],
70 policy: policy.to_string(),
71 }
72}
73
74fn canonical<N, W>(graph: &Graph<N, W>, s: usize, t: usize) -> (usize, usize) {
75 if graph.is_directed() {
76 (s, t)
77 } else {
78 (s.min(t), s.max(t))
79 }
80}
81
82fn checked_sum_weights(group: &[(usize, i64)]) -> Result<i64, GraphError> {
83 let mut total = 0_i64;
84 for (_, weight) in group {
85 total = total
86 .checked_add(*weight)
87 .ok_or_else(|| GraphError::WeightOverflow("parallel edge sum".to_string()))?;
88 }
89 Ok(total)
90}
91
92fn resolve<N>(graph: &Graph<N, i64>, policy: MultiedgePolicy) -> ResolveResult {
95 let mut groups: HashMap<(usize, usize), Vec<(usize, i64)>> = HashMap::new();
96 let mut edge_to_entry = vec![None; graph.edge_count()];
97 for e in &graph.edges {
98 if e.is_self_loop() {
99 continue;
100 }
101 let key = canonical(graph, e.source, e.target);
102 groups.entry(key).or_default().push((e.id, e.weight));
103 edge_to_entry[e.id] = Some((e.source, e.target));
104 }
105 let mut resolved = HashMap::new();
106 for (key, mut group) in groups {
107 group.sort_by_key(|(id, _)| *id);
108 if matches!(policy, MultiedgePolicy::ErrorOnMultiedge) && group.len() > 1 {
109 return Err(GraphError::Unsupported(
110 "multiple edges between a pair under ErrorOnMultiedge".to_string(),
111 ));
112 }
113 let value = match policy {
114 MultiedgePolicy::ErrorOnMultiedge | MultiedgePolicy::KeepFirst => group[0].1,
115 MultiedgePolicy::KeepLast => group[group.len() - 1].1,
116 MultiedgePolicy::MinWeight => group.iter().map(|(_, w)| *w).min().unwrap(),
117 MultiedgePolicy::SumWeight => checked_sum_weights(&group)?,
118 MultiedgePolicy::CountEdges => i64::try_from(group.len())
119 .map_err(|_| GraphError::WeightOverflow("parallel edge count".to_string()))?,
120 };
121 resolved.insert(key, value);
122 }
123 Ok((resolved, edge_to_entry))
124}
125
126fn directed_cells<N>(
128 graph: &Graph<N, i64>,
129 resolved: &HashMap<(usize, usize), i64>,
130) -> Vec<((usize, usize), i64)> {
131 let mut cells = Vec::new();
132 for (&(a, b), &v) in resolved {
133 cells.push(((a, b), v));
134 if !graph.is_directed() {
135 cells.push(((b, a), v));
136 }
137 }
138 cells
139}
140
141pub fn graph_to_bool_adjacency<N, W>(
144 graph: &Graph<N, W>,
145) -> Result<(Matrix<BoolRing>, GraphMatrixMap), GraphError> {
146 graph.validate()?;
147 let n = graph.node_count();
148 let mut m = Matrix::try_filled_with_limits(n, n, BoolRing(false), AlgebraLimits::default())?;
149 let undirected = !graph.is_directed();
150 let mut map = identity_map(n, graph.edge_count(), "boolean");
151 for e in &graph.edges {
152 if e.is_self_loop() {
153 continue;
154 }
155 m.data[e.source * n + e.target] = BoolRing(true);
156 map.edge_to_entry[e.id] = Some((e.source, e.target));
157 if undirected {
158 m.data[e.target * n + e.source] = BoolRing(true);
159 }
160 }
161 Ok((m, map))
162}
163
164pub fn graph_to_minplus_adjacency<N>(
167 graph: &Graph<N, i64>,
168 policy: MultiedgePolicy,
169) -> Result<(Matrix<MinPlus>, GraphMatrixMap), GraphError> {
170 graph.validate()?;
171 let n = graph.node_count();
172 let (resolved, edge_to_entry) = resolve(graph, policy)?;
173 let mut m = Matrix::try_filled_with_limits(n, n, MinPlus::Inf, AlgebraLimits::default())?;
174 for ((r, c), v) in directed_cells(graph, &resolved) {
175 m.data[r * n + c] = MinPlus::Fin(v);
176 }
177 let mut map = identity_map(n, graph.edge_count(), policy.label());
178 map.edge_to_entry = edge_to_entry;
179 Ok((m, map))
180}
181
182pub fn graph_to_sparse_adjacency<N>(
185 graph: &Graph<N, i64>,
186 policy: MultiedgePolicy,
187) -> Result<(SparseMatrix<MinPlus>, GraphMatrixMap), GraphError> {
188 graph.validate()?;
189 let n = graph.node_count();
190 let (resolved, edge_to_entry) = resolve(graph, policy)?;
191 let mut entries = Vec::new();
192 for ((r, c), v) in directed_cells(graph, &resolved) {
193 entries.push(SparseEntry {
194 row: r,
195 col: c,
196 value: MinPlus::Fin(v),
197 });
198 }
199 let sparse = SparseMatrix::from_entries(n, n, entries)
200 .map_err(|e| GraphError::Unsupported(e.to_string()))?;
201 let mut map = identity_map(n, graph.edge_count(), policy.label());
202 map.edge_to_entry = edge_to_entry;
203 Ok((sparse, map))
204}
205
206pub fn graph_to_incidence<N, W>(graph: &Graph<N, W>) -> Result<SparseMatrix<IntRing>, GraphError> {
210 graph.validate()?;
211 let n = graph.node_count();
212 let directed = graph.is_directed();
213 let mut entries = Vec::new();
214 let mut col = 0;
215 for e in &graph.edges {
216 if e.is_self_loop() {
217 continue;
218 }
219 if directed {
220 entries.push(SparseEntry {
221 row: e.source,
222 col,
223 value: IntRing(-1),
224 });
225 entries.push(SparseEntry {
226 row: e.target,
227 col,
228 value: IntRing(1),
229 });
230 } else {
231 entries.push(SparseEntry {
232 row: e.source,
233 col,
234 value: IntRing(1),
235 });
236 entries.push(SparseEntry {
237 row: e.target,
238 col,
239 value: IntRing(1),
240 });
241 }
242 col += 1;
243 }
244 SparseMatrix::from_entries(n, col, entries).map_err(|e| GraphError::Unsupported(e.to_string()))
245}
246
247pub fn graph_to_laplacian<N, W>(graph: &Graph<N, W>) -> Result<Matrix<IntRing>, GraphError> {
250 graph.validate()?;
251 let n = graph.node_count();
252 let mut m = Matrix::try_filled_with_limits(n, n, IntRing(0), AlgebraLimits::default())?;
253 let undirected = !graph.is_directed();
254 for e in &graph.edges {
255 if e.is_self_loop() {
256 continue;
257 }
258 m.data[e.source * n + e.target].0 -= 1;
259 m.data[e.source * n + e.source].0 += 1;
260 if undirected {
261 m.data[e.target * n + e.source].0 -= 1;
262 m.data[e.target * n + e.target].0 += 1;
263 }
264 }
265 Ok(m)
266}
267
268pub fn minplus_adjacency_to_graph(
271 matrix: &Matrix<MinPlus>,
272 directedness: Directedness,
273) -> Result<Graph<usize, i64>, GraphError> {
274 matrix.validate()?;
275 if !matrix.is_square() {
276 return Err(GraphError::Unsupported(
277 "adjacency must be square".to_string(),
278 ));
279 }
280 let n = matrix.rows;
281 let mut g = Graph::with_nodes((0..n).collect(), directedness);
282 let undirected = matches!(directedness, Directedness::Undirected);
283 for i in 0..n {
284 for j in 0..n {
285 if i == j {
286 continue;
287 }
288 if undirected && j < i {
289 continue;
290 }
291 if let MinPlus::Fin(w) = matrix.data[i * n + j] {
292 g.add_edge(i, j, w)?;
293 }
294 }
295 }
296 Ok(g)
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn weighted_directed() -> Graph<usize, i64> {
304 let mut g = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
305 g.add_edge(0, 1, 4).unwrap();
306 g.add_edge(1, 2, 7).unwrap();
307 g
308 }
309
310 #[test]
311 fn adjacency_round_trip_preserves_simple_graph() {
312 let g = weighted_directed();
313 let (m, _map) = graph_to_minplus_adjacency(&g, MultiedgePolicy::ErrorOnMultiedge).unwrap();
314 let back = minplus_adjacency_to_graph(&m, Directedness::Directed).unwrap();
315 let mut got: Vec<_> = back
316 .edges
317 .iter()
318 .map(|e| (e.source, e.target, e.weight))
319 .collect();
320 got.sort_unstable();
321 assert_eq!(got, vec![(0, 1, 4), (1, 2, 7)]);
322 assert!(back.is_directed());
323 }
324
325 #[test]
326 fn directedness_is_preserved() {
327 let mut g: Graph<usize, i64> = Graph::with_nodes(vec![0, 1], Directedness::Undirected);
328 g.add_edge(0, 1, 5).unwrap();
329 let (m, _) = graph_to_minplus_adjacency(&g, MultiedgePolicy::MinWeight).unwrap();
330 assert_eq!(m.data[1], MinPlus::Fin(5));
332 assert_eq!(m.data[2], MinPlus::Fin(5));
333 }
334
335 #[test]
336 fn multiedge_policies_differ() {
337 let mut g: Graph<usize, i64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
338 g.add_edge(0, 1, 3).unwrap();
339 g.add_edge(0, 1, 10).unwrap();
340 let cell = |p| graph_to_minplus_adjacency(&g, p).unwrap().0.data[1];
341 assert_eq!(cell(MultiedgePolicy::MinWeight), MinPlus::Fin(3));
342 assert_eq!(cell(MultiedgePolicy::SumWeight), MinPlus::Fin(13));
343 assert_eq!(cell(MultiedgePolicy::KeepLast), MinPlus::Fin(10));
344 assert_eq!(cell(MultiedgePolicy::CountEdges), MinPlus::Fin(2));
345 assert!(graph_to_minplus_adjacency(&g, MultiedgePolicy::ErrorOnMultiedge).is_err());
346 }
347
348 #[test]
349 fn malformed_edge_ids_fail_before_matrix_indexing() {
350 let g = Graph {
351 nodes: vec![0, 1],
352 edges: vec![crate::edge::Edge {
353 id: 3,
354 source: 0,
355 target: 1,
356 weight: 7,
357 }],
358 directedness: Directedness::Directed,
359 };
360
361 assert!(matches!(
362 graph_to_bool_adjacency(&g),
363 Err(GraphError::InvalidEdgeId {
364 index: 0,
365 id: 3,
366 len: 1,
367 })
368 ));
369 }
370
371 #[test]
372 fn sum_weight_policy_rejects_overflow() {
373 let mut g: Graph<usize, i64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
374 g.add_edge(0, 1, i64::MAX).unwrap();
375 g.add_edge(0, 1, 1).unwrap();
376
377 assert!(matches!(
378 graph_to_minplus_adjacency(&g, MultiedgePolicy::SumWeight),
379 Err(GraphError::WeightOverflow(_))
380 ));
381 }
382
383 #[test]
384 fn self_loops_are_omitted_from_bridge_matrices() {
385 let mut g: Graph<usize, i64> = Graph::with_nodes(vec![0, 1], Directedness::Undirected);
386 g.add_edge(0, 0, 9).unwrap();
387 g.add_edge(0, 1, 5).unwrap();
388
389 let (bool_adj, bool_map) = graph_to_bool_adjacency(&g).unwrap();
390 assert_eq!(bool_adj.data[0], BoolRing(false));
391 assert_eq!(bool_adj.data[1], BoolRing(true));
392 assert_eq!(bool_adj.data[2], BoolRing(true));
393 assert_eq!(bool_map.edge_to_entry[0], None);
394 assert_eq!(bool_map.edge_to_entry[1], Some((0, 1)));
395
396 let (min_adj, min_map) =
397 graph_to_minplus_adjacency(&g, MultiedgePolicy::MinWeight).unwrap();
398 assert_eq!(min_adj.data[0], MinPlus::Inf);
399 assert_eq!(min_adj.data[1], MinPlus::Fin(5));
400 assert_eq!(min_adj.data[2], MinPlus::Fin(5));
401 assert_eq!(min_map.edge_to_entry[0], None);
402
403 let lap = graph_to_laplacian(&g).unwrap();
404 assert_eq!(lap.data[0].0, 1);
405 assert_eq!(lap.data[1].0, -1);
406 assert_eq!(lap.data[2].0, -1);
407 assert_eq!(lap.data[3].0, 1);
408 }
409
410 #[test]
411 fn laplacian_rows_sum_to_zero() {
412 let mut g: Graph<usize, i64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Undirected);
414 g.add_edge(0, 1, 1).unwrap();
415 g.add_edge(1, 2, 1).unwrap();
416 g.add_edge(0, 2, 1).unwrap();
417 let l = graph_to_laplacian(&g).unwrap();
418 for r in 0..3 {
419 let sum: i64 = (0..3).map(|c| l.data[r * 3 + c].0).sum();
420 assert_eq!(sum, 0, "row {r}");
421 }
422 assert_eq!(l.data[0].0, 2);
424 }
425
426 #[test]
427 fn incidence_columns_have_expected_signs() {
428 let g = weighted_directed();
429 let inc = graph_to_incidence(&g).unwrap();
430 let col0: Vec<_> = inc.entries.iter().filter(|e| e.col == 0).collect();
432 assert!(col0.iter().any(|e| e.row == 0 && e.value == IntRing(-1)));
433 assert!(col0.iter().any(|e| e.row == 1 && e.value == IntRing(1)));
434 }
435}