1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
14
15use crate::core::{Graph, IgraphError, IgraphResult};
16
17pub fn dirichlet_energy(
45 graph: &Graph,
46 signal: &[f64],
47 weights: Option<&[f64]>,
48) -> IgraphResult<f64> {
49 validate_inputs(graph, signal, weights)?;
50
51 let mut energy = 0.0_f64;
52 for (eid, (u, v)) in graph.edges().enumerate() {
53 let w = weights.map_or(1.0, |ws| ws[eid]);
54 let diff = signal[u as usize] - signal[v as usize];
55 energy += w * diff * diff;
56 }
57
58 Ok(energy)
59}
60
61pub fn total_variation(
77 graph: &Graph,
78 signal: &[f64],
79 weights: Option<&[f64]>,
80) -> IgraphResult<f64> {
81 validate_inputs(graph, signal, weights)?;
82
83 let mut tv = 0.0_f64;
84 for (eid, (u, v)) in graph.edges().enumerate() {
85 let w = weights.map_or(1.0, |ws| ws[eid]);
86 tv += w * (signal[u as usize] - signal[v as usize]).abs();
87 }
88
89 Ok(tv)
90}
91
92pub fn normalized_dirichlet_energy(
114 graph: &Graph,
115 signal: &[f64],
116 weights: Option<&[f64]>,
117) -> IgraphResult<f64> {
118 validate_inputs(graph, signal, weights)?;
119
120 let nv = graph.vcount() as usize;
121
122 let mut numerator = 0.0_f64;
123 let mut deg_weighted = vec![0.0_f64; nv];
124
125 for (eid, (u, v)) in graph.edges().enumerate() {
126 let w = weights.map_or(1.0, |ws| ws[eid]);
127 let diff = signal[u as usize] - signal[v as usize];
128 numerator += w * diff * diff;
129 deg_weighted[u as usize] += w;
130 deg_weighted[v as usize] += w;
131 }
132
133 let mut denominator = 0.0_f64;
134 for v in 0..nv {
135 denominator += deg_weighted[v] * signal[v] * signal[v];
136 }
137
138 if denominator.abs() < 1e-300 {
139 return Ok(0.0);
140 }
141
142 Ok(numerator / denominator)
143}
144
145pub fn smoothness_ratio(
166 graph: &Graph,
167 signal: &[f64],
168 weights: Option<&[f64]>,
169) -> IgraphResult<f64> {
170 validate_inputs(graph, signal, weights)?;
171
172 let mut dirichlet = 0.0_f64;
173 let mut sum_sq = 0.0_f64;
174
175 for (eid, (u, v)) in graph.edges().enumerate() {
176 let w = weights.map_or(1.0, |ws| ws[eid]);
177 let fu = signal[u as usize];
178 let fv = signal[v as usize];
179 dirichlet += w * (fu - fv) * (fu - fv);
180 sum_sq += w * (fu * fu + fv * fv);
181 }
182
183 let denom = 2.0 * sum_sq;
184 if denom.abs() < 1e-300 {
185 return Ok(1.0);
186 }
187
188 Ok(1.0 - dirichlet / denom)
189}
190
191fn validate_inputs(graph: &Graph, signal: &[f64], weights: Option<&[f64]>) -> IgraphResult<()> {
194 let nv = graph.vcount() as usize;
195 let ne = graph.ecount();
196
197 if signal.len() != nv {
198 return Err(IgraphError::InvalidArgument(format!(
199 "signal length {} does not match vcount {nv}",
200 signal.len()
201 )));
202 }
203
204 if let Some(w) = weights {
205 if w.len() != ne {
206 return Err(IgraphError::InvalidArgument(format!(
207 "weights length {} does not match ecount {ne}",
208 w.len()
209 )));
210 }
211 }
212
213 Ok(())
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn path4() -> Graph {
221 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
222 }
223
224 fn triangle() -> Graph {
225 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
226 }
227
228 fn cycle4() -> Graph {
229 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
230 }
231
232 #[test]
235 fn de_constant_signal() {
236 let g = path4();
237 let e = dirichlet_energy(&g, &[3.0, 3.0, 3.0, 3.0], None).unwrap();
238 assert!(e.abs() < 1e-10);
239 }
240
241 #[test]
242 fn de_step_signal() {
243 let g = path4();
244 let e = dirichlet_energy(&g, &[0.0, 0.0, 1.0, 1.0], None).unwrap();
246 assert!((e - 1.0).abs() < 1e-10);
247 }
248
249 #[test]
250 fn de_linear_signal() {
251 let g = path4();
252 let e = dirichlet_energy(&g, &[0.0, 1.0, 2.0, 3.0], None).unwrap();
254 assert!((e - 3.0).abs() < 1e-10);
255 }
256
257 #[test]
258 fn de_weighted() {
259 let g = path4();
260 let w = vec![2.0, 1.0, 3.0];
261 let e = dirichlet_energy(&g, &[0.0, 1.0, 0.0, 1.0], Some(&w)).unwrap();
264 assert!((e - 6.0).abs() < 1e-10);
265 }
266
267 #[test]
268 fn de_empty_graph() {
269 let g = Graph::with_vertices(3);
270 let e = dirichlet_energy(&g, &[1.0, 2.0, 3.0], None).unwrap();
271 assert!(e.abs() < 1e-10);
272 }
273
274 #[test]
275 fn de_invalid_signal() {
276 let g = path4();
277 assert!(dirichlet_energy(&g, &[1.0], None).is_err());
278 }
279
280 #[test]
281 fn de_invalid_weights() {
282 let g = path4();
283 assert!(dirichlet_energy(&g, &[0.0; 4], Some(&[1.0])).is_err());
284 }
285
286 #[test]
287 fn de_directed() {
288 let g = Graph::from_edges(&[(0, 1), (1, 2)], true, Some(3)).unwrap();
289 let e = dirichlet_energy(&g, &[0.0, 1.0, 3.0], None).unwrap();
290 assert!((e - 5.0).abs() < 1e-10);
292 }
293
294 #[test]
297 fn tv_constant() {
298 let g = triangle();
299 let tv = total_variation(&g, &[5.0, 5.0, 5.0], None).unwrap();
300 assert!(tv.abs() < 1e-10);
301 }
302
303 #[test]
304 fn tv_step() {
305 let g = path4();
306 let tv = total_variation(&g, &[0.0, 0.0, 1.0, 1.0], None).unwrap();
308 assert!((tv - 1.0).abs() < 1e-10);
309 }
310
311 #[test]
312 fn tv_linear() {
313 let g = path4();
314 let tv = total_variation(&g, &[0.0, 1.0, 2.0, 3.0], None).unwrap();
316 assert!((tv - 3.0).abs() < 1e-10);
317 }
318
319 #[test]
320 fn tv_nonneg() {
321 let g = cycle4();
322 let tv = total_variation(&g, &[1.0, -1.0, 2.0, -2.0], None).unwrap();
323 assert!(tv >= 0.0);
324 }
325
326 #[test]
329 fn nde_constant() {
330 let g = triangle();
331 let e = normalized_dirichlet_energy(&g, &[3.0, 3.0, 3.0], None).unwrap();
332 assert!(e.abs() < 1e-10);
333 }
334
335 #[test]
336 fn nde_zero_signal() {
337 let g = triangle();
338 let e = normalized_dirichlet_energy(&g, &[0.0, 0.0, 0.0], None).unwrap();
339 assert!(e.abs() < 1e-10);
340 }
341
342 #[test]
343 fn nde_bounded() {
344 let g = cycle4();
345 let e = normalized_dirichlet_energy(&g, &[1.0, -1.0, 1.0, -1.0], None).unwrap();
347 assert!(e >= -1e-10);
348 assert!(e <= 2.0 + 1e-10);
349 }
350
351 #[test]
352 fn nde_empty_graph() {
353 let g = Graph::with_vertices(3);
354 let e = normalized_dirichlet_energy(&g, &[1.0, 2.0, 3.0], None).unwrap();
355 assert!(e.abs() < 1e-10);
356 }
357
358 #[test]
361 fn sr_constant() {
362 let g = triangle();
363 let sr = smoothness_ratio(&g, &[2.0, 2.0, 2.0], None).unwrap();
364 assert!((sr - 1.0).abs() < 1e-10);
365 }
366
367 #[test]
368 fn sr_bounded() {
369 let g = cycle4();
370 let sr = smoothness_ratio(&g, &[1.0, -1.0, 1.0, -1.0], None).unwrap();
371 assert!(sr >= -1e-10);
372 assert!(sr <= 1.0 + 1e-10);
373 }
374
375 #[test]
376 fn sr_zero_signal() {
377 let g = triangle();
378 let sr = smoothness_ratio(&g, &[0.0, 0.0, 0.0], None).unwrap();
379 assert!((sr - 1.0).abs() < 1e-10);
380 }
381
382 #[test]
383 fn sr_empty_graph() {
384 let g = Graph::with_vertices(3);
385 let sr = smoothness_ratio(&g, &[1.0, 2.0, 3.0], None).unwrap();
386 assert!((sr - 1.0).abs() < 1e-10);
387 }
388
389 #[test]
390 fn sr_alternating_on_path() {
391 let g = path4();
392 let sr = smoothness_ratio(&g, &[1.0, -1.0, 1.0, -1.0], None).unwrap();
394 assert!(sr.abs() < 1e-10);
397 }
398
399 #[test]
402 fn de_equals_tv_squared_for_unit_step() {
403 let g = path4();
404 let signal = [0.0, 0.0, 1.0, 1.0];
405 let de = dirichlet_energy(&g, &signal, None).unwrap();
406 let tv = total_variation(&g, &signal, None).unwrap();
407 assert!((de - tv).abs() < 1e-10);
409 }
410
411 #[test]
412 fn de_geq_zero() {
413 let g = cycle4();
414 for signal in &[
415 vec![1.0, 2.0, 3.0, 4.0],
416 vec![-1.0, 0.5, -0.3, 2.0],
417 vec![0.0, 0.0, 0.0, 0.0],
418 ] {
419 let de = dirichlet_energy(&g, signal, None).unwrap();
420 assert!(de >= -1e-15);
421 }
422 }
423}