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 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}