1use std::collections::VecDeque;
12
13use crate::algorithms::spanning::mst::{MstAlgorithm, minimum_spanning_tree};
14use crate::core::graph::EdgeId;
15use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FasAlgorithm {
20 EadesLinSmyth,
23}
24
25pub fn feedback_arc_set(
59 graph: &Graph,
60 weights: Option<&[f64]>,
61 _algo: FasAlgorithm,
62) -> IgraphResult<Vec<EdgeId>> {
63 if let Some(w) = weights {
64 if w.len() != graph.ecount() {
65 return Err(IgraphError::InvalidArgument(format!(
66 "weights length {} does not match ecount {}",
67 w.len(),
68 graph.ecount()
69 )));
70 }
71 if !w.iter().all(|x| x.is_finite()) {
72 return Err(IgraphError::InvalidArgument(
73 "weights must be finite".into(),
74 ));
75 }
76 }
77
78 if graph.ecount() == 0 {
79 return Ok(Vec::new());
80 }
81
82 if graph.is_directed() {
83 fas_eades(graph, weights)
84 } else {
85 fas_undirected(graph, weights)
86 }
87}
88
89fn fas_undirected(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<EdgeId>> {
90 let negated: Vec<f64>;
91 let mst_weights = match weights {
92 Some(w) => {
93 negated = w.iter().map(|x| -x).collect();
94 Some(negated.as_slice())
95 }
96 None => None,
97 };
98
99 let tree_edges = minimum_spanning_tree(graph, mst_weights, MstAlgorithm::Automatic)?;
100
101 let mut in_tree = vec![false; graph.ecount()];
102 for &eid in &tree_edges {
103 in_tree[eid as usize] = true;
104 }
105
106 let mut result = Vec::new();
107 for (eid, &is_tree) in in_tree.iter().enumerate() {
108 if !is_tree {
109 #[allow(clippy::cast_possible_truncation)]
110 result.push(eid as EdgeId);
111 }
112 }
113 Ok(result)
114}
115
116#[allow(clippy::too_many_lines)]
117fn fas_eades(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<EdgeId>> {
118 let n = graph.vcount() as usize;
119 let edge_count = graph.ecount();
120
121 let mut indegrees: Vec<i32> = vec![0; n];
122 let mut outdegrees: Vec<i32> = vec![0; n];
123 let mut instrengths: Vec<f64> = vec![0.0; n];
124 let mut outstrengths: Vec<f64> = vec![0.0; n];
125
126 for eid in 0..edge_count {
127 #[allow(clippy::cast_possible_truncation)]
128 let (from, to) = graph.edge(eid as EdgeId)?;
129 if from == to {
130 continue;
131 }
132 let w = weights.map_or(1.0, |ws| ws[eid]);
133 outdegrees[from as usize] += 1;
134 outstrengths[from as usize] += w;
135 indegrees[to as usize] += 1;
136 instrengths[to as usize] += w;
137 }
138
139 let mut ordering: Vec<i32> = vec![0; n];
140 let mut order_next_pos: i32 = 0;
141 let mut order_next_neg: i32 = -1;
142
143 let mut sources: VecDeque<VertexId> = VecDeque::new();
144 let mut sinks: VecDeque<VertexId> = VecDeque::new();
145
146 let mut nodes_left: usize = n;
147
148 for u in 0..n {
149 if indegrees[u] == 0 {
150 if outdegrees[u] == 0 {
151 #[allow(clippy::cast_possible_truncation)]
152 {
153 ordering[u] = order_next_pos;
154 order_next_pos += 1;
155 indegrees[u] = -1;
156 outdegrees[u] = -1;
157 nodes_left -= 1;
158 }
159 } else {
160 #[allow(clippy::cast_possible_truncation)]
161 sources.push_back(u as VertexId);
162 }
163 } else if outdegrees[u] == 0 {
164 #[allow(clippy::cast_possible_truncation)]
165 sinks.push_back(u as VertexId);
166 }
167 }
168
169 while nodes_left > 0 {
170 while let Some(u) = sources.pop_front() {
171 let ui = u as usize;
172 ordering[ui] = order_next_pos;
173 order_next_pos += 1;
174 indegrees[ui] = -1;
175 outdegrees[ui] = -1;
176 nodes_left -= 1;
177
178 let out_edges = graph.incident(u)?;
179 for &eid in &out_edges {
180 let to = graph.edge_target(eid)?;
181 if to == u {
182 continue;
183 }
184 let wi = to as usize;
185 if indegrees[wi] <= 0 {
186 continue;
187 }
188 indegrees[wi] -= 1;
189 instrengths[wi] -= weights.map_or(1.0, |ws| ws[eid as usize]);
190 if indegrees[wi] == 0 {
191 sources.push_back(to);
192 }
193 }
194 }
195
196 while let Some(u) = sinks.pop_front() {
197 let ui = u as usize;
198 if indegrees[ui] < 0 {
199 continue;
200 }
201 ordering[ui] = order_next_neg;
202 order_next_neg -= 1;
203 indegrees[ui] = -1;
204 outdegrees[ui] = -1;
205 nodes_left -= 1;
206
207 let in_edges = graph.incident_in(u)?;
208 for &eid in &in_edges {
209 let from = graph.edge_source(eid)?;
210 if from == u {
211 continue;
212 }
213 let wi = from as usize;
214 if outdegrees[wi] <= 0 {
215 continue;
216 }
217 outdegrees[wi] -= 1;
218 outstrengths[wi] -= weights.map_or(1.0, |ws| ws[eid as usize]);
219 if outdegrees[wi] == 0 {
220 sinks.push_back(from);
221 }
222 }
223 }
224
225 let mut best_v: Option<usize> = None;
226 let mut max_diff = f64::NEG_INFINITY;
227 for u in 0..n {
228 if outdegrees[u] < 0 {
229 continue;
230 }
231 let diff = outstrengths[u] - instrengths[u];
232 if diff > max_diff {
233 max_diff = diff;
234 best_v = Some(u);
235 }
236 }
237
238 if let Some(v) = best_v {
239 ordering[v] = order_next_pos;
240 order_next_pos += 1;
241
242 #[allow(clippy::cast_possible_truncation)]
243 let vv = v as VertexId;
244
245 let out_edges = graph.incident(vv)?;
246 for &eid in &out_edges {
247 let to = graph.edge_target(eid)?;
248 if to == vv {
249 continue;
250 }
251 let wi = to as usize;
252 if indegrees[wi] <= 0 {
253 continue;
254 }
255 indegrees[wi] -= 1;
256 instrengths[wi] -= weights.map_or(1.0, |ws| ws[eid as usize]);
257 if indegrees[wi] == 0 {
258 sources.push_back(to);
259 }
260 }
261
262 let in_edges = graph.incident_in(vv)?;
263 for &eid in &in_edges {
264 let from = graph.edge_source(eid)?;
265 if from == vv {
266 continue;
267 }
268 let wi = from as usize;
269 if outdegrees[wi] <= 0 {
270 continue;
271 }
272 outdegrees[wi] -= 1;
273 outstrengths[wi] -= weights.map_or(1.0, |ws| ws[eid as usize]);
274 if outdegrees[wi] == 0 && indegrees[wi] > 0 {
275 sinks.push_back(from);
276 }
277 }
278
279 indegrees[v] = -1;
280 outdegrees[v] = -1;
281 nodes_left -= 1;
282 }
283 }
284
285 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
286 let n_i32 = n as i32;
287 for pos in &mut ordering {
288 if *pos < 0 {
289 *pos += n_i32;
290 }
291 }
292
293 let mut result = Vec::new();
294 for eid in 0..edge_count {
295 #[allow(clippy::cast_possible_truncation)]
296 let (from, to) = graph.edge(eid as EdgeId)?;
297 if from == to || ordering[from as usize] > ordering[to as usize] {
298 #[allow(clippy::cast_possible_truncation)]
299 result.push(eid as EdgeId);
300 }
301 }
302
303 Ok(result)
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::algorithms::cycles::{CycleMode, find_cycle};
310
311 #[test]
312 fn empty_graph() {
313 let g = Graph::with_vertices(0);
314 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
315 assert!(fas.is_empty());
316 }
317
318 #[test]
319 fn dag_no_feedback() {
320 let mut g = Graph::new(4, true).unwrap();
321 g.add_edge(0, 1).unwrap();
322 g.add_edge(1, 2).unwrap();
323 g.add_edge(2, 3).unwrap();
324 g.add_edge(0, 3).unwrap();
325 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
326 assert!(fas.is_empty());
327 }
328
329 #[test]
330 fn simple_directed_cycle() {
331 let mut g = Graph::new(3, true).unwrap();
332 g.add_edge(0, 1).unwrap();
333 g.add_edge(1, 2).unwrap();
334 g.add_edge(2, 0).unwrap();
335 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
336 assert!(!fas.is_empty());
337 assert!(fas.len() <= 2);
338 }
339
340 #[test]
341 fn directed_cycle_4() {
342 let mut g = Graph::new(4, true).unwrap();
343 g.add_edge(0, 1).unwrap();
344 g.add_edge(1, 2).unwrap();
345 g.add_edge(2, 3).unwrap();
346 g.add_edge(3, 0).unwrap();
347 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
348 assert!(!fas.is_empty());
349 assert!(fas.len() <= 2);
350 }
351
352 #[test]
353 fn undirected_tree_no_feedback() {
354 let mut g = Graph::with_vertices(5);
355 g.add_edge(0, 1).unwrap();
356 g.add_edge(1, 2).unwrap();
357 g.add_edge(1, 3).unwrap();
358 g.add_edge(3, 4).unwrap();
359 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
360 assert!(fas.is_empty());
361 }
362
363 #[test]
364 fn undirected_single_cycle() {
365 let mut g = Graph::with_vertices(3);
366 g.add_edge(0, 1).unwrap();
367 g.add_edge(1, 2).unwrap();
368 g.add_edge(2, 0).unwrap();
369 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
370 assert_eq!(fas.len(), 1);
371 }
372
373 #[test]
374 fn undirected_k4() {
375 let mut g = Graph::with_vertices(4);
376 for u in 0..4u32 {
377 for v in (u + 1)..4 {
378 g.add_edge(u, v).unwrap();
379 }
380 }
381 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
382 assert_eq!(fas.len(), 3);
384 }
385
386 #[test]
387 fn self_loops_in_directed() {
388 let mut g = Graph::new(3, true).unwrap();
389 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
391 g.add_edge(1, 2).unwrap();
392 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
393 assert!(fas.contains(&0)); }
395
396 #[test]
397 fn weighted_undirected() {
398 let mut g = Graph::with_vertices(3);
399 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(2, 0).unwrap(); let weights = vec![1.0, 10.0, 10.0];
403 let fas = feedback_arc_set(&g, Some(&weights), FasAlgorithm::EadesLinSmyth).unwrap();
404 assert_eq!(fas.len(), 1);
406 assert_eq!(fas[0], 0); }
408
409 #[test]
410 fn weighted_directed() {
411 let mut g = Graph::new(3, true).unwrap();
412 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(2, 0).unwrap(); let weights = vec![1.0, 1.0, 100.0];
416 let fas = feedback_arc_set(&g, Some(&weights), FasAlgorithm::EadesLinSmyth).unwrap();
417 assert!(!fas.is_empty());
418 }
419
420 #[test]
421 fn invalid_weights_length() {
422 let g = Graph::with_vertices(3);
423 let weights = vec![1.0]; assert!(feedback_arc_set(&g, Some(&weights), FasAlgorithm::EadesLinSmyth).is_err());
425 }
426
427 #[test]
428 fn nan_weights_rejected() {
429 let mut g = Graph::with_vertices(2);
430 g.add_edge(0, 1).unwrap();
431 let weights = vec![f64::NAN];
432 assert!(feedback_arc_set(&g, Some(&weights), FasAlgorithm::EadesLinSmyth).is_err());
433 }
434
435 #[test]
436 fn two_directed_cycles_sharing_vertex() {
437 let mut g = Graph::new(5, true).unwrap();
439 g.add_edge(0, 1).unwrap();
440 g.add_edge(1, 2).unwrap();
441 g.add_edge(2, 0).unwrap();
442 g.add_edge(0, 3).unwrap();
443 g.add_edge(3, 4).unwrap();
444 g.add_edge(4, 0).unwrap();
445 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
446 assert!(fas.len() >= 2);
447 assert!(fas.len() <= 4);
448 }
449
450 #[test]
451 fn bidirectional_edges() {
452 let mut g = Graph::new(2, true).unwrap();
454 g.add_edge(0, 1).unwrap();
455 g.add_edge(1, 0).unwrap();
456 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
457 assert_eq!(fas.len(), 1);
458 }
459
460 #[test]
461 fn isolated_vertices_directed() {
462 let g = Graph::new(5, true).unwrap();
463 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
464 assert!(fas.is_empty());
465 }
466
467 #[test]
468 fn removes_all_cycles() {
469 let mut g = Graph::new(5, true).unwrap();
472 g.add_edge(0, 1).unwrap();
473 g.add_edge(1, 2).unwrap();
474 g.add_edge(2, 0).unwrap();
475 g.add_edge(2, 3).unwrap();
476 g.add_edge(3, 4).unwrap();
477 g.add_edge(4, 2).unwrap();
478
479 let fas = feedback_arc_set(&g, None, FasAlgorithm::EadesLinSmyth).unwrap();
480
481 let fas_set: std::collections::HashSet<EdgeId> = fas.into_iter().collect();
483 let mut g2 = Graph::new(5, true).unwrap();
484 for eid in 0..g.ecount() {
485 #[allow(clippy::cast_possible_truncation)]
486 let eid32 = eid as EdgeId;
487 if !fas_set.contains(&eid32) {
488 let (from, to) = g.edge(eid32).unwrap();
489 g2.add_edge(from, to).unwrap();
490 }
491 }
492 let cycle = find_cycle(&g2, CycleMode::Out);
494 assert!(
495 cycle.is_ok() && cycle.unwrap().vertices.is_empty(),
496 "FAS did not break all cycles"
497 );
498 }
499}