Skip to main content

gammalooprs/graph/parse/
serialization.rs

1use idenso::{
2    color::{ColorSimplifier, ColorSimplifySettings},
3    dirac::GammaSimplifier,
4    shorthands::metric::MetricSimplifier,
5};
6use linnet::{
7    half_edge::{
8        HedgeGraph,
9        involution::{Flow, HedgePair, Orientation},
10        subgraph::SubSetLike,
11    },
12    parser::{DotEdgeData, DotGraph, DotHedgeData, DotVertexData},
13};
14use spenso::shadowing::symbolica_utils::SpensoPrintSettings;
15use symbolica::atom::AtomCore;
16
17use crate::{
18    graph::Graph,
19    processes::DotExportSettings,
20    utils::{GS, W_},
21    uv::UltravioletGraph,
22};
23
24use super::{ParseGraph, string_utils::ToQuoted};
25
26impl Graph {
27    pub fn to_dot_graph_with_settings(&self, settings: &DotExportSettings) -> DotGraph {
28        let mut dotgraph = if settings.split_xs_by_initial_states {
29            self.to_split_dotgraph_with_autogenerated_fields(settings.include_autogenerated_fields)
30        } else {
31            self.to_dot_graph_with_autogenerated_fields(settings.include_autogenerated_fields)
32        };
33
34        if settings.output_full_numerator {
35            let mut num = (self
36                .numerator(&self.full_filter(), &self.empty_subgraph())
37                .get_single_atom()
38                .unwrap()
39                * &self.global_prefactor.num
40                * &self.global_prefactor.projector
41                * &self.overall_factor)
42                .simplify_metrics();
43
44            if settings.do_color_algebra {
45                num = num.simplify_color_with(
46                    ColorSimplifySettings::default().with_cof_dimension_invariants(),
47                );
48            }
49
50            if settings.do_gamma_algebra {
51                num = num.simplify_gamma();
52                crate::debug_tags!(#generation, #graph, #inspect, #dump;
53                    stage = "graph_serialization_after_simplify_gamma",
54                    log.after_gamma = num,
55                    "Graph serialization after gamma simplification"
56                );
57            }
58
59            dotgraph.global_data.statements.insert(
60                "full_num".into(),
61                num.printer(SpensoPrintSettings::typst_options())
62                    .to_string(),
63            );
64        }
65
66        dotgraph
67    }
68
69    pub fn to_split_dotgraph(&self) -> DotGraph {
70        self.to_split_dotgraph_with_autogenerated_fields(false)
71    }
72
73    fn to_split_dotgraph_with_autogenerated_fields(
74        &self,
75        include_autogenerated_fields: bool,
76    ) -> DotGraph {
77        let global_data = self.global_data(include_autogenerated_fields);
78
79        let mut graph: HedgeGraph<DotEdgeData, DotVertexData, DotHedgeData> =
80            if self.initial_state_cut.is_empty() {
81                self.underlying.map_data_ref(
82                    |_, _, v| v.to_dot_data(include_autogenerated_fields),
83                    |_, _, p, e| {
84                        if let HedgePair::Unpaired { flow, .. } = p {
85                            e.map(|e| {
86                                let mut dot = e.to_dot_data(include_autogenerated_fields);
87                                match flow {
88                                    Flow::Source => {
89                                        dot.add_statement("pos", "x:@-left!".to_string());
90                                    }
91                                    Flow::Sink => {
92                                        dot.add_statement("pos", "x:@+right!".to_string());
93                                    }
94                                }
95                                dot
96                            })
97                        } else {
98                            e.map(|e| e.to_dot_data(include_autogenerated_fields))
99                        }
100                    },
101                    |_, d| d.into(),
102                )
103            } else {
104                self.initial_state_cut
105                    .clone()
106                    .to_owned_graph_ref(&self.underlying)
107                    .map(
108                        |_, _, v| v.to_dot_data(include_autogenerated_fields),
109                        |_, _, p, _, e| {
110                            e.map(|e| {
111                                let mut dot =
112                                    e.edge_data().to_dot_data(include_autogenerated_fields);
113
114                                match e.orientation() {
115                                    Orientation::Default => {
116                                        dot.add_statement("is_cut", self.inv(p.any_hedge()));
117                                        dot.add_statement(
118                                            "pos",
119                                            format!("x:@-left!,y:@edge{}!", e.index),
120                                        );
121                                    }
122                                    Orientation::Reversed => {
123                                        dot.add_statement("is_cut", p.any_hedge());
124                                        dot.add_statement(
125                                            "pos",
126                                            format!("x:@+right!,y:@edge{}!", e.index),
127                                        );
128                                    }
129                                    Orientation::Undirected => {}
130                                }
131                                dot
132                            })
133                        },
134                        |_, d| d.into(),
135                    )
136            };
137
138        // self.normal_emr_replacement(subgraph, lmb, rep_args, filter_pair)
139        for (_, i, _) in self.iter_edges() {
140            let loop_expr = self
141                .loop_momentum_basis
142                .loop_atom(i, GS.loop_mom, &[W_.a___], false);
143            let external_expr =
144                self.loop_momentum_basis
145                    .ext_atom(i, GS.external_mom, &[W_.a___], false);
146
147            graph[i].add_statement("lmb_rep", (loop_expr + external_expr).to_quoted());
148        }
149
150        for (l, i) in self.loop_momentum_basis.loop_edges.iter_enumerated() {
151            graph[i].0.add_statement("lmb_id", l);
152        }
153
154        DotGraph { global_data, graph }
155    }
156
157    fn to_dot_graph_with_autogenerated_fields(
158        &self,
159        include_autogenerated_fields: bool,
160    ) -> DotGraph {
161        let global_data = self.global_data(include_autogenerated_fields);
162
163        let mut graph: HedgeGraph<DotEdgeData, DotVertexData, DotHedgeData> =
164            self.underlying.map_data_ref(
165                |_, _, v| v.to_dot_data(include_autogenerated_fields),
166                |_, _, _, e| e.map(|e| e.to_dot_data(include_autogenerated_fields)),
167                |_, d| d.into(),
168            );
169
170        for (_, i, _) in self.iter_edges() {
171            let loop_expr = self
172                .loop_momentum_basis
173                .loop_atom(i, GS.loop_mom, &[W_.a___], false);
174            let external_expr =
175                self.loop_momentum_basis
176                    .ext_atom(i, GS.external_mom, &[W_.a___], false);
177
178            graph[i].add_statement("lmb_rep", (loop_expr + external_expr).to_quoted());
179        }
180
181        for i in self.initial_state_cut.left.included_iter() {
182            let eid = graph[&i];
183            graph[eid].add_statement("is_cut", i);
184        }
185
186        for (l, i) in self.loop_momentum_basis.loop_edges.iter_enumerated() {
187            graph[i].0.add_statement("lmb_id", l);
188        }
189
190        DotGraph { global_data, graph }
191    }
192}
193
194impl From<&Graph> for DotGraph {
195    fn from(value: &Graph) -> Self {
196        value.to_dot_graph_with_autogenerated_fields(false)
197    }
198}
199
200impl From<&ParseGraph> for DotGraph {
201    fn from(value: &ParseGraph) -> Self {
202        let global_data = value.global_data();
203        let graph: HedgeGraph<DotEdgeData, DotVertexData, DotHedgeData> = value.graph.map_data_ref(
204            |_, _, v| v.into(),
205            |_, _, _, e| e.map(|e| e.into()),
206            |_, d| d.into(),
207        );
208
209        DotGraph { global_data, graph }
210    }
211}
212
213impl ParseGraph {
214    #[allow(dead_code)]
215    fn to_simple_dot(&self) -> DotGraph {
216        let global_data = self.global_data();
217        let graph: HedgeGraph<DotEdgeData, DotVertexData, DotHedgeData> = self.graph.map_data_ref(
218            |_, _, v| v.into(),
219            |_, _, _, e| e.map(|e| e.into()),
220            |_, d| d.into(),
221        );
222
223        DotGraph { global_data, graph }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use linnet::half_edge::involution::EdgeIndex;
230    use linnet::parser::DotGraph;
231    use symbolica::{atom::Atom, function};
232
233    use super::{Graph, ParseGraph};
234    use crate::{
235        dot,
236        graph::{
237            autogen::Autogen,
238            parse::IntoGraph,
239            threshold_counterterms::{
240                THRESHOLD_COUNTERTERM_SCHEMA_VERSION, ThresholdCountertermCut,
241                ThresholdCountertermMultiplier, ThresholdCountertermSpec,
242                ThresholdCountertermThreshold, ThresholdCountertermVariant,
243            },
244        },
245        initialisation::test_initialise,
246        processes::DotExportSettings,
247        utils::{GS, load_generic_model},
248    };
249
250    #[test]
251    fn full_numerator_is_a_spenso_aware_typst_fragment() {
252        let graph: Graph = dot!(digraph G {
253            ext [style=invis]
254            node [num=1]
255            ext -> A
256            C -> A
257            A -> D
258            D -> B
259            B -> C
260            C -> D
261            B -> ext
262        })
263        .unwrap();
264        let graph = graph.with_global_numerator_only(
265            "typst_full_num".to_string(),
266            function!(GS.uv_truncate, Atom::num(1)),
267        );
268        let dot = graph.to_dot_graph_with_settings(&DotExportSettings {
269            output_full_numerator: true,
270            ..DotExportSettings::default()
271        });
272        let full_num = &dot.global_data.statements["full_num"];
273
274        assert_eq!(full_num, r#"op("Tr")(1)"#);
275        assert!(!full_num.contains("gammalooprs::Truncate"));
276    }
277
278    fn triangle() -> Graph {
279        test_initialise().unwrap();
280        dot!(digraph threshold_counterterm_roundtrip {
281            edge [pdg=1000]
282            ext [style=invis]
283            ext -> v4 [id=0]
284            v5 -> ext [id=1]
285            v6 -> ext [id=2]
286            v4 -> v5 [id=3, lmb_id=0]
287            v5 -> v6 [id=4]
288            v6 -> v4 [id=5]
289        }, "scalars")
290        .unwrap()
291    }
292
293    #[test]
294    fn generated_threshold_counterterms_are_exported_only_when_requested() {
295        let graph = triangle();
296        assert!(graph.threshold_counterterms.autogenerated);
297        assert_eq!(
298            graph.threshold_counterterms.value,
299            ThresholdCountertermSpec::default()
300        );
301
302        let ordinary = graph.dot_serialize(&DotExportSettings::default());
303        assert!(!ordinary.contains("threshold_counterterms"));
304
305        let normalized = graph.dot_serialize(&DotExportSettings {
306            include_autogenerated_fields: true,
307            ..DotExportSettings::default()
308        });
309        assert!(normalized.contains("threshold_counterterms"));
310        assert!(normalized.contains("threshold_counterterms = \"\nschema_version = 1\n"));
311        assert!(!normalized.contains(r"schema_version = 1\n"));
312
313        let reparsed: Graph = normalized
314            .into_graph(&load_generic_model("scalars"))
315            .unwrap();
316        assert!(!reparsed.threshold_counterterms.autogenerated);
317        assert_eq!(
318            reparsed.threshold_counterterms.value,
319            ThresholdCountertermSpec::default()
320        );
321        assert!(reparsed.threshold_counterterms.is_legacy_equivalent());
322    }
323
324    #[test]
325    fn explicit_threshold_counterterms_round_trip_embedded_strings() {
326        let mut graph = triangle();
327        let spec = ThresholdCountertermSpec {
328            schema_version: THRESHOLD_COUNTERTERM_SCHEMA_VERSION,
329            function_map: [
330                ("shifted(x)".to_string(), "x + 1".to_string()),
331                ("twice(x)".to_string(), "2 * shifted(x)".to_string()),
332            ]
333            .into(),
334            cuts: vec![ThresholdCountertermCut {
335                edges: vec![],
336                thresholds: vec![ThresholdCountertermThreshold {
337                    edges: vec![EdgeIndex(4), EdgeIndex(5)],
338                    counterterms: vec![ThresholdCountertermVariant {
339                        name: Some(
340                            "quoted \"variant\" η literal:\\n regex:\\d tab:\t line:\n end:\\"
341                                .to_string(),
342                        ),
343                        group_id: None,
344                        subspace: Some(vec![EdgeIndex(3)]),
345                        parent_lmb: Some(vec![EdgeIndex(3)]),
346                        disable: false,
347                        multiplier: Some(ThresholdCountertermMultiplier {
348                            expression: "twice(\n Q3(star, 3, cind(1))\t)".to_string(),
349                            function_map: [("shifted(x)".to_string(), "x + 2".to_string())].into(),
350                            symmetrize: true,
351                            opaque_derivatives: true,
352                        }),
353                    }],
354                }],
355            }],
356        };
357        graph.threshold_counterterms = Autogen::explicit(spec.clone());
358
359        let serialized = graph.dot_serialize(&DotExportSettings::default());
360        assert!(serialized.contains("threshold_counterterms"));
361        assert!(serialized.contains("threshold_counterterms = \"\nschema_version = 1\n"));
362        assert!(!serialized.contains(r"schema_version = 1\n"));
363
364        let reparsed: Graph = serialized
365            .as_str()
366            .into_graph(&load_generic_model("scalars"))
367            .unwrap();
368        assert!(!reparsed.threshold_counterterms.autogenerated);
369        assert_eq!(reparsed.threshold_counterterms.value, spec);
370
371        let dot_graph = DotGraph::from_string(&serialized).unwrap();
372        let parse_graph =
373            ParseGraph::from_parsed(dot_graph, &load_generic_model("scalars")).unwrap();
374        let parse_graph_serialized = DotGraph::from(&parse_graph).debug_dot();
375        assert!(
376            parse_graph_serialized.contains("threshold_counterterms = \"\nschema_version = 1\n")
377        );
378        assert!(!parse_graph_serialized.contains(r"schema_version = 1\n"));
379
380        let reparsed: Graph = parse_graph_serialized
381            .into_graph(&load_generic_model("scalars"))
382            .unwrap();
383        assert_eq!(reparsed.threshold_counterterms.value, spec);
384    }
385}