1use std::{
2 fmt::Display,
3 sync::{Arc, Mutex},
4};
5
6use eyre::Context;
7use linnet::{
8 half_edge::{
9 HedgeGraph,
10 involution::{EdgeData, EdgeIndex, Flow, Hedge, HedgePair, Orientation},
11 },
12 parser::{DotEdgeData, DotHedgeData, DotVertexData},
13};
14use spenso::{
15 algebra::complex::Complex,
16 structure::{IndexLess, ScalarStructure, representation::LibraryRep},
17};
18use symbolica::{domains::float::Complex as SymComplex, prelude::*};
19
20use crate::{
21 feyngen::diagram_generator::EdgeColor,
22 integrands::process::ParamBuilder,
23 model::{ArcParticle, Model, UFOSymbol},
24 momentum::{Helicity, sample::LoopIndex},
25 numerator::aind::{Aind, NewAind},
26 utils::{F, FloatLike, GS},
27 uv::uv_graph::UVE,
28};
29
30use super::parse::{StripParse, ToQuoted};
31use crate::graph::Autogen;
32use color_eyre::Result;
33use eyre::eyre;
34
35#[derive(Debug, Clone, bincode_trait_derive::Encode, bincode_trait_derive::Decode)]
36#[trait_decode(trait = crate::GammaLoopContext)]
37pub enum PossibleParticle {
38 Particle(ArcParticle),
39 MassOverriddenParticle { particle: ArcParticle, mass: Atom },
40 JustMass { expr: Atom },
41}
42
43impl From<ArcParticle> for PossibleParticle {
44 fn from(particle: ArcParticle) -> Self {
45 PossibleParticle::Particle(particle)
46 }
47}
48
49impl From<Atom> for PossibleParticle {
50 fn from(atom: Atom) -> Self {
51 PossibleParticle::JustMass { expr: atom }
52 }
53}
54
55impl From<()> for PossibleParticle {
56 fn from(_: ()) -> Self {
57 PossibleParticle::JustMass { expr: Atom::Zero }
58 }
59}
60
61impl PossibleParticle {
62 pub fn reverse(&self, model: &Model) -> Self {
63 match self {
64 PossibleParticle::Particle(p) => PossibleParticle::Particle(p.get_anti_particle(model)),
65 PossibleParticle::MassOverriddenParticle { particle, mass } => {
66 PossibleParticle::MassOverriddenParticle {
67 particle: particle.get_anti_particle(model),
68 mass: mass.clone(),
69 }
70 }
71 PossibleParticle::JustMass { expr } => {
72 PossibleParticle::JustMass { expr: expr.clone() }
73 }
74 }
75 }
76
77 pub fn orientation(&self) -> Orientation {
78 self.particle()
79 .map(|a| {
80 if a.is_fermion() {
81 if a.pdg_code < 0 {
82 Orientation::Reversed
83 } else {
84 Orientation::Default
85 }
86 } else {
87 Orientation::Undirected
88 }
89 })
90 .unwrap_or(Orientation::Undirected)
91 }
92
93 pub fn is_fermion(&self) -> bool {
94 match self {
95 PossibleParticle::Particle(p) => p.is_fermion(),
96 PossibleParticle::MassOverriddenParticle { particle, .. } => particle.is_fermion(),
97 PossibleParticle::JustMass { .. } => false,
98 }
99 }
100
101 pub fn is_self_antiparticle(&self) -> bool {
102 match self {
103 PossibleParticle::Particle(p) => p.is_self_antiparticle(),
104 PossibleParticle::MassOverriddenParticle { particle, .. } => {
105 particle.is_self_antiparticle()
106 }
107 PossibleParticle::JustMass { .. } => false,
108 }
109 }
110 pub fn mass_atom(&self) -> Atom {
111 match &self {
112 PossibleParticle::JustMass { expr, .. } => expr.clone(),
113 PossibleParticle::Particle(p) => p.mass.0.into(),
114 PossibleParticle::MassOverriddenParticle { mass, .. } => mass.clone(),
115 }
116 }
117 pub fn zero() -> Self {
120 ().into()
121 }
122
123 pub(crate) fn override_mass(self, mass: Option<Atom>) -> Self {
124 let Some(mass) = mass else {
125 return self;
126 };
127
128 match self {
129 PossibleParticle::JustMass { .. } => PossibleParticle::JustMass { expr: mass },
130 PossibleParticle::MassOverriddenParticle { particle, .. }
131 | PossibleParticle::Particle(particle) => {
132 PossibleParticle::MassOverriddenParticle { particle, mass }
133 }
134 }
135 }
136
137 pub(crate) fn color_reps(&self, flow: Flow) -> IndexLess {
138 self.particle()
139 .map(|p| p.color_reps(flow))
140 .unwrap_or_else(IndexLess::scalar_structure)
141 }
142
143 pub(crate) fn spin_reps(&self) -> IndexLess<LibraryRep, Aind> {
144 self.particle()
145 .map(|p| p.spin_reps())
146 .unwrap_or_else(IndexLess::scalar_structure)
147 }
148
149 pub(crate) fn particle(&self) -> Option<ArcParticle> {
150 match self {
151 PossibleParticle::Particle(particle)
152 | PossibleParticle::MassOverriddenParticle { particle, .. } => Some(particle.clone()),
153 _ => None,
154 }
155 }
156
157 pub(crate) fn is_massless(&self) -> bool {
158 match self {
159 PossibleParticle::JustMass { expr } => expr.is_zero(),
160 PossibleParticle::Particle(p) => p.is_massless(),
161 PossibleParticle::MassOverriddenParticle { mass, .. } => mass.is_zero(),
162 }
163 }
164
165 pub(crate) fn is_massive(&self) -> bool {
166 !self.is_massless()
167 }
168}
169
170#[derive(Debug, Clone, bincode_trait_derive::Encode, bincode_trait_derive::Decode)]
171#[trait_decode(trait = crate::GammaLoopContext)]
172#[derive(Default)]
173pub struct EdgeExtraData {
174 pub momtrop_edge_power: Option<Atom>,
176 pub vakint_edge_power: Option<isize>,
178}
179
180#[derive(Debug, Clone, bincode_trait_derive::Encode, bincode_trait_derive::Decode)]
181#[trait_decode(trait = crate::GammaLoopContext)]
182pub struct Edge {
183 pub name: Autogen<String>,
185 pub extra_data: EdgeExtraData,
186 pub particle: PossibleParticle,
189 pub mass: EdgeMass,
190 pub num: Autogen<Atom>,
191 pub dod: Autogen<i32>,
193 pub is_dummy: bool, }
195
196#[derive(Debug, Clone, bincode_trait_derive::Encode, bincode_trait_derive::Decode)]
197#[trait_decode(trait = crate::GammaLoopContext)]
198pub enum EdgeMass {
199 Zero,
200 Value(Complex<F<f64>>),
201 ModelVar(Symbol),
202 Evaluator(Arc<Mutex<ExpressionEvaluator<Complex<F<f64>>>>>),
203}
204
205impl PartialEq for EdgeMass {
206 fn eq(&self, other: &Self) -> bool {
207 match (self, other) {
208 (EdgeMass::Zero, EdgeMass::Zero) => true,
209 (EdgeMass::Value(a), EdgeMass::Value(b)) => a == b,
210 (EdgeMass::ModelVar(a), EdgeMass::ModelVar(b)) => a == b,
211 _ => false,
212 }
213 }
214}
215
216impl EdgeMass {
217 pub fn from_atom(atom: Atom, model: &Model, paramb: &ParamBuilder) -> Result<Self> {
218 if atom.is_zero() {
219 return Ok(EdgeMass::Zero);
220 } else if let AtomView::Var(v) = atom.as_view() {
221 if model.contains_symbol(&UFOSymbol(v.get_symbol())) {
222 return Ok(EdgeMass::ModelVar(v.get_symbol()));
223 }
224 } else if let Ok(a) = SymComplex::<Float>::try_from(&atom) {
225 return Ok(EdgeMass::Value(Complex {
226 re: F(a.re.into_inner().to_f64()),
227 im: F(a.im.into_inner().to_f64()),
228 }));
229 }
230
231 let params: Vec<Atom> = (¶mb.pairs)
232 .into_iter()
233 .flat_map(|p| p.params.clone())
234 .collect();
235
236 let a = atom
237 .evaluator(¶ms)
238 .function_map(paramb.fn_map.clone())
239 .optimization_settings(OptimizationSettings::default())
240 .build()
241 .map_err(|a| eyre!(a))?;
242
243 Ok(EdgeMass::Evaluator(Arc::new(Mutex::new(a.map_coeff(
244 &|r| Complex::new(F::from(&r.re), F::from(&r.im)),
245 )))))
246 }
247
248 pub fn value<T: FloatLike>(
249 &self,
250 model: &Model,
251 paramb: &ParamBuilder,
252 ) -> Option<Complex<F<T>>> {
253 match self {
254 EdgeMass::Zero => None,
255 EdgeMass::Value(v) => Some(*v),
256 EdgeMass::ModelVar(s) => model.get_symbol_value(UFOSymbol(*s)),
257 EdgeMass::Evaluator(a) => Some(a.lock().unwrap().evaluate_single(¶mb.values[0])),
258 }
259 .map(|a| a.map_ref(|a| F::from_ff64(*a)))
260 }
261}
262
263impl UVE for Edge {
264 fn mass_atom(&self) -> Atom {
265 match &self.particle {
266 PossibleParticle::JustMass { expr, .. } => {
267 expr.replace(UFOSymbol::zero().0).with(Atom::Zero)
268 }
269 PossibleParticle::Particle(p) => Atom::var(p.mass.0.0)
270 .replace(UFOSymbol::zero().0)
271 .with(Atom::Zero),
272 PossibleParticle::MassOverriddenParticle { mass, .. } => {
273 mass.replace(UFOSymbol::zero().0).with(Atom::Zero)
274 }
275 }
276 }
277
278 fn particle_pdg_code(&self) -> Option<isize> {
279 self.particle().map(|particle| particle.pdg_code)
280 }
281
282 fn is_massive(&self) -> bool {
283 self.particle.is_massive()
284 }
285}
286
287impl Edge {
288 pub fn random_helicity(&self, seed: u64) -> Helicity {
289 if let PossibleParticle::Particle(p) = &self.particle {
290 p.random_helicity(seed)
291 } else {
292 Helicity::ZERO
293 }
294 }
295
296 pub(crate) fn particle(&self) -> Option<ArcParticle> {
297 self.particle.particle()
298 }
299
300 pub(crate) fn mass_value<T: FloatLike>(
301 &self,
302 model: &Model,
303 paramb: &ParamBuilder,
304 ) -> Option<Complex<F<T>>> {
305 self.mass.value(model, paramb)
306 }
307}
308
309impl From<&ParseEdge> for DotEdgeData {
310 fn from(value: &ParseEdge) -> Self {
311 let mut e = DotEdgeData::empty();
312 if let Some(name) = &value.name {
313 e.add_statement("name", name.clone());
314 }
315 match &value.particle {
316 PossibleParticle::Particle(p) => {
317 e.add_statement("particle", format!("{}", p.name));
318 }
319 PossibleParticle::JustMass { expr, .. } => {
320 e.add_statement("mass", expr.to_quoted());
321 }
322 PossibleParticle::MassOverriddenParticle { mass, particle, .. } => {
323 e.add_statement("mass", mass.to_quoted());
324 e.add_statement("particle", format!("{}", particle.name));
325 }
326 }
327 if let Some(lmb_id) = &value.lmb_id {
328 e.add_statement("lmb_id", usize::from(*lmb_id));
329 }
330 if let Some(cut) = &value.is_cut {
331 e.add_statement("is_cut", usize::from(*cut));
332 }
333 if let Some(dod) = &value.dod {
334 e.add_statement("dod", *dod);
335 }
336 if let Some(num) = &value.num {
337 e.add_statement("num", num.to_quoted());
338 }
339
340 if let Some(mep) = value.momtrop_edge_power.as_ref() {
341 e.add_statement("momtrop_edge_power", mep.to_canonical_string());
342 }
343
344 if let Some(vak) = value.vakint_edge_power {
345 e.add_statement("vakint_edge_power", vak);
346 }
347
348 if value.is_dummy {
349 e.add_statement("is_dummy", value.is_dummy);
350 }
351 e
353 }
354}
355
356impl From<&Edge> for DotEdgeData {
357 fn from(value: &Edge) -> Self {
358 value.to_dot_data(false)
359 }
360}
361
362impl From<&Edge> for ParseEdge {
363 fn from(value: &Edge) -> Self {
364 value.to_parse_edge(false)
365 }
366}
367
368impl Edge {
369 pub(crate) fn to_parse_edge(&self, include_autogenerated_fields: bool) -> ParseEdge {
370 ParseEdge {
371 name: self
372 .name
373 .clone()
374 .option_with_generated(include_autogenerated_fields),
375 particle: self.particle.clone(),
376 dod: self.dod.option_with_generated(include_autogenerated_fields),
377 is_dummy: self.is_dummy,
378 lmb_id: None,
379 num: self
380 .num
381 .clone()
382 .option_with_generated(include_autogenerated_fields),
383 is_cut: None,
384 momtrop_edge_power: self.extra_data.momtrop_edge_power.clone(),
385 vakint_edge_power: self.extra_data.vakint_edge_power,
386 }
387 }
388
389 pub(crate) fn to_dot_data(&self, include_autogenerated_fields: bool) -> DotEdgeData {
390 let parse_edge = self.to_parse_edge(include_autogenerated_fields);
391 let mut e: DotEdgeData = (&parse_edge).into();
392 if include_autogenerated_fields {
393 if self.name.autogenerated {
394 e.add_statement("name_autogen", true);
395 }
396 if self.dod.autogenerated {
397 e.add_statement("dod_autogen", true);
398 }
399 if self.num.autogenerated {
400 e.add_statement("num_autogen", true);
401 }
402 }
403 e
404 }
405}
406
407#[derive(Debug, Clone)]
408pub struct ParseEdge {
409 pub name: Option<String>,
410 pub particle: PossibleParticle,
411
412 pub dod: Option<i32>,
413 pub is_dummy: bool,
414 pub lmb_id: Option<LoopIndex>,
415
416 pub num: Option<Atom>,
418 pub is_cut: Option<Hedge>,
420 pub momtrop_edge_power: Option<Atom>,
421 pub vakint_edge_power: Option<isize>,
422}
423
424impl ParseEdge {
425 pub fn from_symbolica_edge(
426 model: &Model,
427 edge_color: &EdgeColor,
428 is_cut: Option<Hedge>,
429 ) -> Self {
430 let particle = model.get_particle_from_pdg(edge_color.pdg);
431 let mut e = ParseEdge::new(particle);
432 e.is_cut = is_cut;
433 e
434 }
435}
436
437impl Display for ParseEdge {
438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439 DotEdgeData::from(self).fmt(f)
440 }
441}
442
443impl UVE for ParseEdge {
444 fn mass_atom(&self) -> Atom {
445 match &self.particle {
446 PossibleParticle::JustMass { expr, .. } => expr.clone(),
447 PossibleParticle::Particle(p) => p.mass.0.into(),
448 PossibleParticle::MassOverriddenParticle { mass, .. } => mass.clone(),
449 }
450 }
451
452 fn particle_pdg_code(&self) -> Option<isize> {
453 self.particle.particle().map(|particle| particle.pdg_code)
454 }
455
456 fn is_massive(&self) -> bool {
457 self.particle.is_massive()
458 }
459}
460
461impl ParseEdge {
462 pub fn new(particle: impl Into<PossibleParticle>) -> Self {
463 ParseEdge {
464 is_dummy: false,
465 name: None,
466 particle: particle.into(),
467 dod: None,
468 lmb_id: None,
469 num: None,
470 is_cut: None,
471 momtrop_edge_power: None,
472 vakint_edge_power: None,
473 }
474 }
475
476 pub fn is_dummy(mut self) -> Self {
477 self.is_dummy = true;
478 self
479 }
480
481 pub fn with_label(mut self, label: String) -> Self {
482 self.name = Some(label);
483 self
484 }
485
486 pub fn with_dod(mut self, dod: i32) -> Self {
487 self.dod = Some(dod);
488 self
489 }
490
491 pub fn with_lmb_id(mut self, lmb_id: LoopIndex) -> Self {
492 self.lmb_id = Some(lmb_id);
493 self
494 }
495
496 pub fn with_num(mut self, num: Atom) -> Self {
497 self.num = Some(num);
498 self
499 }
500}
501
502impl ParseEdge {
503 pub(crate) fn localize_ainds(
504 atom: impl AtomCore,
505 eid: EdgeIndex,
506 hedge_pair: HedgePair,
507 ) -> Atom {
508 let a = atom
509 .replace(GS.edgeid)
510 .with(Atom::num(usize::from(eid) as i64))
511 .replace_map(|term, _, out| {
512 if let AtomView::Fun(f) = term
513 && f.get_symbol() == GS.edgeid
514 && f.get_nargs() == 1
515 && let Ok(i) = i64::try_from(f.iter().next().unwrap())
516 && let Ok(u) = u16::try_from(i)
517 {
518 **out = eid.aind(u).into();
519 }
520 });
521
522 match hedge_pair {
523 HedgePair::Paired { source, sink } | HedgePair::Split { source, sink, .. } => a
524 .replace(GS.sink_id)
525 .with(Atom::num(sink.0 as i64))
526 .replace_map(|term, _, out| {
527 if let AtomView::Fun(f) = term
528 && f.get_symbol() == GS.sink_id
529 && f.get_nargs() == 1
530 && let Ok(i) = i64::try_from(f.iter().next().unwrap())
531 && let Ok(u) = u16::try_from(i)
532 {
533 **out = sink.aind(u).into();
534 }
535 })
536 .replace(GS.source_id)
537 .with(Atom::num(source.0 as i64))
538 .replace_map(|term, _, out| {
539 if let AtomView::Fun(f) = term
540 && f.get_symbol() == GS.source_id
541 && f.get_nargs() == 1
542 && let Ok(i) = i64::try_from(f.iter().next().unwrap())
543 && let Ok(u) = u16::try_from(i)
544 {
545 **out = source.aind(u).into();
546 }
547 }),
548 HedgePair::Unpaired { hedge, flow } => match flow {
549 Flow::Source => a
550 .replace(GS.source_id)
551 .with(Atom::num(hedge.0 as i64))
552 .replace_map(|term, _, out| {
553 if let AtomView::Fun(f) = term
554 && f.get_symbol() == GS.source_id
555 && f.get_nargs() == 1
556 && let Ok(i) = i64::try_from(f.iter().next().unwrap())
557 && let Ok(u) = u16::try_from(i)
558 {
559 **out = hedge.aind(u).into();
560 }
561 }),
562 Flow::Sink => a
563 .replace(GS.sink_id)
564 .with(Atom::num(hedge.0 as i64))
565 .replace_map(|term, _, out| {
566 if let AtomView::Fun(f) = term
567 && f.get_symbol() == GS.sink_id
568 && f.get_nargs() == 1
569 && let Ok(i) = i64::try_from(f.iter().next().unwrap())
570 && let Ok(u) = u16::try_from(i)
571 {
572 **out = hedge.aind(u).into();
573 }
574 }),
575 },
576 }
577 }
578 #[allow(clippy::type_complexity)]
579 pub(crate) fn parse<'a>(
580 model: &'a Model,
581 ) -> impl FnMut(
582 &'a HedgeGraph<DotEdgeData, DotVertexData, DotHedgeData>,
583 EdgeIndex,
584 HedgePair,
585 EdgeData<&'a DotEdgeData>,
586 ) -> Result<EdgeData<Self>> {
587 |_: &'a HedgeGraph<DotEdgeData, DotVertexData, DotHedgeData>,
588 eid: EdgeIndex,
589 p: HedgePair,
590 e_data: EdgeData<&'a DotEdgeData>| {
591 let e = e_data.data;
592 let label = e.get::<_, String>("name").transpose()?;
593
594 let lmb_id: Option<LoopIndex> = e
595 .get::<_, usize>("lmb_id")
596 .transpose()?
597 .map(LoopIndex::from);
598
599 let is_cut: Option<Hedge> = e.get::<_, usize>("is_cut").transpose()?.map(Hedge::from);
600
601 let dod = e
602 .get::<_, String>("dod")
603 .transpose()
604 .with_context(|| "Error parsing dod".to_string())?
605 .map(|a| a.strip_parse())
606 .transpose()?;
607 let is_dummy = e.get::<_, bool>("is_dummy").transpose()?.unwrap_or(false);
608
609 let num = e
610 .get::<_, String>("num")
611 .transpose()?
612 .map(|a| -> Result<Atom> {
613 Ok(Self::localize_ainds(a.strip_parse::<Atom>()?, eid, p))
614 })
615 .transpose()?;
616
617 let mass = e
618 .get::<_, String>("mass")
619 .transpose()?
620 .map(|a| a.strip_parse::<Atom>())
621 .transpose()?;
622
623 let momtrop_edge_power: Option<Atom> = e
624 .get::<_, String>("momtrop_edge_power")
625 .transpose()?
626 .map(|a| parse!(&a));
627 let vakint_edge_power: Option<isize> =
628 e.get::<_, isize>("vakint_edge_power").transpose()?;
629
630 let particle: PossibleParticle = if let Some(v) = e.get::<_, isize>("pdg") {
631 model.try_get_particle_from_pdg(v?)?.into()
632 } else if let Some(v) = e.get::<_, String>("particle") {
633 let pname: String = v?.strip_parse()?;
634 model.try_get_particle(pname)?.into()
635 } else {
636 ().into()
637 };
638
639 let orientation = if e.local_statements.contains_key("dir") {
640 e_data.orientation
641 } else {
642 particle.orientation()
643 };
644 Ok(EdgeData::new(
645 ParseEdge {
646 is_dummy,
647 dod,
648 lmb_id,
649 particle: particle.override_mass(mass),
650 num,
651 is_cut,
652 name: label,
653 momtrop_edge_power,
654 vakint_edge_power,
655 },
656 orientation,
657 ))
658 }
659 }
660}