Skip to main content

gammalooprs/graph/parse/
mod.rs

1use std::{
2    collections::BTreeMap,
3    ops::Deref,
4    path::{Path, PathBuf},
5};
6
7use crate::{
8    cff::generation::SurfaceCache,
9    feyngen::{
10        GenerationType,
11        diagram_generator::{EdgeColor, NodeColorWithVertexRule},
12    },
13    graph::{
14        GraphGroup, GroupId, LoopMomentumBasis, attribute_warnings::warn_about_unknown_attributes,
15        edge::EdgeExtraData,
16    },
17    integrands::process::ParamBuilder,
18    model::Model,
19    momentum::sample::LoopIndex,
20    numerator::{
21        GlobalPrefactor,
22        aind::{Aind, NewAind},
23        graph::GeneratePolarizations,
24        ufo::UFO,
25    },
26    processes::DotExportSettings,
27    utils::symbolica_ext::DOD,
28    uv::{UltravioletGraph, uv_graph::UVE},
29};
30use ahash::{AHashMap, AHashSet};
31use idenso::{
32    color::{ColorSimplifier, ColorSimplifySettings},
33    tensor::SymbolicNetParse,
34};
35use spenso::shadowing::symbolica_utils::LogPrint;
36
37use color_eyre::{Report, Result, Section};
38
39use eyre::{Context, Ok, eyre};
40use itertools::Itertools;
41// use symbolica::{atom::Atom, graph::Graph as SymbolicaGraph};
42
43use linnet::{
44    half_edge::{
45        HedgeGraph, NodeIndex,
46        builder::HedgeGraphBuilder,
47        involution::{EdgeData, EdgeIndex, EdgeVec, Flow, Hedge, HedgePair},
48        nodestore::NodeStorageVec,
49        subgraph::{Inclusion, ModifySubSet, OrientedCut, SuBitGraph, SubSetLike, SubSetOps},
50        swap::Swap,
51    },
52    parser::{DotEdgeData, DotGraph, DotHedgeData, DotVertexData, GraphSet, HedgeParseError},
53    permutation::Permutation,
54};
55use spenso::{
56    contraction::Contract,
57    network::parsing::ParseSettings,
58    structure::{HasStructure, OrderedStructure, representation::Euclidean, slot::IsAbstractSlot},
59    tensors::{data::StorageTensor, parametric::ParamTensor},
60};
61use symbolica::{
62    atom::{Atom, AtomOrView},
63    graph::Graph as SymbolicaGraph,
64};
65use tracing::instrument;
66use tracing::{debug, warn};
67use typed_index_collections::TiVec;
68
69use super::{
70    Autogen, Edge, Graph, HedgeData, LMBext, Vertex,
71    edge::{EdgeMass, ParseEdge},
72    global::ParseData,
73    hedge_data::{NumIndices, ParseHedgeData},
74    vertex::ParseVertex,
75};
76
77/// Extract oriented particles from hedges, filtering out dummy edges
78pub fn extract_oriented_particles_from_vertex_hedges<I, V>(
79    graph: &HedgeGraph<ParseEdge, V, ParseHedgeData>,
80    hedges: I,
81    model: &Model,
82) -> Vec<crate::model::ArcParticle>
83where
84    I: Iterator<Item = Hedge>,
85{
86    hedges
87        .filter_map(|h| {
88            let eid = graph[&h];
89            if graph[eid].is_dummy {
90                return None;
91            }
92            let particle = graph[eid].particle.particle()?;
93            Some(if graph.flow(h) != Flow::Sink {
94                particle.get_anti_particle(model)
95            } else {
96                particle
97            })
98        })
99        .collect()
100}
101
102// Type aliases for cleaner code
103type NumGraph = HedgeGraph<ParseEdge, ParseVertex, HedgeData>;
104type UnderlyingGraph = HedgeGraph<Edge, Vertex, HedgeData>;
105
106pub mod string_utils;
107pub use string_utils::{FromStripedStr, StripParse, ToQuoted};
108
109#[derive(Clone, Debug)]
110pub struct ParseGraph {
111    pub global_data: ParseData,
112    pub graph: HedgeGraph<ParseEdge, ParseVertex, ParseHedgeData>,
113}
114
115impl Deref for ParseGraph {
116    type Target = HedgeGraph<ParseEdge, ParseVertex, ParseHedgeData>;
117
118    fn deref(&self) -> &Self::Target {
119        &self.graph
120    }
121}
122
123impl ParseGraph {
124    pub fn n_fermion_loops(&self) -> usize {
125        let fermions: SuBitGraph = self.graph.from_filter(|a| a.particle.is_fermion());
126
127        self.graph.cyclotomatic_number(&fermions)
128    }
129    pub fn n_external_fermion_loops(&mut self) -> Result<usize> {
130        let internal = self.n_fermion_loops();
131        self.graph
132            .sew(
133                |_, ae, _, be| {
134                    if let (Some(a), Some(b)) = (ae.data.is_cut, be.data.is_cut) {
135                        a == b
136                    } else {
137                        false
138                    }
139                },
140                |af, ae, bf, be| match (af, bf) {
141                    (Flow::Sink, Flow::Source) => (Flow::Sink, ae),
142                    (Flow::Source, Flow::Sink) => (Flow::Source, be),
143                    _ => panic!("Cannot sew hedges with flow {:?} and {:?}", af, bf),
144                },
145            )
146            .map_err(|e| eyre::eyre!("Graph sewing failed: {:?}", e))?;
147
148        Ok(self.n_fermion_loops() - internal)
149    }
150
151    pub fn debug_dot(&self) -> String {
152        DotGraph::from(self).debug_dot()
153    }
154    pub(crate) fn hedge_order(&self, model: &Model) -> Result<Vec<u8>> {
155        let mut hedges = vec![None; self.n_hedges()];
156
157        for (_, neighs, v) in self.iter_nodes() {
158            self.process_vertex_hedges(&mut hedges, neighs, v, model)?;
159        }
160
161        hedges
162            .into_iter()
163            .collect::<Option<Vec<u8>>>()
164            .ok_or_else(|| eyre!("Nodes do not cover hedges"))
165    }
166
167    fn process_vertex_hedges(
168        &self,
169        hedges: &mut [Option<u8>],
170        neighs: impl Iterator<Item = Hedge>,
171        vertex: &ParseVertex,
172        model: &Model,
173    ) -> Result<()> {
174        let (mut particles, vertex_name) = Self::extract_vertex_particles(vertex);
175        let mut other_order = particles.len();
176
177        let hedge_vec: Vec<_> = neighs.collect();
178        let oriented_particles =
179            extract_oriented_particles_from_vertex_hedges(self, hedge_vec.iter().copied(), model);
180
181        // Create iterator that pairs each hedge with its oriented particle (if any)
182        let mut particle_iter = oriented_particles.into_iter();
183
184        for h in hedge_vec {
185            let eid = self[&h];
186
187            let order = if self[eid].is_dummy || self[eid].particle.particle().is_none() {
188                other_order += 1;
189                (other_order - 1) as u8
190            } else {
191                // This hedge has a particle, so get the next oriented particle
192                let oriented_particle = particle_iter
193                    .next()
194                    .expect("Mismatch between hedges and oriented particles");
195                debug!("Oriented particle: {h} : {}", oriented_particle.name);
196                // Try to match with vertex rule particles
197                if let Some(name) = &vertex_name {
198                    if let Some((pos, matched_particle)) = particles
199                        .iter_mut()
200                        .find_position(|p| **p == Some(oriented_particle.clone()))
201                    {
202                        *matched_particle = None;
203                        pos as u8
204                    } else {
205                        return Err(eyre!(
206                            "Particle {} not in vertex rule {}",
207                            oriented_particle.name.to_string(),
208                            name
209                        ));
210                    }
211                } else {
212                    other_order += 1;
213                    (other_order - 1) as u8
214                }
215            };
216
217            hedges[h.0] = Some(order);
218        }
219
220        // Verify all particles in vertex rule were matched
221        if particles.iter().any(|p| p.is_some()) {
222            return Err(eyre!(
223                "Particles to vertex rules no match for set: {:?}",
224                particles
225            ));
226        }
227
228        Ok(())
229    }
230
231    fn extract_vertex_particles(
232        vertex: &ParseVertex,
233    ) -> (Vec<Option<crate::model::ArcParticle>>, Option<String>) {
234        if let Some(vertex_rule) = &vertex.vertex_rule {
235            let particles = vertex_rule
236                .particles
237                .iter()
238                .map(|p| Some(p.clone()))
239                .collect();
240            (particles, Some(vertex_rule.name.to_string()))
241        } else {
242            (Vec::new(), None)
243        }
244    }
245
246    #[instrument(skip_all, fields(graph= %graph.to_dot(),name = %graph_name.as_ref(),external_connections = ?external_connections))]
247    pub(crate) fn from_symbolica_graph(
248        model: &Model,
249        graph_name: impl AsRef<str>,
250        graph: &SymbolicaGraph<NodeColorWithVertexRule, EdgeColor>,
251        symmetry_factor: Atom,
252        external_connections: &[(Option<usize>, Option<usize>)],
253    ) -> Result<Self> {
254        fn mark_edge_as_seen(seen_edges: &mut AHashSet<usize>, edge_idx: usize) -> Result<()> {
255            if !seen_edges.insert(edge_idx) {
256                return Err(eyre!(
257                    "External connections must be unique: edge {} already used",
258                    edge_idx
259                ));
260            }
261            Ok(())
262        }
263
264        fn validate_edge_compatibility(
265            out_edge: &symbolica::graph::Edge<EdgeColor>,
266            in_edge: &symbolica::graph::Edge<EdgeColor>,
267            in_id: usize,
268            out_id: usize,
269        ) -> Result<()> {
270            if out_edge.directed != in_edge.directed {
271                return Err(eyre!(
272                    "External edges must have the same directedness, for edge ids {} and {} found {:?} and {:?}",
273                    in_id,
274                    out_id,
275                    in_edge,
276                    out_edge
277                ));
278            }
279
280            if in_edge.data.pdg.abs() != out_edge.data.pdg.abs() {
281                return Err(eyre!(
282                    "External edges must have the same pdg in abs, for edge ids {} and {} found {:?} and {:?}",
283                    in_id,
284                    out_id,
285                    in_edge,
286                    out_edge
287                ));
288            }
289
290            Ok(())
291        }
292
293        /// Add dangling hedges based on external connections
294        /// external connections is provided in  the process definition order
295        /// it maps the external_tag s of the external degree 1 nodes together
296        #[allow(clippy::too_many_arguments)]
297        fn process_single_connection_internal(
298            edge_idx: usize,
299            flow: Flow,
300            hedge: Option<Hedge>,
301            vertex_map: &AHashMap<usize, NodeIndex>,
302            seen_edges: &mut AHashSet<usize>,
303            graph: &SymbolicaGraph<NodeColorWithVertexRule, EdgeColor>,
304            model: &Model,
305            builder: &mut HedgeGraphBuilder<ParseEdge, ParseVertex, ParseHedgeData>,
306        ) -> Result<()> {
307            // Only mark as seen if not already processed (for bidirectional case)
308            if !seen_edges.contains(&edge_idx) {
309                mark_edge_as_seen(seen_edges, edge_idx)?;
310            }
311
312            let edge = &graph.edges()[edge_idx];
313            let mut data = ParseEdge::from_symbolica_edge(model, &edge.data, hedge);
314
315            let (out_vertex, in_vertex) = edge.vertices;
316            let mut orientation = data.particle.orientation();
317
318            // Determine which vertex to connect to and adjust particle/orientation if needed
319            let (node_idx, final_orientation, final_flow) = match flow {
320                Flow::Source => {
321                    if let Some(&sink_node) = vertex_map.get(&in_vertex) {
322                        data.particle = data.particle.reverse(model);
323                        orientation = orientation.reverse();
324                        (sink_node, orientation, flow)
325                    } else if let Some(&source_node) = vertex_map.get(&out_vertex) {
326                        (source_node, orientation, flow)
327                    } else {
328                        return Err(eyre!(
329                            "Outgoing external edges must be attached to an external node (degree 1)"
330                        ));
331                    }
332                }
333                Flow::Sink => {
334                    if let Some(&sink_node) = vertex_map.get(&in_vertex) {
335                        (sink_node, orientation, flow)
336                    } else if let Some(&source_node) = vertex_map.get(&out_vertex) {
337                        data.particle = data.particle.reverse(model);
338                        orientation = orientation.reverse();
339                        (source_node, orientation, flow)
340                    } else {
341                        return Err(eyre!(
342                            "Incoming external edges must be attached to an external node (degree 1)"
343                        ));
344                    }
345                }
346            };
347
348            // debug!(node =  %node_idx, edge_data = ?data ,"adding_external_edge");
349            builder.add_external_edge(node_idx, data, final_orientation, final_flow);
350            Ok(())
351        }
352
353        // debug!("Input:{}", graph.to_dot());
354        let mut builder = HedgeGraphBuilder::new();
355
356        let mut tags_to_edge_id = BTreeMap::new();
357        let mut vertex_map = AHashMap::new();
358        for (i, n) in graph.nodes().iter().enumerate() {
359            if n.edges.len() == 1 {
360                tags_to_edge_id.insert(n.data.external_tag, n.edges[0]);
361            } else {
362                vertex_map.insert(i, builder.add_node(ParseVertex::from(&n.data)));
363            }
364        }
365
366        let mut seen_edges = AHashSet::new();
367        let mut generation_type: Option<GenerationType> = None;
368
369        // first add incoming edges
370        for (i, &(in_tag, out_tag)) in external_connections.iter().enumerate() {
371            if let Some(in_tag) = in_tag {
372                let in_edge_idx = tags_to_edge_id[&(in_tag as i32)];
373                mark_edge_as_seen(&mut seen_edges, in_edge_idx)?;
374                let in_edge = &graph.edges()[in_edge_idx];
375
376                let is_cut_hedge = if let Some(out_id) = out_tag {
377                    let out_edge_idx = tags_to_edge_id[&(out_id as i32)];
378                    mark_edge_as_seen(&mut seen_edges, out_edge_idx)?;
379
380                    let out_edge = &graph.edges()[out_edge_idx];
381
382                    validate_edge_compatibility(out_edge, in_edge, in_tag, out_id)?;
383                    if let Some(existing_type) = &generation_type {
384                        if *existing_type != GenerationType::CrossSection {
385                            return Err(eyre!(
386                                "Cannot have both incoming and outgoing external connections for amplitudes"
387                            ));
388                        }
389                    } else {
390                        generation_type = Some(GenerationType::CrossSection);
391                    }
392                    Some(Hedge(i))
393                } else {
394                    None
395                };
396
397                process_single_connection_internal(
398                    in_edge_idx,
399                    Flow::Sink,
400                    is_cut_hedge,
401                    &vertex_map,
402                    &mut seen_edges,
403                    graph,
404                    model,
405                    &mut builder,
406                )?;
407            } else if out_tag.is_some() {
408                if let Some(existing_type) = &generation_type {
409                    if *existing_type != GenerationType::Amplitude {
410                        return Err(eyre!(
411                            "Cannot mix single directional connections with bidirectional ones for cross sections"
412                        ));
413                    }
414                } else {
415                    generation_type = Some(GenerationType::Amplitude);
416                }
417            }
418        }
419
420        // then add outgoing amplitude edges
421        for (in_id, out_id) in external_connections.iter() {
422            if let Some(out_id) = out_id
423                && in_id.is_none()
424            {
425                let out_edge_idx = tags_to_edge_id[&(*out_id as i32)];
426                mark_edge_as_seen(&mut seen_edges, out_edge_idx)?;
427
428                process_single_connection_internal(
429                    out_edge_idx,
430                    Flow::Source,
431                    None,
432                    &vertex_map,
433                    &mut seen_edges,
434                    graph,
435                    model,
436                    &mut builder,
437                )?;
438            }
439        }
440
441        // Add internal edges
442        for (i, edge) in graph.edges().iter().enumerate() {
443            if seen_edges.contains(&i) {
444                continue;
445            }
446            let (source_v, sink_v) = edge.vertices;
447
448            let source = vertex_map[&source_v];
449            let sink = vertex_map[&sink_v];
450            let data = ParseEdge::from_symbolica_edge(model, &edge.data, None);
451            let orientation = data.particle.orientation();
452            builder.add_edge(source, sink, data, orientation);
453        }
454
455        // then add outgoing cross_section edges
456        for (i, &(in_id, out_id)) in external_connections.iter().enumerate() {
457            if let Some(out_id) = out_id
458                && in_id.is_some()
459            {
460                let out_edge_idx = tags_to_edge_id[&(out_id as i32)];
461
462                process_single_connection_internal(
463                    out_edge_idx,
464                    Flow::Source,
465                    Some(Hedge(i)),
466                    &vertex_map,
467                    &mut seen_edges,
468                    graph,
469                    model,
470                    &mut builder,
471                )?;
472            }
473        }
474
475        let mut parsed = ParseGraph {
476            global_data: ParseData {
477                name: graph_name.as_ref().into(),
478                overall_factor: symmetry_factor,
479                ..Default::default()
480            },
481            graph: builder.into(),
482        };
483
484        debug!("Parsing {}", parsed.debug_dot());
485        parsed.fix_cp_vertex_rules(model)?;
486        debug!("Parsing fixed{}", parsed.debug_dot());
487        Ok(parsed)
488    }
489
490    fn fix_cp_vertex_rules(&mut self, model: &Model) -> Result<()> {
491        let mut new_nodes = AHashMap::new();
492        for (node_id, neighs, v) in self.graph.iter_nodes() {
493            let (particles, vertex_name) = Self::extract_vertex_particles(v);
494            let Some(vertex) = vertex_name.map(|a| model.get_vertex_rule(a)) else {
495                continue;
496            };
497            let hedge_vec: Vec<_> = neighs.collect();
498            let particles = particles.into_iter().flatten().sorted().collect_vec();
499            let mut oriented_particles = extract_oriented_particles_from_vertex_hedges(
500                self,
501                hedge_vec.iter().copied(),
502                model,
503            );
504            oriented_particles.sort();
505
506            let couplings = vertex.coupling_orders(model);
507            // let particles_n = particles.iter().map(|p| p.name.as_str()).collect_vec();
508            // let particles_vn = oriented_particles
509            //     .iter()
510            //     .map(|p| p.name.as_str())
511            //     .collect_vec();
512
513            // debug!(
514            //     "Comparing  vertex rules particles {:?} with incoming particles {:?}",
515            //     particles_n, particles_vn
516            // );
517
518            if particles != oriented_particles {
519                debug!("Need to change");
520                let cp_particles: Vec<_> = oriented_particles
521                    .iter()
522                    .map(|a| a.get_anti_particle(model))
523                    .sorted()
524                    .collect();
525                if cp_particles == particles {
526                    let res = model
527                        .particle_set_to_vertex_rules_map
528                        .get(&oriented_particles);
529
530                    if let Some(res) = res {
531                        let possible: Vec<_> = res
532                            .iter()
533                            .filter(|a| a.coupling_orders(model) == couplings)
534                            .collect();
535
536                        if possible.len() == 1 {
537                            new_nodes.insert(node_id, possible[0].clone());
538                        } else {
539                            let particles = particles.iter().map(|p| p.name.as_str()).collect_vec();
540
541                            return Err(eyre!(
542                                "Multiple compatible  vertex rules for {:?}",
543                                particles
544                            ));
545                        }
546                    } else {
547                        return Err(eyre!(
548                            "Failed to find CP vertex rule for particles: {:?} for node {node_id} in graph {}",
549                            particles,
550                            self.global_data.name,
551                        ));
552                    }
553                } else {
554                    let particles = particles.iter().map(|p| p.name.as_str()).collect_vec();
555                    return Err(eyre!(
556                        "Failed to find CP vertex rule for particles: {:?} for node {node_id} in graph {}",
557                        particles,
558                        self.global_data.name,
559                    ));
560                }
561            }
562        }
563
564        for (node_id, vr) in new_nodes {
565            debug!("New vr for {node_id}:{}", vr.name);
566            self.graph[node_id].vertex_rule = Some(vr);
567        }
568        Ok(())
569    }
570}
571
572impl ParseGraph {
573    pub(crate) fn from_parsed(graph: DotGraph, model: &Model) -> Result<Self> {
574        warn_about_unknown_attributes(&graph);
575        let global_data = graph.global_data.into();
576        let graph = graph
577            .graph
578            .map_data_ref_result(
579                |_, _, v| Ok(v),
580                ParseEdge::parse(model),
581                ParseHedgeData::parse(),
582            )?
583            .map_data_ref_result(
584                ParseVertex::parse(model, &global_data),
585                |_, _, _, e| Ok(e.map(Clone::clone)),
586                |(_, h)| Ok(h.clone()),
587            )?;
588
589        Ok(Self { graph, global_data })
590    }
591}
592
593/// Helper struct to hold initial data extracted from ParseGraph
594struct InitialGraphData {
595    overall_factor: Atom,
596    global_prefactor: GlobalPrefactor,
597    additional_params: Vec<Atom>,
598    add_polarizations: bool,
599    group_id: Option<GroupId>,
600    is_group_master: bool,
601    name: String,
602}
603
604/// Result of processing cut edges
605struct CutProcessingResult {
606    lmb_ids: BTreeMap<LoopIndex, EdgeIndex>,
607    xs_ext_id: BTreeMap<Hedge, (EdgeIndex, Hedge)>,
608    initial_hedges: SuBitGraph,
609    full_cut: SuBitGraph,
610}
611
612impl CutProcessingResult {
613    fn permute(&mut self, graph: &mut NumGraph) -> Result<()> {
614        let (h_perm, edge_perm): (Vec<_>, Vec<_>) = self
615            .xs_ext_id
616            .iter()
617            .enumerate()
618            .map(|(target_pos, (_, (edge_idx, h_id)))| {
619                ((h_id.0, target_pos), (edge_idx.0, target_pos))
620            })
621            .unzip();
622
623        let per = Permutation::from_mappings(edge_perm, graph.n_edges()).unwrap();
624        let perh = Permutation::from_mappings(h_perm, graph.n_hedges()).unwrap();
625
626        debug!("Before: {}", graph.dot(&self.initial_hedges));
627        <HedgeGraph<_, _, _> as Swap<Hedge>>::permute(graph, &perh);
628        let trans = perh.transpositions();
629
630        for (i, j) in trans.into_iter().rev() {
631            self.full_cut.swap(Hedge(i), Hedge(j));
632            // self.initial_hedges.swap(i, j);// initial hedges is already assuming permuted hedges
633        }
634
635        debug!("Before after: {}", graph.dot(&self.initial_hedges));
636        <HedgeGraph<_, _, _> as Swap<EdgeIndex>>::permute(graph, &per);
637
638        debug!(" after: {}", graph.dot(&self.initial_hedges));
639        Ok(())
640    }
641}
642
643/// Edge and vertex numerators
644struct NumeratorData {
645    color_edge: EdgeVec<Atom>,
646    spin_edge: EdgeVec<Atom>,
647    color_vertex: Vec<Option<ParamTensor<OrderedStructure<Euclidean, Aind>>>>,
648    spin_vertex: Vec<Option<ParamTensor<OrderedStructure<Euclidean, Aind>>>>,
649}
650
651fn display_graph_source_path(path: &Path) -> PathBuf {
652    if path.is_absolute() {
653        return path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
654    }
655
656    let joined = std::env::current_dir()
657        .map(|cwd| cwd.join(path))
658        .unwrap_or_else(|_| path.to_path_buf());
659    joined.canonicalize().unwrap_or(joined)
660}
661
662impl Graph {
663    pub fn dot_serialize(&self, settings: &DotExportSettings) -> String {
664        let mut out = String::new();
665        self.dot_serialize_fmt(&mut out, settings).unwrap();
666        out
667    }
668
669    pub(crate) fn dot_serialize_io(
670        &self,
671        writer: &mut impl std::io::Write,
672        settings: &DotExportSettings,
673    ) -> Result<(), std::io::Error> {
674        let g = self.to_dot_graph_with_settings(settings);
675        g.write_io(writer)
676    }
677
678    #[allow(dead_code)]
679    pub(crate) fn dot_split_serialize_io(
680        &self,
681        writer: &mut impl std::io::Write,
682    ) -> Result<(), std::io::Error> {
683        let g = self.to_split_dotgraph();
684        g.write_io(writer)
685    }
686
687    pub fn dot_serialize_fmt(
688        &self,
689        writer: &mut impl std::fmt::Write,
690        settings: &DotExportSettings,
691    ) -> Result<(), std::fmt::Error> {
692        let g = self.to_dot_graph_with_settings(settings);
693        g.write_fmt(writer)
694    }
695
696    pub(crate) fn from_parsed_with_validation(graph: ParseGraph, model: &Model) -> Result<Self> {
697        let res = Self::from_parsed(graph, model)?;
698        res.validate_full_numerator_tensor_network()
699            .with_context(|| {
700                format!(
701                    "Failed to validate full numerator tensor network for graph {}",
702                    res.name
703                )
704            })?;
705        Ok(res)
706    }
707
708    #[instrument(skip_all, fields(graph= %graph.debug_dot(),name = %graph.global_data.name.as_str()))]
709    pub(crate) fn from_parsed(graph: ParseGraph, model: &Model) -> Result<Self> {
710        let (initial_data, mut graph) = Self::extract_initial_data(&graph, model)?;
711
712        // Sew the graph based on cut edges
713        graph
714            .sew(
715                |_, ae, _, be| {
716                    if let (Some(a), Some(b)) = (ae.data.is_cut, be.data.is_cut) {
717                        a == b
718                    } else {
719                        false
720                    }
721                },
722                |af, ae, bf, be| match (af, bf) {
723                    (Flow::Sink, Flow::Source) => (Flow::Sink, ae),
724                    (Flow::Source, Flow::Sink) => (Flow::Source, be),
725                    _ => panic!("Cannot sew hedges with flow {:?} and {:?}", af, bf),
726                },
727            )
728            .map_err(|e| eyre::eyre!("Graph sewing failed: {:?}", e))?;
729
730        let mut cut_result = Self::process_cut_edges(&graph)?;
731
732        cut_result.permute(&mut graph)?;
733
734        let numerators = Self::generate_numerators(&graph, model)?;
735
736        let initial_state_cut =
737            OrientedCut::from_underlying_strict(cut_result.initial_hedges, &graph)?;
738
739        let (global_prefactor, param_builder) = Self::setup_global_prefactor_and_params(
740            initial_data.global_prefactor,
741            initial_data.add_polarizations,
742            initial_data.additional_params.clone(),
743            &initial_state_cut,
744            &graph,
745            model,
746        )
747        .with_context(|| {
748            format!(
749                "Failed to setup_global_prefactor_and_params  for graph {}",
750                initial_data.name
751            )
752        })?;
753
754        let underlying = Self::build_underlying_graph(
755            graph,
756            &initial_state_cut,
757            &numerators,
758            model,
759            &param_builder,
760        )
761        .with_context(|| format!("Failed to build underlying graph {}", initial_data.name))?;
762
763        let loop_momentum_basis = Self::setup_loop_momentum_basis(
764            &underlying,
765            &cut_result.full_cut,
766            &cut_result.lmb_ids,
767            &cut_result.xs_ext_id,
768        )
769        .with_context(|| format!("Failed to build lmb for  graph {}", initial_data.name))?;
770
771        let mut full_without_initials = underlying.full_filter();
772        full_without_initials.subtract_with(&initial_state_cut.left);
773        let mut tree_edges = underlying.bridges_of(&full_without_initials);
774        tree_edges.union_with(&initial_state_cut.left);
775
776        let mut g = Graph {
777            overall_factor: initial_data.overall_factor,
778            polarizations: global_prefactor.polarizations(),
779            global_prefactor,
780            tree_edges,
781            name: initial_data.name,
782            loop_momentum_basis,
783            initial_state_cut,
784            underlying,
785            surface_cache: SurfaceCache::new(),
786            group_id: initial_data.group_id,
787            is_group_master: initial_data.is_group_master,
788            param_builder,
789        };
790
791        let external_momentum_edge_order = g.external_momentum_edge_order();
792        g.loop_momentum_basis
793            .canonicalize_external_order(&external_momentum_edge_order);
794
795        let updated_param_builder_with_lmb = ParamBuilder::new(
796            &g,
797            model,
798            &g.loop_momentum_basis,
799            initial_data.additional_params,
800        );
801
802        debug!(
803            "Updated param builder with LMB: {}\n{}",
804            g.loop_momentum_basis,
805            updated_param_builder_with_lmb.table(),
806        );
807
808        g.param_builder = updated_param_builder_with_lmb;
809
810        debug!("{}", g.debug_dot());
811
812        Ok(g)
813    }
814
815    fn validate_full_numerator_tensor_network(&self) -> Result<()> {
816        let full_num = self
817            .numerator(&self.full_filter(), &self.empty_subgraph())
818            .get_single_atom()
819            .unwrap()
820            * &self.global_prefactor.num
821            * &self.global_prefactor.projector
822            * &self.overall_factor;
823        let color_simplified = full_num
824            .as_view()
825            .simplify_color_with(ColorSimplifySettings::default().with_cof_dimension_invariants());
826        if !full_num.is_zero() && color_simplified.is_zero() {
827            warn!(
828                "Full numerator for graph '{}' becomes zero after color algebra. The graph/projector color structure likely annihilates the amplitude.",
829                self.name
830            );
831        }
832        let net = full_num
833            .parse_to_symbolic_net::<Aind>(&ParseSettings::default())
834            .map_err(Report::from)?;
835        let dangling = net.graph.dangling_indices();
836        if !dangling.is_empty() {
837            return Err(eyre!(
838                "Full numerator still has dangling tensor indices: \n{}",
839                dangling
840                    .iter()
841                    .map(|slot| format!(
842                        "{}:{}",
843                        slot.to_atom().log_print(None),
844                        slot.to_atom().to_plain_string()
845                    ))
846                    .join(",\n")
847            ));
848        }
849
850        Ok(())
851    }
852
853    fn extract_initial_data(
854        parse_graph: &ParseGraph,
855        model: &Model,
856    ) -> Result<(InitialGraphData, NumGraph)> {
857        let hedge_order = parse_graph.hedge_order(model)?;
858        let global_data = &parse_graph.global_data;
859
860        let initial_data = InitialGraphData {
861            additional_params: global_data.parameters.clone(),
862            overall_factor: global_data.overall_factor.clone(),
863            global_prefactor: GlobalPrefactor {
864                num: global_data.num.clone(),
865                projector: global_data.projectors.clone().unwrap_or(Atom::one()),
866            },
867            add_polarizations: global_data.projectors.is_none(),
868            group_id: global_data.group_id,
869            is_group_master: global_data.is_group_master,
870            name: global_data.name.clone(),
871        };
872
873        let num_graph = parse_graph.graph.map_data_ref(
874            |_, _, v| v.clone(),
875            |_, _, _, e| e.map(|e| e.clone()),
876            |h, hd| HedgeData {
877                num_indices: NumIndices::parse(parse_graph)(h, hd),
878                ufo_order: Autogen::from_option_or_generate(hd.ufo_order, || hedge_order[h.0]),
879            },
880        );
881
882        Ok((initial_data, num_graph))
883    }
884
885    fn process_cut_edges(graph: &NumGraph) -> Result<CutProcessingResult> {
886        let mut lmb_ids: BTreeMap<LoopIndex, EdgeIndex> = BTreeMap::new();
887        let mut xs_ext_id: BTreeMap<Hedge, (EdgeIndex, Hedge)> = BTreeMap::new();
888        let mut full_cut: SuBitGraph = graph.full_filter();
889
890        for (p, eid, e) in graph.iter_edges() {
891            let HedgePair::Paired { sink, .. } = p else {
892                if e.data.is_cut.is_some() {
893                    //As we have already sewn the graph, all cut edges must be paired, failure to do so would indicate a bug
894                    return Err(eyre!("Cut edge must be paired"));
895                } else {
896                    continue;
897                }
898            };
899
900            if let Some(lmb_id) = e.data.lmb_id {
901                if let Some(old_value) = lmb_ids.insert(lmb_id, eid) {
902                    return Err(eyre!(
903                        "lmb_id {lmb_id:?} already exists with value {old_value:?}",
904                    ));
905                }
906                debug!("Cutting {eid} for lmb_id{lmb_id}");
907                full_cut.sub(p);
908            } else if let Some(h) = e.data.is_cut {
909                if let Some(old_value) = xs_ext_id.insert(h, (eid, sink)) {
910                    return Err(eyre!("h {h:?} already exists with value {old_value:?}",));
911                }
912                full_cut.sub(p);
913            }
914        }
915
916        // debug!("Graph now:{}", graph.dot(full_cut));
917
918        let mut initial_hedges: SuBitGraph = graph.empty_subgraph();
919        for (target_pos, _) in xs_ext_id.iter().enumerate() {
920            initial_hedges.add(Hedge(target_pos));
921        }
922
923        Ok(CutProcessingResult {
924            full_cut,
925            lmb_ids,
926            xs_ext_id,
927            initial_hedges,
928        })
929    }
930
931    fn generate_numerators(graph: &NumGraph, model: &Model) -> Result<NumeratorData> {
932        let mut color_edge: EdgeVec<_> = vec![Atom::num(1); graph.n_edges()].into();
933        let mut spin_edge: EdgeVec<_> = vec![Atom::i(); graph.n_edges()].into();
934
935        for (p, eid, e) in graph.iter_edges() {
936            if let HedgePair::Paired { source, sink } = p {
937                let prop = e
938                    .data
939                    .particle
940                    .particle()
941                    .map(|p| model.get_propagator_for_particle(&p.name).numerator.clone())
942                    .unwrap_or(Atom::num(1));
943
944                color_edge[eid] = graph[source].color_kronekers(&graph[sink]);
945
946                let spin_slots = [
947                    &graph[source].num_indices.spin_indices.edge_indices,
948                    &graph[sink].num_indices.spin_indices.edge_indices,
949                ];
950
951                let momenta = [(Flow::Source, graph[&source]), (Flow::Sink, graph[&sink])];
952                let spin_nume = UFO.reindex_spin(&spin_slots, &momenta, prop, |i| {
953                    Aind::Edge(usize::from(eid) as u16, i as u16)
954                })?;
955
956                spin_edge[eid] = spin_nume;
957            }
958        }
959
960        let (color_vertex, spin_vertex) = Self::generate_vertex_numerators(graph)?;
961
962        Ok(NumeratorData {
963            color_edge,
964            spin_edge,
965            color_vertex,
966            spin_vertex,
967        })
968    }
969
970    fn setup_global_prefactor_and_params<'a, A: Into<AtomOrView<'a>>, P: IntoIterator<Item = A>>(
971        mut global_prefactor: GlobalPrefactor,
972        add_polarizations: bool,
973        params: P,
974        initial_state_cut: &OrientedCut,
975        graph: &NumGraph,
976        model: &Model,
977    ) -> Result<(GlobalPrefactor, ParamBuilder)> {
978        debug!("Initial state cut: {}", graph.dot(&initial_state_cut.left));
979
980        if add_polarizations {
981            let external_edges = initial_state_cut
982                .left
983                .union(initial_state_cut)
984                .union(&graph.external_filter::<SuBitGraph>());
985            let polarizations = graph.generate_polarizations_of(&external_edges);
986            global_prefactor.projector *= polarizations;
987        }
988
989        let polarizations = global_prefactor.polarizations();
990        let param_builder =
991            ParamBuilder::new(&(&polarizations, graph), model, &graph.lmb(), params);
992
993        Ok((global_prefactor, param_builder))
994    }
995
996    #[allow(clippy::type_complexity)]
997    fn generate_vertex_numerators(
998        graph: &NumGraph,
999    ) -> Result<(
1000        Vec<Option<ParamTensor<OrderedStructure<Euclidean, Aind>>>>,
1001        Vec<Option<ParamTensor<OrderedStructure<Euclidean, Aind>>>>,
1002    )> {
1003        let mut color_vertex: Vec<Option<ParamTensor<OrderedStructure<Euclidean, Aind>>>> =
1004            vec![None; graph.n_nodes()];
1005        let mut spin_vertex = color_vertex.clone();
1006
1007        for (ni, c, v) in graph.iter_nodes() {
1008            let mut color_slots = vec![];
1009            let mut spin_slots = vec![];
1010            let mut order = vec![];
1011            let mut momenta = vec![];
1012            for h in c {
1013                color_slots.push(&graph[h].num_indices.color_indices.vertex_indices);
1014                spin_slots.push(&graph[h].num_indices.spin_indices.vertex_indices);
1015                order.push(graph[h].ufo_order.value);
1016                momenta.push((graph.flow(h), graph[&h]));
1017            }
1018
1019            let perm = Permutation::sort(&order);
1020            perm.apply_slice_in_place(&mut color_slots);
1021            perm.apply_slice_in_place(&mut spin_slots);
1022            perm.apply_slice_in_place(&mut momenta);
1023
1024            let Some(vertex_rule) = &v.vertex_rule else {
1025                continue;
1026            };
1027
1028            let [mut color_structure, couplings, mut spin_structure] =
1029                vertex_rule.tensors(ni.aind(1), ni.aind(0));
1030
1031            spin_structure.map_data_ref_mut_result(|a| {
1032                *a =
1033                    UFO.reindex_spin(&spin_slots, &momenta, (*a).clone(), |u| ni.aind(u as u16))?;
1034
1035                Ok(())
1036            })?;
1037
1038            // couplings.map_data_mut(|a| *a = UFO.normalize_complex((*a).clone()));
1039
1040            color_structure.map_data_ref_mut_result(|a| {
1041                *a = UFO.reindex_color(&color_slots, (*a).clone(), |u| ni.aind(u as u16))?;
1042                Ok(())
1043            })?;
1044
1045            spin_vertex[ni.0] = Some(spin_structure.contract(&couplings).unwrap());
1046            color_vertex[ni.0] = Some(color_structure);
1047        }
1048
1049        Ok((color_vertex, spin_vertex))
1050    }
1051
1052    fn build_underlying_graph(
1053        graph: NumGraph,
1054        initial_state_cut: &OrientedCut,
1055        numerators: &NumeratorData,
1056        model: &Model,
1057        param_builder: &ParamBuilder,
1058    ) -> Result<UnderlyingGraph> {
1059        let mut loop_edge_filter = graph.full_filter();
1060        loop_edge_filter.subtract_with(&graph.bridges_of(&loop_edge_filter));
1061        let mut vertex_color_nums = numerators.color_vertex.clone();
1062        let mut vertex_spin_nums = numerators.spin_vertex.clone();
1063        let intermediate: UnderlyingGraph = graph.map_result(
1064            |_, i, v| {
1065                let num = match v.num {
1066                    Some(num) => Autogen::explicit(num),
1067                    None => Autogen::generated(
1068                        vertex_spin_nums[i.0]
1069                            .take()
1070                            .unwrap()
1071                            .contract(&vertex_color_nums[i.0].take().unwrap())
1072                            .unwrap()
1073                            .scalar()
1074                            .unwrap(),
1075                    ),
1076                };
1077
1078                let dod = match v.dod {
1079                    Some(dod) => Autogen::explicit(dod),
1080                    None => Autogen::generated(if num.autogenerated {
1081                        v.vertex_rule.as_ref().map(|vr| vr.dod).unwrap_or(0)
1082                    } else {
1083                        num.all_dod()
1084                    }),
1085                };
1086
1087                Ok(Vertex {
1088                    name: Autogen::from_option_or_generate(v.name, || i.to_string()),
1089                    num,
1090                    dod,
1091                    vertex_rule: v.vertex_rule,
1092                })
1093            },
1094            |_, _, p, eid, ed| {
1095                let e = ed.data;
1096                if e.particle.is_fermion() && !e.particle.is_self_antiparticle()&&  e.particle.orientation() != ed.orientation {
1097                    return Err(eyre!(
1098                        "Edge orientation {:?} does not match particle orientation {:?} for edge {},{}",
1099                        ed.orientation,
1100                        e.particle.orientation(),
1101                        eid,
1102                        e
1103                    ));
1104                    }
1105
1106                let mass = EdgeMass::from_atom(e.mass_atom(), model, param_builder)?;
1107
1108                let num = match e.num {
1109                    Some(num) => Autogen::explicit(num),
1110                    None => Autogen::generated(if initial_state_cut.left.intersects(&p) {
1111                        numerators.color_edge[eid].clone()
1112                    } else {
1113                        &numerators.color_edge[eid] * &numerators.spin_edge[eid]
1114                    }),
1115                };
1116
1117                let dod = match e.dod {
1118                    Some(dod) => Autogen::explicit(dod),
1119                    None => Autogen::generated(if num.autogenerated {
1120                        if let Some(particle) = e.particle.particle() {
1121                            model.get_propagator_for_particle(&particle.name).dod
1122                        } else {
1123                            -2
1124                        }
1125                    } else {
1126                        num.edge_dod(eid) -2
1127                    }),
1128                };
1129
1130                Ok(EdgeData::new(
1131                    Edge {
1132                        mass,
1133                        is_dummy: e.is_dummy,
1134                        name: Autogen::from_option_or_generate(e.name, || eid.to_string()),
1135                        particle: e.particle,
1136                        num,
1137                        dod,
1138                        extra_data: EdgeExtraData {
1139                            momtrop_edge_power: e.momtrop_edge_power,
1140                            vakint_edge_power: e.vakint_edge_power,
1141                        }
1142                    },
1143                    ed.orientation,
1144                ))
1145            },
1146            |_, h| Ok(h),
1147        )?;
1148
1149        Ok(intermediate)
1150    }
1151
1152    fn setup_loop_momentum_basis(
1153        underlying: &UnderlyingGraph,
1154        full_cut: &SuBitGraph,
1155        lmb_ids: &BTreeMap<LoopIndex, EdgeIndex>,
1156        xs_ext_id: &BTreeMap<Hedge, (EdgeIndex, Hedge)>,
1157    ) -> Result<LoopMomentumBasis> {
1158        debug!("{}", underlying.dot(full_cut));
1159
1160        let mut loop_momentum_basis = if full_cut.included_iter().next().is_some() {
1161            let mut full = underlying.full_filter();
1162
1163            for (p, _, i) in underlying.iter_edges() {
1164                if i.data.is_dummy {
1165                    full.sub(p);
1166                }
1167            }
1168            let external = underlying.internal_crown(&full);
1169            underlying.lmb_impl(&full, full_cut, external)?
1170        } else {
1171            return Err(eyre!(
1172                "No included edges found in full_cut for loop momentum basis setup"
1173            ));
1174        };
1175
1176        let inv_lmb_ids: BTreeMap<_, _> = lmb_ids
1177            .iter()
1178            .map(|(k, v)| {
1179                // debug!("v{v}k{k}");
1180                (*v, *k)
1181            })
1182            .collect();
1183
1184        for e in 0..xs_ext_id.len() {
1185            let (l, _) = loop_momentum_basis
1186                .loop_edges
1187                .iter()
1188                .find_position(|a| *a == &EdgeIndex(e))
1189                .unwrap();
1190
1191            loop_momentum_basis.put_loop_to_ext(LoopIndex(l));
1192        }
1193
1194        // Process swaps until no more changes needed
1195        let mut swapped = true;
1196        while swapped {
1197            swapped = false;
1198            for i in 0..loop_momentum_basis.loop_edges.len() {
1199                if let Some(&target_pos) =
1200                    inv_lmb_ids.get(&loop_momentum_basis.loop_edges[LoopIndex(i)])
1201                    && target_pos.0 < loop_momentum_basis.loop_edges.len()
1202                    && target_pos.0 != i
1203                {
1204                    loop_momentum_basis.swap_loops(LoopIndex(i), target_pos);
1205                    swapped = true;
1206                    break;
1207                }
1208            }
1209        }
1210
1211        Ok(loop_momentum_basis)
1212    }
1213
1214    pub fn from_dot(graph: DotGraph, model: &Model) -> Result<Self> {
1215        Self::from_parsed(ParseGraph::from_parsed(graph, model)?, model)
1216    }
1217    pub fn from_file<P>(p: P, model: &Model) -> Result<Vec<Self>>
1218    where
1219        P: AsRef<Path>,
1220    {
1221        Self::from_path(p, model)
1222    }
1223
1224    pub fn from_path<P>(p: P, model: &Model) -> Result<Vec<Self>>
1225    where
1226        P: AsRef<Path>,
1227    {
1228        let path = p.as_ref();
1229
1230        if path.is_dir() {
1231            // Load all .dot files from directory
1232            let mut all_graphs = Vec::new();
1233            let entries = std::fs::read_dir(path)
1234                .with_context(|| format!("Failed to read directory: {}", path.display()))?;
1235
1236            let mut dot_files = Vec::new();
1237            for entry in entries {
1238                let entry = entry?;
1239                let file_path = entry.path();
1240                if file_path.is_file() && file_path.extension().is_some_and(|ext| ext == "dot") {
1241                    dot_files.push(file_path);
1242                }
1243            }
1244
1245            // Sort files for consistent ordering
1246            dot_files.sort();
1247
1248            for dot_file in dot_files {
1249                let graphs = Self::from_single_file(&dot_file, model)?;
1250                all_graphs.extend(graphs);
1251            }
1252
1253            if all_graphs.is_empty() {
1254                return Err(eyre!(
1255                    "No .dot files found in directory: {}",
1256                    path.display()
1257                ));
1258            }
1259
1260            Ok(all_graphs)
1261        } else {
1262            // Load single file
1263            Self::from_single_file(path, model)
1264        }
1265    }
1266
1267    fn from_single_file<P>(p: P, model: &Model) -> Result<Vec<Self>>
1268    where
1269        P: AsRef<Path>,
1270    {
1271        let hedge_graph_set: GraphSet<
1272            DotEdgeData,
1273            DotVertexData,
1274            DotHedgeData,
1275            linnet::parser::GlobalData,
1276            NodeStorageVec<DotVertexData>,
1277        > = GraphSet::from_file(p.as_ref()).map_err(|a| match a {
1278            HedgeParseError::GraphFromFile(e) => match e.as_ref() {
1279                dot_parser::ast::GraphFromFileError::FileError(e) => eyre!(e.to_string())
1280                    .with_note(|| {
1281                        format!(
1282                            "Tried to access the file at: {}",
1283                            display_graph_source_path(p.as_ref()).display()
1284                        )
1285                    }),
1286                dot_parser::ast::GraphFromFileError::ParseError(e) => {
1287                    eyre!("Dot parsing error: {}", e)
1288                }
1289                dot_parser::ast::GraphFromFileError::PestParseError(e) => {
1290                    eyre!(e.to_string())
1291                }
1292            },
1293            HedgeParseError::ParseError(i) => color_eyre::Report::from(i),
1294            _ => {
1295                eyre!("Hedge parse error")
1296            }
1297        })?;
1298        Self::from_hedge_graph_set(hedge_graph_set, model)
1299    }
1300
1301    pub fn from_string<Str: AsRef<str>>(s: Str, model: &Model) -> Result<Vec<Self>> {
1302        let hedge_graph_set: GraphSet<
1303            DotEdgeData,
1304            DotVertexData,
1305            DotHedgeData,
1306            linnet::parser::GlobalData,
1307            NodeStorageVec<DotVertexData>,
1308        > = GraphSet::from_string(s).map_err(|a| match a {
1309            HedgeParseError::GraphFromFile(e) => match e.as_ref() {
1310                dot_parser::ast::GraphFromFileError::FileError(e) => {
1311                    eyre!(e.to_string())
1312                }
1313                dot_parser::ast::GraphFromFileError::ParseError(e) => {
1314                    eyre!("Dot parsing error: {}", e)
1315                }
1316                dot_parser::ast::GraphFromFileError::PestParseError(e) => {
1317                    eyre!(e.to_string())
1318                }
1319            },
1320            HedgeParseError::ParseError(i) => color_eyre::Report::from(i),
1321            _ => {
1322                eyre!("Hedge parse error")
1323            }
1324        })?;
1325
1326        Self::from_hedge_graph_set(hedge_graph_set, model)
1327    }
1328
1329    fn from_hedge_graph_set(
1330        set: GraphSet<
1331            DotEdgeData,
1332            DotVertexData,
1333            DotHedgeData,
1334            linnet::parser::GlobalData,
1335            NodeStorageVec<DotVertexData>,
1336        >,
1337        model: &Model,
1338    ) -> Result<Vec<Self>> {
1339        let mut graphs = Vec::new();
1340
1341        for (graph, global_data) in set.set.into_iter().zip(set.global_data) {
1342            let graph = DotGraph { global_data, graph };
1343            debug!("Parsing: \n{}", graph.debug_dot());
1344            graphs.push(Graph::from_parsed(
1345                ParseGraph::from_parsed(graph, model)?,
1346                model,
1347            )?);
1348        }
1349        Ok(graphs)
1350    }
1351}
1352
1353pub mod serialization;
1354
1355/// completes and extract the user defined group structure on a lis of graphs
1356pub(crate) fn complete_group_parsing(graphs: &mut [Graph]) -> Result<TiVec<GroupId, GraphGroup>> {
1357    // validate the input
1358    let defined_group_ids = graphs
1359        .iter()
1360        .filter_map(|graph| graph.group_id)
1361        .sorted()
1362        .dedup()
1363        .collect_vec();
1364
1365    let expected_group_ids = (0..defined_group_ids.len()).map(GroupId).collect_vec();
1366
1367    if defined_group_ids != expected_group_ids {
1368        return Err(eyre!(
1369            "invalid group ids, group ids must start at 0 and contain no gaps"
1370        ));
1371    }
1372    // now set the remaining group ids
1373    let mut current_group_id = defined_group_ids.len();
1374    for graph in graphs.iter_mut() {
1375        if graph.group_id.is_none() {
1376            graph.group_id = Some(GroupId(current_group_id));
1377            graph.is_group_master = true;
1378            current_group_id += 1;
1379        }
1380    }
1381
1382    let num_groups = current_group_id;
1383
1384    // build the groups
1385    (0..num_groups)
1386        .map(|group_id| {
1387            let group_id = GroupId(group_id);
1388            let graphs_in_group = graphs
1389                .iter()
1390                .enumerate()
1391                .filter_map(|(i, g)| {
1392                    if g.group_id == Some(group_id) {
1393                        Some(i)
1394                    } else {
1395                        None
1396                    }
1397                })
1398                .collect_vec();
1399
1400            // the special case of a single graph in the group is easy
1401            if graphs_in_group.len() == 1 {
1402                graphs[graphs_in_group[0]].is_group_master = true;
1403                Ok(GraphGroup {
1404                    master: graphs_in_group[0],
1405                    remaining: vec![],
1406                })
1407            } else {
1408                // see if a master is defined
1409                let master = graphs_in_group
1410                    .iter()
1411                    .find(|&&i| graphs[i].is_group_master)
1412                    .copied();
1413
1414                if let Some(master) = master {
1415                    // find the remaining graphs and make sure no other master is defined
1416                    let remaining = graphs_in_group
1417                        .into_iter()
1418                        .filter(|&i| i != master)
1419                        .collect_vec();
1420
1421                    let duplicate_master = remaining.iter().any(|&i| graphs[i].is_group_master);
1422
1423                    if duplicate_master {
1424                        return Err(eyre!(
1425                            "Multiple group masters defined for group {group_id:?}"
1426                        ));
1427                    }
1428                    Ok(GraphGroup { master, remaining })
1429                } else {
1430                    // no master defined, take the first graph as master
1431                    let master = graphs_in_group[0];
1432                    graphs[master].is_group_master = true;
1433                    Ok(GraphGroup {
1434                        master,
1435                        remaining: graphs_in_group[1..].to_vec(),
1436                    })
1437                }
1438            }
1439        })
1440        .collect::<Result<TiVec<GroupId, GraphGroup>>>()
1441}
1442
1443pub mod from_dot;
1444pub use from_dot::*;
1445#[cfg(test)]
1446pub mod tests;