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