1use crate::core::{Graph, IgraphResult};
11
12pub fn constraint(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
49 let n = graph.vcount();
50 let n_us = n as usize;
51
52 if let Some(w) = weights {
53 if w.len() != graph.ecount() {
54 return Err(crate::core::IgraphError::InvalidArgument(
55 "weight vector length must equal edge count".to_string(),
56 ));
57 }
58 }
59
60 let strength = compute_strength(graph, weights)?;
61 let mut result = vec![0.0_f64; n_us];
62 let mut contrib = vec![0.0_f64; n_us];
63
64 for i in 0..n {
65 let i_us = i as usize;
66 let in_edges = graph.incident_in(i)?;
67 let out_edges = graph.incident(i)?;
68
69 if in_edges.is_empty() && out_edges.is_empty() {
70 result[i_us] = f64::NAN;
71 continue;
72 }
73
74 clear_contrib(graph, i, &in_edges, &mut contrib)?;
75 if graph.is_directed() {
76 clear_contrib(graph, i, &out_edges, &mut contrib)?;
77 }
78
79 let deg_i = strength[i_us];
80
81 add_direct(graph, i, &in_edges, weights, deg_i, &mut contrib)?;
82 if graph.is_directed() {
83 add_direct(graph, i, &out_edges, weights, deg_i, &mut contrib)?;
84 }
85
86 add_indirect(graph, i, &in_edges, weights, &strength, deg_i, &mut contrib)?;
87 if graph.is_directed() {
88 add_indirect(
89 graph,
90 i,
91 &out_edges,
92 weights,
93 &strength,
94 deg_i,
95 &mut contrib,
96 )?;
97 }
98
99 result[i_us] += square_and_clear(graph, i, &in_edges, &mut contrib)?;
100 if graph.is_directed() {
101 result[i_us] += square_and_clear(graph, i, &out_edges, &mut contrib)?;
102 }
103 }
104
105 Ok(result)
106}
107
108fn clear_contrib(graph: &Graph, i: u32, edges: &[u32], contrib: &mut [f64]) -> IgraphResult<()> {
109 for &eid in edges {
110 let j = graph.edge_other(eid, i)?;
111 contrib[j as usize] = 0.0;
112 }
113 Ok(())
114}
115
116fn add_direct(
117 graph: &Graph,
118 i: u32,
119 edges: &[u32],
120 weights: Option<&[f64]>,
121 deg_i: f64,
122 contrib: &mut [f64],
123) -> IgraphResult<()> {
124 for &eid in edges {
125 let j = graph.edge_other(eid, i)?;
126 if i != j {
127 contrib[j as usize] += edge_weight(weights, eid) / deg_i;
128 }
129 }
130 Ok(())
131}
132
133fn add_indirect(
134 graph: &Graph,
135 i: u32,
136 edges: &[u32],
137 weights: Option<&[f64]>,
138 strength: &[f64],
139 deg_i: f64,
140 contrib: &mut [f64],
141) -> IgraphResult<()> {
142 for &eid in edges {
143 let j = graph.edge_other(eid, i)?;
144 if i == j {
145 continue;
146 }
147 let w_ij = edge_weight(weights, eid);
148 let deg_j = strength[j as usize];
149 let factor = w_ij / (deg_i * deg_j);
150 let j_in = graph.incident_in(j)?;
151 for &eid2 in &j_in {
152 let q = graph.edge_other(eid2, j)?;
153 if j != q {
154 contrib[q as usize] += factor * edge_weight(weights, eid2);
155 }
156 }
157 if graph.is_directed() {
158 let j_out = graph.incident(j)?;
159 for &eid2 in &j_out {
160 let q = graph.edge_other(eid2, j)?;
161 if j != q {
162 contrib[q as usize] += factor * edge_weight(weights, eid2);
163 }
164 }
165 }
166 }
167 Ok(())
168}
169
170fn square_and_clear(
171 graph: &Graph,
172 i: u32,
173 edges: &[u32],
174 contrib: &mut [f64],
175) -> IgraphResult<f64> {
176 let mut sum = 0.0;
177 for &eid in edges {
178 let j = graph.edge_other(eid, i)?;
179 if i != j {
180 sum += contrib[j as usize] * contrib[j as usize];
181 contrib[j as usize] = 0.0;
182 }
183 }
184 Ok(sum)
185}
186
187fn edge_weight(weights: Option<&[f64]>, eid: u32) -> f64 {
188 weights.map_or(1.0, |w| w[eid as usize])
189}
190
191fn compute_strength(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
194 let n = graph.vcount();
195 let n_us = n as usize;
196 let mut strength = vec![0.0_f64; n_us];
197
198 for v in 0..n {
199 let v_us = v as usize;
200 let in_edges = graph.incident_in(v)?;
201 for &eid in &in_edges {
202 let other = graph.edge_other(eid, v)?;
203 if other != v {
204 strength[v_us] += edge_weight(weights, eid);
205 }
206 }
207 if graph.is_directed() {
208 let out_edges = graph.incident(v)?;
209 for &eid in &out_edges {
210 let other = graph.edge_other(eid, v)?;
211 if other != v {
212 strength[v_us] += edge_weight(weights, eid);
213 }
214 }
215 }
216 }
217
218 Ok(strength)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::create;
225
226 fn approx_eq(a: f64, b: f64) -> bool {
227 (a - b).abs() < 1e-9
228 }
229
230 #[test]
231 fn empty_graph() {
232 let g = Graph::with_vertices(0);
233 let c = constraint(&g, None).expect("ok");
234 assert!(c.is_empty());
235 }
236
237 #[test]
238 fn isolated_vertex_is_nan() {
239 let g = Graph::with_vertices(3);
240 let c = constraint(&g, None).expect("ok");
241 assert!(c[0].is_nan());
242 assert!(c[1].is_nan());
243 assert!(c[2].is_nan());
244 }
245
246 #[test]
247 fn single_edge() {
248 let g = create(&[(0, 1)], 2, false).expect("ok");
250 let c = constraint(&g, None).expect("ok");
251 assert!(approx_eq(c[0], 1.0));
252 assert!(approx_eq(c[1], 1.0));
253 }
254
255 #[test]
256 fn triangle() {
257 let g = create(&[(0, 1), (1, 2), (0, 2)], 3, false).expect("ok");
264 let c = constraint(&g, None).expect("ok");
265 assert!(approx_eq(c[0], 1.125), "got {}", c[0]);
266 assert!(approx_eq(c[1], 1.125), "got {}", c[1]);
267 assert!(approx_eq(c[2], 1.125), "got {}", c[2]);
268 }
269
270 #[test]
271 fn star_4_leaves() {
272 let g = create(&[(0, 1), (0, 2), (0, 3), (0, 4)], 5, false).expect("ok");
277 let c = constraint(&g, None).expect("ok");
278 assert!(approx_eq(c[0], 0.25), "center: {}", c[0]);
279 for (i, &val) in c.iter().enumerate().skip(1) {
280 assert!(approx_eq(val, 1.0), "leaf {i}: {val}");
281 }
282 }
283
284 #[test]
285 fn path_3_vertices() {
286 let g = create(&[(0, 1), (1, 2)], 3, false).expect("ok");
292 let c = constraint(&g, None).expect("ok");
293 assert!(approx_eq(c[0], 1.0), "v0: {}", c[0]);
294 assert!(approx_eq(c[1], 0.5), "v1: {}", c[1]);
295 assert!(approx_eq(c[2], 1.0), "v2: {}", c[2]);
296 }
297
298 #[test]
299 fn self_loop_excluded() {
300 let g = create(&[(0, 0), (0, 1)], 2, false).expect("ok");
302 let c = constraint(&g, None).expect("ok");
303 assert!(approx_eq(c[0], 1.0), "v0: {}", c[0]);
305 assert!(approx_eq(c[1], 1.0), "v1: {}", c[1]);
306 }
307
308 #[test]
309 fn weighted_single_edge() {
310 let g = create(&[(0, 1)], 2, false).expect("ok");
311 let c = constraint(&g, Some(&[3.0])).expect("ok");
312 assert!(approx_eq(c[0], 1.0));
313 assert!(approx_eq(c[1], 1.0));
314 }
315
316 #[test]
317 fn weighted_path_3() {
318 let g = create(&[(0, 1), (1, 2)], 3, false).expect("ok");
325 let c = constraint(&g, Some(&[1.0, 3.0])).expect("ok");
326 assert!(approx_eq(c[0], 1.0), "v0: {}", c[0]);
327 assert!(approx_eq(c[1], 0.625), "v1: {}", c[1]);
328 assert!(approx_eq(c[2], 1.0), "v2: {}", c[2]);
329 }
330
331 #[test]
332 fn invalid_weight_length() {
333 let g = create(&[(0, 1)], 2, false).expect("ok");
334 let r = constraint(&g, Some(&[1.0, 2.0]));
335 assert!(r.is_err());
336 }
337
338 #[test]
339 fn mixed_connected_isolated() {
340 let g = create(&[(0, 1)], 3, false).expect("ok");
342 let c = constraint(&g, None).expect("ok");
343 assert!(approx_eq(c[0], 1.0));
344 assert!(approx_eq(c[1], 1.0));
345 assert!(c[2].is_nan());
346 }
347
348 #[test]
349 fn k4_complete() {
350 let g = create(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], 4, false).expect("ok");
357 let c = constraint(&g, None).expect("ok");
358 let expected = 25.0 / 27.0;
359 for (i, &val) in c.iter().enumerate() {
360 assert!(approx_eq(val, expected), "v{i}: {val} expected {expected}");
361 }
362 }
363
364 #[test]
365 fn directed_path() {
366 let g = create(&[(0, 1), (1, 2)], 3, true).expect("ok");
379 let c = constraint(&g, None).expect("ok");
380 assert!(approx_eq(c[0], 1.0), "v0: {}", c[0]);
381 assert!(approx_eq(c[1], 0.5), "v1: {}", c[1]);
382 assert!(approx_eq(c[2], 1.0), "v2: {}", c[2]);
383 }
384}
385
386#[cfg(all(test, feature = "proptest-harness"))]
387mod proptests {
388 use super::*;
389 use crate::create;
390 use proptest::prelude::*;
391
392 fn arb_graph(max_v: u32) -> impl Strategy<Value = Graph> {
393 (2..=max_v).prop_flat_map(|n| {
394 let max_e = (n as usize)
395 .saturating_mul(n.saturating_sub(1) as usize)
396 .min(20);
397 proptest::collection::vec((0..n, 0..n), 0..=max_e).prop_map(move |edges| {
398 let edge_tuples: Vec<(u32, u32)> = edges.into_iter().collect();
399 create(&edge_tuples, n, false).expect("arb graph")
400 })
401 })
402 }
403
404 proptest! {
405 #[test]
406 fn constraint_non_negative_or_nan(g in arb_graph(8)) {
407 let c = constraint(&g, None).expect("ok");
408 for (i, &val) in c.iter().enumerate() {
409 prop_assert!(
410 val.is_nan() || val >= 0.0,
411 "negative constraint {val} at vertex {i}"
412 );
413 }
414 }
415
416 #[test]
417 fn isolated_vertex_is_nan_prop(n in 1u32..10) {
418 let g = Graph::with_vertices(n);
419 let c = constraint(&g, None).expect("ok");
420 for (i, &val) in c.iter().enumerate() {
421 prop_assert!(val.is_nan(), "expected NaN for isolated vertex {i}, got {val}");
422 }
423 }
424
425 #[test]
426 fn unit_weights_match_unweighted(g in arb_graph(8)) {
427 let ne = g.ecount();
428 let unw = constraint(&g, None).expect("ok");
429 let w = constraint(&g, Some(&vec![1.0; ne])).expect("ok");
430 for (i, (&a, &b)) in unw.iter().zip(w.iter()).enumerate() {
431 if a.is_nan() {
432 prop_assert!(b.is_nan(), "v{i}: unweighted NaN but weighted {b}");
433 } else {
434 prop_assert!(
435 (a - b).abs() < 1e-9,
436 "v{i}: unweighted {a} != weighted {b}"
437 );
438 }
439 }
440 }
441 }
442}