rustworkx_core/graph_ext/contraction.rs
1// Licensed under the Apache License, Version 2.0 (the "License"); you may
2// not use this file except in compliance with the License. You may obtain
3// a copy of the License at
4//
5// http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10// License for the specific language governing permissions and limitations
11// under the License.
12
13//! This module defines graph traits for node contraction.
14
15use crate::dictmap::{DictMap, InitWithHasher};
16use crate::err::{ContractError, ContractSimpleError};
17use crate::graph_ext::NodeRemovable;
18use indexmap::map::Entry;
19use indexmap::IndexSet;
20use petgraph::data::Build;
21use petgraph::graphmap;
22use petgraph::stable_graph;
23use petgraph::visit::{Data, Dfs, EdgeRef, GraphBase, GraphProp, IntoEdgesDirected, Visitable};
24use petgraph::{Directed, Direction, Undirected};
25use std::convert::Infallible;
26use std::error::Error;
27use std::hash::Hash;
28use std::ops::Deref;
29
30pub trait ContractNodesDirected: Data {
31 /// The error type returned by contraction.
32 type Error: Error;
33
34 /// Substitute a set of nodes with a single new node.
35 ///
36 /// The specified `nodes` are removed and replaced with a new node
37 /// with the given `weight`. Any nodes not in the graph are ignored.
38 /// It is valid for `nodes` to be empty, in which case the new node
39 /// is added to the graph without edges.
40 ///
41 /// The contraction may result in multiple edges between nodes if
42 /// the underlying graph is a multi-graph. If this is not desired,
43 /// use [ContractNodesSimpleDirected::contract_nodes_simple].
44 ///
45 /// If `check_cycle` is enabled and the contraction would introduce
46 /// a cycle, an error is returned and the graph is not modified.
47 ///
48 /// The `NodeId` of the newly created node is returned.
49 ///
50 /// # Example
51 /// ```
52 /// use std::convert::Infallible;
53 /// use petgraph::prelude::*;
54 /// use rustworkx_core::graph_ext::*;
55 ///
56 /// // Performs the following transformation:
57 /// // ┌─┐
58 /// // │a│
59 /// // └┬┘ ┌─┐
60 /// // 0 │a│
61 /// // ┌▼┐ └┬┘
62 /// // │b│ 0
63 /// // └┬┘ ┌▼┐
64 /// // 1 ───► │m│
65 /// // ┌▼┐ └┬┘
66 /// // │c│ 2
67 /// // └┬┘ ┌▼┐
68 /// // 2 │d│
69 /// // ┌▼┐ └─┘
70 /// // │d│
71 /// // └─┘
72 /// let mut dag: StableDiGraph<char, usize> = StableDiGraph::default();
73 /// let a = dag.add_node('a');
74 /// let b = dag.add_node('b');
75 /// let c = dag.add_node('c');
76 /// let d = dag.add_node('d');
77 /// dag.add_edge(a.clone(), b.clone(), 0);
78 /// dag.add_edge(b.clone(), c.clone(), 1);
79 /// dag.add_edge(c.clone(), d.clone(), 2);
80 ///
81 /// let m = dag.contract_nodes([b, c], 'm', true).unwrap();
82 /// assert_eq!(dag.edge_weight(dag.find_edge(a.clone(), m.clone()).unwrap()).unwrap(), &0);
83 /// assert_eq!(dag.edge_weight(dag.find_edge(m.clone(), d.clone()).unwrap()).unwrap(), &2);
84 /// ```
85 fn contract_nodes<I>(
86 &mut self,
87 nodes: I,
88 weight: Self::NodeWeight,
89 check_cycle: bool,
90 ) -> Result<Self::NodeId, Self::Error>
91 where
92 I: IntoIterator<Item = Self::NodeId>;
93}
94
95impl<N, E, Ix> ContractNodesDirected for stable_graph::StableGraph<N, E, Directed, Ix>
96where
97 Ix: stable_graph::IndexType,
98 E: Clone,
99{
100 type Error = ContractError;
101
102 fn contract_nodes<I>(
103 &mut self,
104 nodes: I,
105 obj: Self::NodeWeight,
106 check_cycle: bool,
107 ) -> Result<Self::NodeId, Self::Error>
108 where
109 I: IntoIterator<Item = Self::NodeId>,
110 {
111 let nodes = IndexSet::from_iter(nodes);
112 if check_cycle && !can_contract(self.deref(), &nodes) {
113 return Err(ContractError::DAGWouldCycle);
114 }
115 Ok(contract_stable(self, nodes, obj, NoCallback::None).unwrap())
116 }
117}
118
119impl<N, E> ContractNodesDirected for graphmap::GraphMap<N, E, Directed>
120where
121 for<'a> N: graphmap::NodeTrait + 'a,
122 for<'a> E: Clone + 'a,
123{
124 type Error = ContractError;
125
126 fn contract_nodes<I>(
127 &mut self,
128 nodes: I,
129 obj: Self::NodeWeight,
130 check_cycle: bool,
131 ) -> Result<Self::NodeId, Self::Error>
132 where
133 I: IntoIterator<Item = Self::NodeId>,
134 {
135 let nodes = IndexSet::from_iter(nodes);
136 if check_cycle && !can_contract(self.deref(), &nodes) {
137 return Err(ContractError::DAGWouldCycle);
138 }
139 Ok(contract_stable(self, nodes, obj, NoCallback::None).unwrap())
140 }
141}
142
143pub trait ContractNodesSimpleDirected: Data {
144 /// The error type returned by contraction.
145 type Error<Ex: Error>: Error;
146
147 /// Substitute a set of nodes with a single new node.
148 ///
149 /// The specified `nodes` are removed and replaced with a new node
150 /// with the given `weight`. Any nodes not in the graph are ignored.
151 /// It is valid for `nodes` to be empty, in which case the new node
152 /// is added to the graph without edges.
153 ///
154 /// The specified function `weight_combo_fn` is used to merge
155 /// would-be parallel edges during contraction; this function
156 /// preserves simple graphs.
157 ///
158 /// If `check_cycle` is enabled and the contraction would introduce
159 /// a cycle, an error is returned and the graph is not modified.
160 ///
161 /// The `NodeId` of the newly created node is returned.
162 ///
163 /// # Example
164 /// ```
165 /// use std::convert::Infallible;
166 /// use petgraph::prelude::*;
167 /// use rustworkx_core::graph_ext::*;
168 ///
169 /// // Performs the following transformation:
170 /// // ┌─┐
171 /// // ┌─┐ │a│
172 /// // ┌0─┤a├─1┐ └┬┘
173 /// // │ └─┘ │ 1
174 /// // ┌▼┐ ┌▼┐ ┌▼┐
175 /// // │b│ │c│ ───► │m│
176 /// // └┬┘ └┬┘ └┬┘
177 /// // │ ┌─┐ │ 3
178 /// // └2►│d│◄3┘ ┌▼┐
179 /// // └─┘ │d│
180 /// // └─┘
181 /// let mut dag: StableDiGraph<char, usize> = StableDiGraph::default();
182 /// let a = dag.add_node('a');
183 /// let b = dag.add_node('b');
184 /// let c = dag.add_node('c');
185 /// let d = dag.add_node('d');
186 /// dag.add_edge(a.clone(), b.clone(), 0);
187 /// dag.add_edge(a.clone(), c.clone(), 1);
188 /// dag.add_edge(b.clone(), d.clone(), 2);
189 /// dag.add_edge(c.clone(), d.clone(), 3);
190 ///
191 /// let m = dag.contract_nodes_simple([b, c], 'm', true, |&e1, &e2| Ok::<_, Infallible>(if e1 > e2 { e1 } else { e2 } )).unwrap();
192 /// assert_eq!(dag.edge_weight(dag.find_edge(a.clone(), m.clone()).unwrap()).unwrap(), &1);
193 /// assert_eq!(dag.edge_weight(dag.find_edge(m.clone(), d.clone()).unwrap()).unwrap(), &3);
194 /// ```
195 fn contract_nodes_simple<I, F, C: Error>(
196 &mut self,
197 nodes: I,
198 weight: Self::NodeWeight,
199 check_cycle: bool,
200 weight_combo_fn: F,
201 ) -> Result<Self::NodeId, Self::Error<C>>
202 where
203 I: IntoIterator<Item = Self::NodeId>,
204 F: FnMut(&Self::EdgeWeight, &Self::EdgeWeight) -> Result<Self::EdgeWeight, C>;
205}
206
207impl<N, E, Ix> ContractNodesSimpleDirected for stable_graph::StableGraph<N, E, Directed, Ix>
208where
209 Ix: stable_graph::IndexType,
210 E: Clone,
211{
212 type Error<Err: Error> = ContractSimpleError<Err>;
213
214 fn contract_nodes_simple<I, F, C: Error>(
215 &mut self,
216 nodes: I,
217 weight: Self::NodeWeight,
218 check_cycle: bool,
219 weight_combo_fn: F,
220 ) -> Result<Self::NodeId, Self::Error<C>>
221 where
222 I: IntoIterator<Item = Self::NodeId>,
223 F: FnMut(&Self::EdgeWeight, &Self::EdgeWeight) -> Result<Self::EdgeWeight, C>,
224 {
225 let nodes = IndexSet::from_iter(nodes);
226 if check_cycle && !can_contract(self.deref(), &nodes) {
227 return Err(ContractSimpleError::DAGWouldCycle);
228 }
229 contract_stable(self, nodes, weight, Some(weight_combo_fn))
230 .map_err(ContractSimpleError::MergeError)
231 }
232}
233
234impl<N, E> ContractNodesSimpleDirected for graphmap::GraphMap<N, E, Directed>
235where
236 for<'a> N: graphmap::NodeTrait + 'a,
237 for<'a> E: Clone + 'a,
238{
239 type Error<Err: Error> = ContractSimpleError<Err>;
240
241 fn contract_nodes_simple<I, F, C: Error>(
242 &mut self,
243 nodes: I,
244 weight: Self::NodeWeight,
245 check_cycle: bool,
246 weight_combo_fn: F,
247 ) -> Result<Self::NodeId, Self::Error<C>>
248 where
249 I: IntoIterator<Item = Self::NodeId>,
250 F: FnMut(&Self::EdgeWeight, &Self::EdgeWeight) -> Result<Self::EdgeWeight, C>,
251 {
252 let nodes = IndexSet::from_iter(nodes);
253 if check_cycle && !can_contract(self.deref(), &nodes) {
254 return Err(ContractSimpleError::DAGWouldCycle);
255 }
256 contract_stable(self, nodes, weight, Some(weight_combo_fn))
257 .map_err(ContractSimpleError::MergeError)
258 }
259}
260
261pub trait ContractNodesUndirected: Data {
262 /// Substitute a set of nodes with a single new node.
263 ///
264 /// The specified `nodes` are removed and replaced with a new node
265 /// with the given `weight`. Any nodes not in the graph are ignored.
266 /// It is valid for `nodes` to be empty, in which case the new node
267 /// is added to the graph without edges.
268 ///
269 /// The contraction may result in multiple edges between nodes if
270 /// the underlying graph is a multi-graph. If this is not desired,
271 /// use [ContractNodesSimpleUndirected::contract_nodes_simple].
272 ///
273 /// The `NodeId` of the newly created node is returned.
274 ///
275 /// # Example
276 /// ```
277 /// use petgraph::prelude::*;
278 /// use rustworkx_core::graph_ext::*;
279 ///
280 /// // Performs the following transformation:
281 /// // ┌─┐
282 /// // │a│
283 /// // └┬┘ ┌─┐
284 /// // 0 │a│
285 /// // ┌┴┐ └┬┘
286 /// // │b│ 0
287 /// // └┬┘ ┌┴┐
288 /// // 1 ───► │m│
289 /// // ┌┴┐ └┬┘
290 /// // │c│ 2
291 /// // └┬┘ ┌┴┐
292 /// // 2 │d│
293 /// // ┌┴┐ └─┘
294 /// // │d│
295 /// // └─┘
296 /// let mut dag: StableUnGraph<char, usize> = StableUnGraph::default();
297 /// let a = dag.add_node('a');
298 /// let b = dag.add_node('b');
299 /// let c = dag.add_node('c');
300 /// let d = dag.add_node('d');
301 /// dag.add_edge(a.clone(), b.clone(), 0);
302 /// dag.add_edge(b.clone(), c.clone(), 1);
303 /// dag.add_edge(c.clone(), d.clone(), 2);
304 ///
305 /// let m = dag.contract_nodes([b, c], 'm');
306 /// assert_eq!(dag.edge_weight(dag.find_edge(a.clone(), m.clone()).unwrap()).unwrap(), &0);
307 /// assert_eq!(dag.edge_weight(dag.find_edge(m.clone(), d.clone()).unwrap()).unwrap(), &2);
308 /// ```
309 fn contract_nodes<I>(&mut self, nodes: I, weight: Self::NodeWeight) -> Self::NodeId
310 where
311 I: IntoIterator<Item = Self::NodeId>;
312}
313
314impl<N, E, Ix> ContractNodesUndirected for stable_graph::StableGraph<N, E, Undirected, Ix>
315where
316 Ix: stable_graph::IndexType,
317 E: Clone,
318{
319 fn contract_nodes<I>(&mut self, nodes: I, obj: Self::NodeWeight) -> Self::NodeId
320 where
321 I: IntoIterator<Item = Self::NodeId>,
322 {
323 contract_stable(self, IndexSet::from_iter(nodes), obj, NoCallback::None).unwrap()
324 }
325}
326
327impl<N, E> ContractNodesUndirected for graphmap::GraphMap<N, E, Undirected>
328where
329 for<'a> N: graphmap::NodeTrait + 'a,
330 for<'a> E: Clone + 'a,
331{
332 fn contract_nodes<I>(&mut self, nodes: I, obj: Self::NodeWeight) -> Self::NodeId
333 where
334 I: IntoIterator<Item = Self::NodeId>,
335 {
336 contract_stable(self, IndexSet::from_iter(nodes), obj, NoCallback::None).unwrap()
337 }
338}
339
340pub trait ContractNodesSimpleUndirected: Data {
341 type Error<Ex: Error>: Error;
342
343 /// Substitute a set of nodes with a single new node.
344 ///
345 /// The specified `nodes` are removed and replaced with a new node
346 /// with the given `weight`. Any nodes not in the graph are ignored.
347 /// It is valid for `nodes` to be empty, in which case the new node
348 /// is added to the graph without edges.
349 ///
350 /// The specified function `weight_combo_fn` is used to merge
351 /// would-be parallel edges during contraction; this function
352 /// preserves simple graphs.
353 ///
354 /// The `NodeId` of the newly created node is returned.
355 ///
356 /// # Example
357 /// ```
358 /// use std::convert::Infallible;
359 /// use petgraph::prelude::*;
360 /// use rustworkx_core::graph_ext::*;
361 ///
362 /// // Performs the following transformation:
363 /// // ┌─┐
364 /// // ┌─┐ │a│
365 /// // ┌0─┤a├─1┐ └┬┘
366 /// // │ └─┘ │ 1
367 /// // ┌┴┐ ┌┴┐ ┌┴┐
368 /// // │b│ │c│ ───► │m│
369 /// // └┬┘ └┬┘ └┬┘
370 /// // │ ┌─┐ │ 3
371 /// // └2─│d├─3┘ ┌┴┐
372 /// // └─┘ │d│
373 /// // └─┘
374 /// let mut dag: StableUnGraph<char, usize> = StableUnGraph::default();
375 /// let a = dag.add_node('a');
376 /// let b = dag.add_node('b');
377 /// let c = dag.add_node('c');
378 /// let d = dag.add_node('d');
379 /// dag.add_edge(a.clone(), b.clone(), 0);
380 /// dag.add_edge(a.clone(), c.clone(), 1);
381 /// dag.add_edge(b.clone(), d.clone(), 2);
382 /// dag.add_edge(c.clone(), d.clone(), 3);
383 ///
384 /// let m = dag.contract_nodes_simple([b, c], 'm', |&e1, &e2| Ok::<_, Infallible>(if e1 > e2 { e1 } else { e2 } )).unwrap();
385 /// assert_eq!(dag.edge_weight(dag.find_edge(a.clone(), m.clone()).unwrap()).unwrap(), &1);
386 /// assert_eq!(dag.edge_weight(dag.find_edge(m.clone(), d.clone()).unwrap()).unwrap(), &3);
387 /// ```
388 fn contract_nodes_simple<I, F, C: Error>(
389 &mut self,
390 nodes: I,
391 weight: Self::NodeWeight,
392 weight_combo_fn: F,
393 ) -> Result<Self::NodeId, Self::Error<C>>
394 where
395 I: IntoIterator<Item = Self::NodeId>,
396 F: FnMut(&Self::EdgeWeight, &Self::EdgeWeight) -> Result<Self::EdgeWeight, C>;
397}
398
399impl<N, E, Ix> ContractNodesSimpleUndirected for stable_graph::StableGraph<N, E, Undirected, Ix>
400where
401 Ix: stable_graph::IndexType,
402 E: Clone,
403{
404 type Error<Err: Error> = ContractSimpleError<Err>;
405
406 fn contract_nodes_simple<I, F, C: Error>(
407 &mut self,
408 nodes: I,
409 weight: Self::NodeWeight,
410 weight_combo_fn: F,
411 ) -> Result<Self::NodeId, Self::Error<C>>
412 where
413 I: IntoIterator<Item = Self::NodeId>,
414 F: FnMut(&Self::EdgeWeight, &Self::EdgeWeight) -> Result<Self::EdgeWeight, C>,
415 {
416 contract_stable(
417 self,
418 IndexSet::from_iter(nodes),
419 weight,
420 Some(weight_combo_fn),
421 )
422 .map_err(ContractSimpleError::MergeError)
423 }
424}
425
426impl<N, E> ContractNodesSimpleUndirected for graphmap::GraphMap<N, E, Undirected>
427where
428 for<'a> N: graphmap::NodeTrait + 'a,
429 for<'a> E: Clone + 'a,
430{
431 type Error<Err: Error> = ContractSimpleError<Err>;
432
433 fn contract_nodes_simple<I, F, C: Error>(
434 &mut self,
435 nodes: I,
436 weight: Self::NodeWeight,
437 weight_combo_fn: F,
438 ) -> Result<Self::NodeId, Self::Error<C>>
439 where
440 I: IntoIterator<Item = Self::NodeId>,
441 F: FnMut(&Self::EdgeWeight, &Self::EdgeWeight) -> Result<Self::EdgeWeight, C>,
442 {
443 contract_stable(
444 self,
445 IndexSet::from_iter(nodes),
446 weight,
447 Some(weight_combo_fn),
448 )
449 .map_err(ContractSimpleError::MergeError)
450 }
451}
452
453fn merge_duplicates<K, V, F, E>(xs: Vec<(K, V)>, mut merge_fn: F) -> Result<Vec<(K, V)>, E>
454where
455 K: Hash + Eq,
456 F: FnMut(&V, &V) -> Result<V, E>,
457{
458 let mut kvs = DictMap::with_capacity(xs.len());
459 for (k, v) in xs {
460 match kvs.entry(k) {
461 Entry::Occupied(entry) => {
462 *entry.into_mut() = merge_fn(&v, entry.get())?;
463 }
464 Entry::Vacant(entry) => {
465 entry.insert(v);
466 }
467 }
468 }
469 Ok(kvs.into_iter().collect::<Vec<_>>())
470}
471
472fn contract_stable<G, F, E: Error>(
473 graph: &mut G,
474 mut nodes: IndexSet<G::NodeId, foldhash::fast::RandomState>,
475 weight: G::NodeWeight,
476 weight_combo_fn: Option<F>,
477) -> Result<G::NodeId, E>
478where
479 G: GraphProp + NodeRemovable + Build + Visitable,
480 for<'b> &'b G:
481 GraphBase<NodeId = G::NodeId> + Data<EdgeWeight = G::EdgeWeight> + IntoEdgesDirected,
482 G::NodeId: Ord + Hash,
483 G::EdgeWeight: Clone,
484 F: FnMut(&G::EdgeWeight, &G::EdgeWeight) -> Result<G::EdgeWeight, E>,
485{
486 let node_index = graph.add_node(weight);
487
488 // Sanitize new node index from user input.
489 nodes.swap_remove(&node_index);
490
491 // Connect old node edges to the replacement.
492 add_edges(graph, node_index, &nodes, weight_combo_fn).unwrap();
493
494 // Remove nodes that have been replaced.
495 for index in nodes {
496 graph.remove_node(index);
497 }
498
499 Ok(node_index)
500}
501/// Check if a set of nodes in a directed graph can be contracted without introducing a cycle.
502///
503/// This function determines whether contracting the given set of nodes into a single node
504/// would introduce a cycle in the graph. It is typically used to check the feasibility of the contraction before performing
505/// the actual operation.
506///
507/// # Arguments
508///
509/// * `graph` - The graph to check.
510/// * `nodes` - A set of node indices to be contracted.
511///
512/// A bool whether the graph can be contracted based on the given nodes is returned.
513///
514/// # Example
515///
516/// ```rust
517/// use petgraph::stable_graph::StableDiGraph;
518/// use indexmap::IndexSet;
519/// use foldhash::fast::RandomState;
520/// use rustworkx_core::graph_ext::contraction::can_contract;
521///
522/// // Create a simple DAG: a -> b -> c
523/// let mut graph = StableDiGraph::<&str, ()>::default();
524/// let a = graph.add_node("a");
525/// let b = graph.add_node("b");
526/// let c = graph.add_node("c");
527/// graph.add_edge(a, b, ());
528/// graph.add_edge(b, c, ());
529///
530/// // Try to contract nodes b and c
531/// let mut nodes = IndexSet::with_hasher(RandomState::default());
532/// nodes.insert(b);
533/// nodes.insert(c);
534///
535/// let can_contract = can_contract(&graph, &nodes);
536/// assert!(can_contract); // true: contracting b and c does not introduce a cycle
537/// ```
538pub fn can_contract<G>(graph: G, nodes: &IndexSet<G::NodeId, foldhash::fast::RandomState>) -> bool
539where
540 G: Data + Visitable + IntoEdgesDirected,
541 G::NodeId: Eq + Hash,
542{
543 // Start with successors of `nodes` that aren't in `nodes` itself.
544 let visit_next: Vec<G::NodeId> = nodes
545 .iter()
546 .flat_map(|n| graph.edges(*n))
547 .filter_map(|edge| {
548 let target_node = edge.target();
549 if !nodes.contains(&target_node) {
550 Some(target_node)
551 } else {
552 None
553 }
554 })
555 .collect();
556
557 // Now, if we can reach any of `nodes`, there exists a path from `nodes`
558 // back to `nodes` of length > 1, meaning contraction is disallowed.
559 let mut dfs = Dfs::from_parts(visit_next, graph.visit_map());
560 while let Some(node) = dfs.next(graph) {
561 if nodes.contains(&node) {
562 // we found a path back to `nodes`
563 return false;
564 }
565 }
566 true
567}
568
569// Helper type for specifying `NoCallback::None` at callsites of `contract`.
570type NoCallback<E> = Option<fn(&E, &E) -> Result<E, Infallible>>;
571
572fn add_edges<G, F, E>(
573 graph: &mut G,
574 new_node: G::NodeId,
575 nodes: &IndexSet<G::NodeId, foldhash::fast::RandomState>,
576 mut weight_combo_fn: Option<F>,
577) -> Result<(), E>
578where
579 G: GraphProp + Build + Visitable,
580 for<'b> &'b G:
581 GraphBase<NodeId = G::NodeId> + Data<EdgeWeight = G::EdgeWeight> + IntoEdgesDirected,
582 G::NodeId: Ord + Hash,
583 G::EdgeWeight: Clone,
584 F: FnMut(&G::EdgeWeight, &G::EdgeWeight) -> Result<G::EdgeWeight, E>,
585{
586 // Determine and add edges for new node.
587 {
588 // Note: even when the graph is undirected, we used edges_directed because
589 // it gives us a consistent endpoint order.
590 let mut incoming_edges: Vec<(G::NodeId, G::EdgeWeight)> = nodes
591 .iter()
592 .flat_map(|i| graph.edges_directed(*i, Direction::Incoming))
593 .filter_map(|edge| {
594 let pred = edge.source();
595 (!nodes.contains(&pred)).then_some((pred, edge.weight().clone()))
596 })
597 .collect();
598
599 if let Some(merge_fn) = &mut weight_combo_fn {
600 incoming_edges = merge_duplicates(incoming_edges, merge_fn)?;
601 }
602
603 for (source, weight) in incoming_edges.into_iter() {
604 graph.add_edge(source, new_node, weight);
605 }
606 }
607
608 if graph.is_directed() {
609 let mut outgoing_edges: Vec<(G::NodeId, G::EdgeWeight)> = nodes
610 .iter()
611 .flat_map(|&i| graph.edges_directed(i, Direction::Outgoing))
612 .filter_map(|edge| {
613 let succ = edge.target();
614 (!nodes.contains(&succ)).then_some((succ, edge.weight().clone()))
615 })
616 .collect();
617
618 if let Some(merge_fn) = &mut weight_combo_fn {
619 outgoing_edges = merge_duplicates(outgoing_edges, merge_fn)?;
620 }
621
622 for (target, weight) in outgoing_edges.into_iter() {
623 graph.add_edge(new_node, target, weight);
624 }
625 }
626
627 Ok(())
628}