1use std::{cell::RefCell, collections::BTreeSet, ops::Deref};
2
3use ahash::{AHashMap, AHashSet};
4use color_eyre::Result;
5use eyre::eyre;
6use idenso::shorthands::schoonschip::Schoonschip;
7use linnet::half_edge::{
8 HedgeGraph, PowersetIterator,
9 involution::{Flow, Hedge, HedgePair},
10 subgraph::{
11 Cycle, InternalSubGraph, ModifySubSet, PairwiseSubSetOps, SuBitGraph, SubGraphLike,
12 SubGraphOps, SubSetLike, SubSetOps, subset::SubSet,
13 },
14};
15use symbolica::{
16 atom::{Atom, AtomCore, Symbol},
17 domains::atom::AtomField,
18 function,
19 poly::series::Series,
20};
21use tracing::debug;
22
23use crate::{
24 graph::{Edge, FeynmanGraph, Graph, HedgeData, LMBext, LoopMomentumBasis, Vertex},
25 integrands::process::param_builder::ParamBuilderGraph,
26 momentum::sample::LoopIndex,
27 numerator::{AppliedFeynmanRule, Numerator},
28 settings::global::MediumSettings,
29 utils::{GS, W_, symbolica_ext::DOD},
30 uv::{ApproximationType, UVgenerationSettings, settings::CTIdentifier},
31};
32
33use super::{Spinney, Wood, spenso_lor_atom};
34
35pub trait UltravioletGraph: LMBext + FeynmanGraph + ParamBuilderGraph {
36 fn n_loops<S: SubGraphLike, E, V, H>(&self, subgraph: &S) -> usize
37 where
38 Self: AsRef<HedgeGraph<E, V, H>>,
39 {
40 self.as_ref().cyclotomatic_number(subgraph)
41 }
42
43 fn dummy_less_full_crown<S: SubGraphLike>(&self, subgraph: &S) -> S::Base
46 where
47 S::Base: ModifySubSet<Hedge> + SubGraphOps;
48
49 fn numerator<S: SubGraphLike + SubSetOps>(
52 &self,
53 subgraph: &S,
54 without: &S,
55 ) -> Numerator<AppliedFeynmanRule>;
56 fn denominator<S: SubGraphLike, T: Fn(&Edge) -> isize>(
57 &self,
58 subgraph: &S,
59 edge_powers: T,
60 ) -> Atom;
61
62 fn boundary_pdg_set<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
63 &self,
64 subgraph: &S,
65 ) -> BTreeSet<isize>
66 where
67 Self: AsRef<HedgeGraph<E, V, H>>,
68 {
69 let graph = self.as_ref();
70 graph
71 .full_crown(subgraph)
72 .included_iter()
73 .filter_map(|hedge| {
74 let edge_id = graph[&hedge];
75 graph[edge_id].particle_pdg_code().map(|pdg| {
76 if graph.flow(hedge) == Flow::Source {
77 -pdg
78 } else {
79 pdg
80 }
81 })
82 })
83 .collect()
84 }
85
86 fn internal_pdg_set<E: UVE, V, H, S: SubGraphLike>(&self, subgraph: &S) -> BTreeSet<isize>
87 where
88 Self: AsRef<HedgeGraph<E, V, H>>,
89 {
90 self.as_ref()
91 .iter_edges_of(subgraph)
92 .filter_map(|(pair, edge_id, _)| {
93 pair.is_paired()
94 .then(|| self.as_ref()[edge_id].particle_pdg_code())
95 .flatten()
96 })
97 .collect()
98 }
99
100 fn has_massive_boundary_external<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
101 &self,
102 subgraph: &S,
103 ) -> bool
104 where
105 Self: AsRef<HedgeGraph<E, V, H>>,
106 {
107 let graph = self.as_ref();
108 graph.full_crown(subgraph).included_iter().any(|hedge| {
109 let edge_id = graph[&hedge];
110 graph[edge_id].is_massive()
111 })
112 }
113
114 fn ct_identifier<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
115 &self,
116 subgraph: &S,
117 ) -> CTIdentifier
118 where
119 Self: AsRef<HedgeGraph<E, V, H>>,
120 {
121 CTIdentifier::new(
122 self.boundary_pdg_set(subgraph),
123 Some(self.internal_pdg_set(subgraph)),
124 )
125 }
126
127 fn approximation_scheme<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
128 &self,
129 subgraph: &S,
130 settings: &UVgenerationSettings,
131 dod: i32,
132 ) -> ApproximationType
133 where
134 Self: AsRef<HedgeGraph<E, V, H>>,
135 {
136 settings.approximation_scheme_for(
137 &self.ct_identifier(subgraph),
138 dod,
139 self.has_massive_boundary_external(subgraph),
140 )
141 }
142
143 fn classify_spinney<E: UVE, V, H>(
144 &self,
145 spinney: InternalSubGraph,
146 settings: &UVgenerationSettings,
147 lmb: &LoopMomentumBasis,
148 ) -> Result<Option<Spinney>>
149 where
150 Self: AsRef<HedgeGraph<E, V, H>>,
151 {
152 let dod = self.compute_dod(&spinney.filter);
153 if dod < 0 {
154 return Ok(None);
155 }
156
157 let renormalization_scheme = self.approximation_scheme(&spinney.filter, settings, dod);
158
159 if renormalization_scheme != ApproximationType::Unsubtracted {
160 Spinney::with_scheme(spinney, self, lmb, renormalization_scheme, dod)
161 } else {
162 Ok(None)
163 }
164 }
165
166 fn classified_spinneys<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
167 &self,
168 subgraph: &S,
169 settings: &UVgenerationSettings,
170 lmb: &LoopMomentumBasis,
171 ) -> Result<Vec<Spinney>>
172 where
173 Self: AsRef<HedgeGraph<E, V, H>>,
174 {
175 if !settings.subtract_uv {
176 return Ok(vec![Spinney::empty(self)]);
177 }
178
179 let graph = self.as_ref();
180 let (connected, disconnected): (Vec<_>, Vec<_>) = self
181 .spinneys(subgraph)
182 .into_iter()
183 .partition(|spinney| graph.connected_components(spinney).len() <= 1);
184
185 let mut classified: Vec<_> = connected
186 .into_iter()
187 .filter_map(|spinney| self.classify_spinney(spinney, settings, lmb).transpose())
188 .collect::<Result<_>>()?;
189 let retained_factors: AHashSet<_> = classified
190 .iter()
191 .map(|spinney| spinney.filter().clone())
192 .collect();
193
194 for spinney in disconnected {
195 let components = graph.connected_components(&spinney);
196 if !components.into_iter().all(|component| {
197 let factor = InternalSubGraph::cleaned_filter_optimist(component, graph);
198 retained_factors.contains(&factor.filter)
199 }) {
200 continue;
201 }
202
203 let dod = self.compute_dod(&spinney.filter);
204 if let Some(spinney) =
205 Spinney::with_scheme(spinney, self, lmb, ApproximationType::MUV, dod)?
206 {
207 classified.push(spinney);
208 }
209 }
210 Ok(classified)
211 }
212
213 fn remove_full_observable_spinney<E, V, H>(
214 &self,
215 spinneys: &mut Vec<Spinney>,
216 medium: &MediumSettings,
217 ) where
218 Self: AsRef<HedgeGraph<E, V, H>>,
219 {
220 if !medium.vacuum_subtraction {
221 return;
222 }
223
224 let graph = self.as_ref();
228 let full_observable =
229 InternalSubGraph::cleaned_filter_pessimist(graph.non_bridges(), graph);
230 spinneys
231 .retain(|spinney| spinney.subgraph.is_empty() || spinney.subgraph != full_observable);
232 }
233
234 fn all_cycle_unions<E, V, H, S: SubGraphLike<Base = SuBitGraph>>(
235 &self,
236 subgraph: &S,
237 ) -> AHashSet<InternalSubGraph>
238 where
239 Self: AsRef<HedgeGraph<E, V, H>>,
240 {
241 let ref_graph = self.as_ref();
242 let _init_node = ref_graph.iter_nodes_of(subgraph).next().unwrap().0;
243 let all_subcycles: Vec<_> =
244 Cycle::all_sum_powerset_filter_map(&ref_graph.cycle_basis_of(subgraph).0, &Some)
245 .map(|a| a.into_iter().map(|c| c.internal_graph(ref_graph)).collect())
246 .unwrap();
247
248 let spinneys: AHashSet<_> = InternalSubGraph::all_unions_iterative(&all_subcycles);
250
251 spinneys
252 }
253 fn all_limits<E, V, H, S: SubGraphLike>(
254 &self,
255 subgraph: &S,
256 expr: &Atom,
257 expansion: Symbol,
258 lmb: &LoopMomentumBasis,
259 ) -> Vec<(SubSet<LoopIndex>, Series<AtomField>)>
260 where
261 Self: AsRef<HedgeGraph<E, V, H>>,
262 {
263 let mom_reps = self.normal_emr_replacement(subgraph, lmb, &[W_.x___], |_s| true);
264
265 let ose_reps = self.get_ose_replacements();
266 let expr = expr
279 .replace_multiple(&ose_reps)
280 .replace_multiple(&mom_reps)
281 .replace(function!(GS.broadcasting_sqrt, W_.a_))
282 .with(Atom::var(W_.a_).sqrt())
283 .replace(function!(GS.on_shell_energy, W_.a_, W_.prop_))
284 .with(Atom::var(W_.prop_).sqrt());
285 let mut loops = PowersetIterator::<LoopIndex>::new(lmb.loop_edges.len() as u8);
287
288 let mut limits = Vec::new();
289
290 loops.next();
291
292 for ls in loops {
293 let mut expr = expr.clone();
294 for l in ls.included_iter() {
295 let e = usize::from(lmb.loop_edges[l]) as i64;
296 expr = expr
297 .replace(function!(GS.emr_mom, e, W_.x___))
298 .with(function!(GS.emr_mom, e, W_.x___) / expansion);
299
300 expr /= Atom::var(expansion).pow(3);
301 }
302
303 let series = expr.series(expansion, Atom::Zero, 0).unwrap();
304
305 limits.push((ls, series));
308 }
309 limits
310 }
311
312 fn wood<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(&self, subgraph: &S) -> Result<Wood>
313 where
314 Self: AsRef<HedgeGraph<E, V, H>>,
315 {
316 self.wood_with_settings(
317 subgraph,
318 &UVgenerationSettings::default(),
319 &self.as_ref().lmb_of(&self.as_ref().full_filter()),
320 )
321 }
322
323 fn wood_with_settings<E: UVE, V, H, S: SubGraphLike<Base = SuBitGraph>>(
324 &self,
325 subgraph: &S,
326 settings: &UVgenerationSettings,
327 lmb: &LoopMomentumBasis,
328 ) -> Result<Wood>
329 where
330 Self: AsRef<HedgeGraph<E, V, H>>,
331 {
332 Ok(Wood::from_spinneys(
333 self.classified_spinneys(subgraph, settings, lmb)?,
334 self,
335 ))
336 }
337
338 fn compute_dod<S: SubGraphLike<Base = SuBitGraph> + SubSetOps>(&self, subgraph: &S) -> i32;
339
340 fn compute_energy_dod<E, V, H, S>(&self, subgraph: &S) -> i32
346 where
347 Self: AsRef<HedgeGraph<E, V, H>>,
348 S: SubGraphLike<Base = SuBitGraph> + SubSetOps,
349 {
350 self.compute_dod(subgraph) - 3 * self.n_loops(subgraph) as i32
351 }
352
353 fn local_dod<S: SubGraphLike>(&self, subgraph: &S) -> i32;
354
355 fn spinneys<E, V, H, S: SubGraphLike<Base = SuBitGraph>>(
356 &self,
357 subgraph: &S,
358 ) -> AHashSet<InternalSubGraph>
359 where
360 Self: AsRef<HedgeGraph<E, V, H>>,
361 {
362 let ref_graph: &HedgeGraph<E, V, H> = self.as_ref();
363 debug!(subgraph=%ref_graph.dot(subgraph),"Spinneys of subgraph");
364 let _b: SuBitGraph = ref_graph.empty_subgraph();
365
366 if subgraph.is_empty() {
367 let mut spinneys = AHashSet::new();
368 spinneys.insert(ref_graph.empty_subgraph());
369 return spinneys;
370 }
371
372 let cycles = ref_graph.cycle_basis_of(subgraph).0;
373
374 let _init_node = ref_graph.iter_nodes_of(subgraph).next().unwrap().0;
375 let all_subcycles: Vec<_> = Cycle::all_sum_powerset_filter_map(&cycles, &Some)
376 .map(|a| a.into_iter().map(|c| c.internal_graph(ref_graph)).collect())
377 .unwrap();
378
379 let dod_cache = RefCell::new(AHashMap::new());
380
381 let mut spinneys: AHashSet<_> = InternalSubGraph::all_ops_iterative_filter_map(
382 &all_subcycles,
383 &|a, b| a.union(b),
384 &|union| {
385 let cached_dod = dod_cache.borrow().get(&union).copied();
387 let keep = match cached_dod {
388 Some(keep) => keep,
389 None => {
390 let keep = self.local_dod(&union) >= 0;
391 dod_cache.borrow_mut().insert(union.clone(), keep);
392 keep
393 }
394 };
395 if keep { Some(union) } else { None }
396 },
397 );
398
399 spinneys.insert(ref_graph.empty_subgraph());
400
401 spinneys
402 }
403}
404
405impl AsRef<HedgeGraph<Edge, Vertex, HedgeData>> for Graph {
406 fn as_ref(&self) -> &HedgeGraph<Edge, Vertex, HedgeData> {
407 &self.underlying
408 }
409}
410
411impl Graph {
412 pub(crate) fn ensure_energy_convergent_cycles<S>(&self, domain: &S) -> Result<()>
416 where
417 S: SubGraphLike<Base = SuBitGraph>,
418 {
419 let numerator = self.production_numerator_atom_for_full_3d_expression();
420 if numerator.is_zero() {
421 return Ok(());
422 }
423 let mut failures = self
424 .all_cycle_unions(domain)
425 .into_iter()
426 .filter(|cycle| !cycle.is_empty())
427 .filter_map(|cycle| {
428 let loop_count = self.n_loops(&cycle.filter);
429 let lmb = self.lmb_of(&cycle.filter);
437 let integrand = &numerator / self.denominator(&cycle.filter, |_| 1);
438 let four_d_dod = self
439 .uv_rescaled(cycle.filter.included(), loop_count, &lmb, &lmb, &integrand)
440 .trailing_exponent();
441 let energy_dod = four_d_dod - 3 * loop_count as i32;
444 (energy_dod >= 0)
445 .then(|| (cycle.string_label(), loop_count, four_d_dod, energy_dod))
446 })
447 .collect::<Vec<_>>();
448 failures.sort();
449
450 if failures.is_empty() {
451 return Ok(());
452 }
453
454 let details = failures
455 .into_iter()
456 .map(|(cycle, loop_count, four_d_dod, energy_dod)| {
457 format!("cycle {cycle}: L={loop_count}, DOD_4D={four_d_dod}, DOD_E={energy_dod}")
458 })
459 .collect::<Vec<_>>()
460 .join("; ");
461 Err(eyre!(
462 "graph `{}` contains a loop-energy nonconvergent integral ({details}); expected DOD_E < 0 for every cycle union",
463 self.name,
464 ))
465 }
466}
467
468impl UltravioletGraph for Graph {
469 fn dummy_less_full_crown<S: SubGraphLike>(&self, subgraph: &S) -> S::Base
470 where
471 S::Base: ModifySubSet<Hedge>,
472 {
473 let a = self.full_crown(subgraph);
474 let mut ac = a.clone();
475
476 a.included_iter().for_each(|a| {
477 if self[self[&a]].is_dummy {
478 ac.sub(a);
479 }
480 });
481
482 ac
483 }
484
485 fn denominator<S: SubGraphLike, T: Fn(&Edge) -> isize>(
503 &self,
504 subgraph: &S,
505 edge_powers: T,
506 ) -> Atom {
507 let mut den = Atom::num(1);
508
509 for (pair, eid, d) in self.underlying.iter_edges_of(subgraph) {
510 if matches!(pair, HedgePair::Paired { .. }) {
511 let m2 = d.data.mass_atom().pow(2);
512 let edge_power = edge_powers(d.data);
513 let is_power_negative = edge_power < 0;
514 let prop_den = GS.den(
515 usize::from(eid),
516 function!(GS.emr_mom, usize::from(eid)),
517 &m2,
518 spenso_lor_atom(usize::from(eid) as i32, usize::from(eid), GS.dim)
519 .pow(2)
520 .to_dots()
521 - &m2,
522 );
523 for _i in 0..edge_power.abs() {
524 if is_power_negative {
525 den /= prop_den.clone();
526 } else {
527 den *= prop_den.clone();
528 }
529 }
530 }
531 }
532
533 den
534 }
535 fn numerator<S: SubGraphLike + SubSetOps>(
536 &self,
537 subgraph: &S,
538 without: &S,
539 ) -> Numerator<AppliedFeynmanRule> {
540 let num = Numerator::default();
541
542 num.fill_in_reduced(self, subgraph, without)
543 }
544
545 fn compute_dod<S: SubGraphLike<Base = SuBitGraph> + SubSetOps>(&self, subgraph: &S) -> i32 {
546 let lmb = self.lmb_of(subgraph);
547 let empty = self.underlying.empty_subgraph();
548 let integrand = self
549 .numerator(subgraph, &empty)
550 .to_d_dim(GS.dim)
551 .get_single_atom()
552 .unwrap()
553 / self.denominator(subgraph, |_| 1);
554 let nloops: usize = self.n_loops(subgraph);
555 self.uv_rescaled(subgraph.included(), nloops, &lmb, &lmb, &integrand)
556 .trailing_exponent()
557 }
558
559 fn local_dod<S: SubGraphLike>(&self, subgraph: &S) -> i32 {
560 let mut dod: i32 = 4 * self.n_loops(subgraph) as i32;
561 for (p, _, e) in self.underlying.iter_edges_of(subgraph) {
562 if p.is_paired() {
563 dod += e.data.dod.deref();
564 }
565 }
566
567 for (_, _, n) in self.underlying.iter_nodes_of(subgraph) {
568 dod += n.dod.deref();
569 }
570
571 dod
572 }
573}
574
575pub trait UVE {
576 fn mass_atom(&self) -> Atom;
577 fn particle_pdg_code(&self) -> Option<isize>;
578 fn is_massive(&self) -> bool;
579 fn is_fermion(&self) -> bool;
580 fn chemical_potential_atom(&self) -> Option<Atom>;
581}