1mod charge;
2
3use charge::SerializedCharge;
4
5use crate::HasModel;
6use crate::momentum::Helicity;
7use crate::numerator::aind::Aind;
8use crate::utils::serde_utils::SmartSerde;
9use crate::utils::symbolica_ext::DOD;
10use crate::utils::{self, F, W_};
11use ahash::{AHashMap, HashSet, RandomState};
12use bincode::{Decode, Encode};
13use color_eyre::Report;
14use color_eyre::owo_colors::OwoColorize;
15use colored::Colorize;
16use eyre::eyre;
17use itertools::Itertools;
18use linnet::half_edge::involution::{EdgeIndex, Flow};
20use rand::rngs::SmallRng;
21use rand::{Rng, SeedableRng};
22use serde::de::DeserializeOwned;
23#[cfg(test)]
24use spenso::shadowing::symbolica_utils::SpensoPrintSettings;
25use spenso::structure::{Canonicalized, IndexLess, TensorDataLayout};
26use symbolica_utils::{PrintSettingsExt, Replaces};
27use tabled::settings::Modify;
28use tabled::{
29 builder::Builder,
30 settings::{Span, object::Cell},
31 settings::{Style, style::VerticalLine},
32};
33
34use idenso::{
36 dirac::AGS,
37 representations::{Bispinor, ColorAdjoint, ColorFundamental, ColorSextet},
38};
39use serde::{Deserialize, Serialize};
40use smartstring::{LazyCompact, SmartString};
41use spenso::algebra::complex::Complex;
42use spenso::network::{library::symbolic::ETS, tags::SPENSO_TAG};
43use spenso::structure::OrderedStructure;
44use spenso::structure::representation::Euclidean;
45use spenso::structure::representation::{LibraryRep, Minkowski};
46use spenso::structure::{
47 representation::BaseRepName, representation::Lorentz, representation::RepName, slot::DummyAind,
48 slot::IsAbstractSlot, slot::Slot,
49};
50use spenso::tensors::data::{DataTensor, DenseTensor, SetTensorData, SparseTensor};
51use spenso::tensors::parametric::ParamTensor;
52use std::collections::BTreeMap;
53use std::fmt::{Display, Formatter};
54use std::fs;
55use symbolica::{domains::rational::Fraction, prelude::*, printer::PrintUserData};
56use tracing::info;
57
58use color_eyre::Result;
59use std::collections::HashMap;
60use std::ops::Deref;
61use std::path::Path;
62use std::sync::Arc;
63
64use crate::settings::global::VectorPolarizationSumGauge;
65use crate::utils::GS;
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68pub struct SerializableInputParamCard<T> {
69 pub data: BTreeMap<String, (T, T)>,
70}
71
72impl<T> SerializableInputParamCard<T>
73where
74 T: From<f64> + Clone + Serialize + DeserializeOwned,
75{
76 pub(crate) fn from_file(file_path: impl AsRef<Path>) -> Result<Self, Report> {
77 let hashmap_card: BTreeMap<String, (T, T)> = SmartSerde::from_file(file_path, "model")?;
78 Ok(SerializableInputParamCard { data: hashmap_card })
79 }
80
81 pub fn from_str(s: String, format: &str) -> Result<Self, Report> {
82 let hashmap_card: BTreeMap<String, (T, T)> =
83 SmartSerde::from_str(s, format, "model_parameters")?;
84 Ok(SerializableInputParamCard { data: hashmap_card })
85 }
86
87 pub fn from_input_param_card(card: &InputParamCard<T>) -> Self {
88 let serializeable_card: BTreeMap<String, (T, T)> = card
89 .data
90 .iter()
91 .map(|(k, v)| {
92 (
93 k.namespaceless_string().to_string(),
94 (v.re.clone(), v.im.clone()),
95 )
96 })
97 .collect();
98 SerializableInputParamCard {
99 data: serializeable_card,
100 }
101 }
102
103 pub fn to_file<P: AsRef<Path>>(&self, path: P, overwrite: bool) -> Result<(), Report> {
104 SmartSerde::to_file(&self.data, path, overwrite)
105 }
106}
107
108#[derive(Debug, Clone)]
109pub struct InputParamCard<T>
110where
111 T: From<f64> + Clone + Serialize + DeserializeOwned,
112{
113 data: HashMap<UFOSymbol, Complex<T>>,
114}
115
116impl<T> Default for InputParamCard<T>
117where
118 T: From<f64> + Clone + Serialize + DeserializeOwned,
119{
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125impl<T> InputParamCard<T>
126where
127 T: From<f64> + Clone + Serialize + DeserializeOwned,
128{
129 pub fn new() -> Self {
131 Self {
132 data: HashMap::new(),
133 }
134 }
135
136 pub fn insert(&mut self, sym: UFOSymbol, value: Complex<T>) -> Option<Complex<T>> {
138 self.data.insert(sym, value)
139 }
140
141 pub fn get(&self, sym: &UFOSymbol) -> Option<&Complex<T>> {
143 self.data.get(sym)
144 }
145
146 pub fn get_mut(&mut self, sym: &UFOSymbol) -> Option<&mut Complex<T>> {
148 self.data.get_mut(sym)
149 }
150
151 pub fn remove(&mut self, sym: &UFOSymbol) -> Option<Complex<T>> {
153 self.data.remove(sym)
154 }
155
156 pub fn iter(&self) -> impl Iterator<Item = (&UFOSymbol, &Complex<T>)> {
158 self.data.iter()
159 }
160
161 pub fn to_serializable(&self) -> SerializableInputParamCard<T> {
162 SerializableInputParamCard::from_input_param_card(self)
163 }
164
165 pub fn from_serializable(
166 serializable_input_param_card: &SerializableInputParamCard<T>,
167 ) -> Self {
168 let data: HashMap<UFOSymbol, Complex<T>> = serializable_input_param_card
169 .data
170 .iter()
171 .map(|(k, v)| {
172 (
173 UFOSymbol::from(k.as_str()),
174 Complex::new(v.0.clone(), v.1.clone()),
175 )
176 })
177 .collect();
178 InputParamCard { data }
179 }
180
181 pub fn from_file(file_path: impl AsRef<Path>) -> Result<Self, Report> {
182 let serializable_input_param_card = SerializableInputParamCard::from_file(file_path)?;
183 Ok(Self::from_serializable(&serializable_input_param_card))
184 }
185 pub fn from_str(s: String, format: &str) -> Result<Self, Report> {
186 let serializable_input_param_card = SerializableInputParamCard::from_str(s, format)?;
187 Ok(Self::from_serializable(&serializable_input_param_card))
188 }
189
190 pub fn to_file<P: AsRef<Path>>(&self, path: P, overwrite: bool) -> Result<(), Report> {
191 let serializable_card = self.to_serializable();
192 serializable_card.to_file(path, overwrite)?;
193 Ok(())
194 }
195}
196
197impl InputParamCard<F<f64>> {
198 pub fn apply_to_model(&self, model: &mut Model) -> Result<(), Report> {
199 for (param, value) in &self.data {
200 if let Some(model_param) = model.get_parameter_mut_opt(param.namespaceless_string()) {
201 model_param.value = Some(*value);
202 } else {
203 return Err(eyre!(
204 "Parameter {} not found in model when applying input parameter card",
205 param
206 ));
207 }
208 }
209 model.recompute_dependents()
210 }
211}
212
213impl InputParamCard<F<f64>> {
214 pub fn default_from_model(model: &Model) -> Self {
215 let mut card = InputParamCard::new();
216 for param in model.parameters.values() {
217 if param.nature == ParameterNature::External
218 && !param.name.is_zero()
219 && let Some(value) = param.value
220 {
221 card.insert(param.name, value);
222 }
223 }
224 card
225 }
226}
227
228impl<T> std::ops::Deref for InputParamCard<T>
230where
231 T: From<f64> + Clone + Serialize + DeserializeOwned,
232{
233 type Target = HashMap<UFOSymbol, Complex<T>>;
234 fn deref(&self) -> &Self::Target {
235 &self.data
236 }
237}
238impl<T> std::ops::DerefMut for InputParamCard<T>
239where
240 T: From<f64> + Clone + Serialize + DeserializeOwned,
241{
242 fn deref_mut(&mut self) -> &mut Self::Target {
243 &mut self.data
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
248pub struct UFOSymbol(pub Symbol);
249
250impl Display for UFOSymbol {
251 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
252 write!(f, "{}", self.0)
253 }
254}
255
256impl From<UFOSymbol> for Atom {
257 fn from(value: UFOSymbol) -> Self {
258 Atom::var(value.0)
259 }
260}
261
262impl UFOSymbol {
263 pub fn zero() -> Self {
264 UFOSymbol(GS.ufozero)
265 }
266
267 pub fn is_zero(&self) -> bool {
268 self.0 == Self::zero().0
269 }
270
271 pub fn namespaceless_string(&self) -> &str {
272 self.0.get_stripped_name()
273 }
274}
275
276#[test]
277fn zerosym() {
278 println!("{}", Atom::num(1) * Atom::from(UFOSymbol::zero()));
279 println!("{}", Atom::from(UFOSymbol::zero()));
280 }
282
283#[test]
284fn ufo_symbol_typst_quoting_requires_typst_mode() {
285 let atom = Atom::from(UFOSymbol::from("typst_mode_probe"));
286
287 assert_eq!(
288 atom.printer(SpensoPrintSettings::typst_options())
289 .to_string(),
290 r#""typst_mode_probe""#
291 );
292
293 let mut symbolica = SpensoPrintSettings::typst().nice_symbolica();
294 symbolica.color_builtin_symbols = false;
295 assert_eq!(atom.printer(symbolica).to_string(), "typst_mode_probe");
296}
297
298#[test]
299fn model_forward_references_preserve_ufo_symbol_printers() -> Result<()> {
300 crate::initialisation::test_initialise()?;
301 let mut serialized = Model::default().to_serializable();
302 serialized.parameters = [
303 ("forward_parameter", "UFO::later_parameter"),
304 ("later_parameter", "1"),
305 ("ZERO", "0"),
306 ]
307 .into_iter()
308 .map(|(name, expression)| SerializableParameter {
309 name: name.into(),
310 lhablock: None,
311 lhacode: None,
312 nature: ParameterNature::Internal,
313 parameter_type: ParameterType::Real,
314 value: None,
315 expression: Some(expression.into()),
316 })
317 .collect();
318 serialized.couplings = [
319 ("forward_coupling", "UFO::later_coupling"),
320 ("later_coupling", "UFO::later_parameter"),
321 ]
322 .into_iter()
323 .map(|(name, expression)| SerializableCoupling {
324 name: name.into(),
325 expression: expression.into(),
326 orders: BTreeMap::new(),
327 value: None,
328 })
329 .collect();
330
331 let model = Model::from_serializable_model(serialized);
332 for name in ["later_parameter", "later_coupling"] {
333 let symbol = UFOSymbol::from(name);
334 assert!(symbol.0.get_print_function().is_some(), "{name}");
335 let atom = Atom::from(symbol);
336 assert_eq!(
337 atom.printer(SpensoPrintSettings::typst_options())
338 .to_string(),
339 format!("\"{name}\"")
340 );
341 let mut plain = SpensoPrintSettings::typst().nice_symbolica();
342 plain.color_builtin_symbols = false;
343 assert_eq!(atom.printer(plain).to_string(), name);
344 }
345 assert_eq!(
346 model.parameters[&ParameterName(UFOSymbol::from("forward_parameter"))]
347 .expression
348 .as_ref(),
349 Some(&Atom::from(UFOSymbol::from("later_parameter")))
350 );
351 assert_eq!(
352 model.couplings[&CouplingName(UFOSymbol::from("forward_coupling"))].expression,
353 Atom::from(UFOSymbol::from("later_coupling"))
354 );
355 assert!(
356 model.parameters[&ParameterName(UFOSymbol::zero())]
357 .name
358 .is_zero()
359 );
360 assert_eq!(UFOSymbol::from("ZERO"), UFOSymbol::zero());
361 assert!(UFOSymbol::zero().0.get_normalization_function().is_some());
362 Ok(())
363}
364
365impl<T> From<T> for UFOSymbol
366where
367 T: AsRef<str>,
368{
369 fn from(s: T) -> Self {
370 let is_zero = s.as_ref() == "ZERO";
371 if is_zero {
372 UFOSymbol::zero()
373 } else {
374 let name = format!("UFO::{}", s.as_ref());
375 if let Some(a) = get_symbol!(&name) {
376 UFOSymbol(a)
377 } else {
378 UFOSymbol(symbol!(
379 &name,
380 print = |a, opt, _state| {
381 let AtomView::Var(a) = a else {
382 return None;
383 };
384 match opt.custom_print_mode.get("spenso") {
385 Some(PrintUserData::Integer(_)) if opt.typst_mode().is_some() => {
386 Some(format!("\"{}\"", a.get_symbol().get_stripped_name()))
387 }
388 _ => None,
389 }
390 }
391 ))
392 }
393 }
394 }
395}
396
397#[derive(Debug, Clone)]
398pub struct ArcPropagator(pub Arc<Propagator>);
399impl Deref for ArcPropagator {
400 type Target = Propagator;
401 fn deref(&self) -> &Self::Target {
402 self.0.deref()
403 }
404}
405
406impl Encode for ArcPropagator {
407 fn encode<E: bincode::enc::Encoder>(
408 &self,
409 encoder: &mut E,
410 ) -> std::result::Result<(), bincode::error::EncodeError> {
411 Encode::encode(&self.0.name.to_string(), encoder)?;
412 Ok(())
413 }
414}
415
416impl<T: HasModel> Decode<T> for ArcPropagator {
417 fn decode<D: bincode::de::Decoder<Context = T>>(
418 decoder: &mut D,
419 ) -> std::result::Result<Self, bincode::error::DecodeError> {
420 let name: String = Decode::decode(decoder)?;
421 let context = decoder.context();
422 let model = context.get_model();
423 let prop = model.get_propagator(&name);
424 Ok(prop)
425 }
426}
427
428#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)]
429pub struct ArcParticle(pub Arc<Particle>);
430
431impl Deref for ArcParticle {
432 type Target = Particle;
433 fn deref(&self) -> &Self::Target {
434 self.0.deref()
435 }
436}
437
438#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)]
439pub struct ArcVertexRule(pub Arc<VertexRule>);
440impl Deref for ArcVertexRule {
441 type Target = VertexRule;
442 fn deref(&self) -> &Self::Target {
443 self.0.deref()
444 }
445}
446
447impl Encode for ArcParticle {
448 fn encode<E: bincode::enc::Encoder>(
449 &self,
450 encoder: &mut E,
451 ) -> std::result::Result<(), bincode::error::EncodeError> {
452 Encode::encode(&self.0.pdg_code, encoder)?;
453 Ok(())
454 }
455}
456
457impl<T: HasModel> Decode<T> for ArcParticle {
458 fn decode<D: bincode::de::Decoder<Context = T>>(
459 decoder: &mut D,
460 ) -> std::result::Result<Self, bincode::error::DecodeError> {
461 let pdg_code: isize = Decode::decode(decoder)?;
462 let context = decoder.context();
463 let model = context.get_model();
464 let particle = model.get_particle_from_pdg(pdg_code);
465 Ok(particle)
466 }
467}
468
469impl Encode for ArcVertexRule {
470 fn encode<E: bincode::enc::Encoder>(
471 &self,
472 encoder: &mut E,
473 ) -> std::result::Result<(), bincode::error::EncodeError> {
474 Encode::encode(&self.0.name.to_string(), encoder)?;
475 Ok(())
476 }
477}
478
479impl<T: HasModel> Decode<T> for ArcVertexRule {
480 fn decode<D: bincode::de::Decoder<Context = T>>(
481 decoder: &mut D,
482 ) -> std::result::Result<Self, bincode::error::DecodeError> {
483 let vertex_rule_name: String = Decode::decode(decoder)?;
484 let context = decoder.context();
485 let model = context.get_model();
486 let vertex_rule = model.get_vertex_rule(vertex_rule_name);
487 Ok(vertex_rule)
488 }
489}
490
491#[allow(unused)]
492pub(crate) fn normalise_complex(atom: &Atom) -> Atom {
493 let re = parse!("re_");
494 let im = parse!("im_");
495
496 let comp_id = symbol!("complex");
497
498 let complexfn = function!(comp_id, re, im).to_pattern();
499
500 let i = Atom::i();
501 let complexpanded = &re + i * &im;
502
503 atom.replace(&complexfn).with(complexpanded.to_pattern())
504}
505
506#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
507pub enum ParameterNature {
508 #[default]
509 #[serde(rename = "external")]
510 External,
511 #[serde(rename = "internal")]
512 Internal,
513}
514
515#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
516pub enum ParameterType {
517 #[default]
518 #[serde(rename = "real")]
519 Real,
520 #[serde(rename = "complex")]
521 Imaginary,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct SerializableVertexRule {
526 pub name: SmartString<LazyCompact>,
527 pub particles: Vec<SmartString<LazyCompact>>,
528 pub color_structures: Vec<SmartString<LazyCompact>>,
529 pub lorentz_structures: Vec<SmartString<LazyCompact>>,
530 pub couplings: Vec<Vec<Option<SmartString<LazyCompact>>>>,
531}
532
533impl SerializableVertexRule {
534 pub(crate) fn from_vertex_rule(vertex_rule: &VertexRule) -> SerializableVertexRule {
535 SerializableVertexRule {
536 name: vertex_rule.name.clone(),
537 particles: vertex_rule
538 .particles
539 .iter()
540 .map(|particle| particle.0.name.clone())
541 .collect(),
542 color_structures: vertex_rule
543 .color_structures
544 .iter()
545 .map(|a| a.to_canonical_string())
546 .map(SmartString::from)
547 .collect(),
548 lorentz_structures: vertex_rule
549 .lorentz_structures
550 .iter()
551 .map(|lorentz_structure| lorentz_structure.name.clone())
552 .collect(),
553 couplings: vertex_rule
554 .couplings
555 .iter()
556 .map(|couplings| {
557 couplings
558 .iter()
559 .map(|coupling| {
560 coupling
561 .as_ref()
562 .map(|cpl| cpl.namespaceless_string().into())
563 })
564 .collect()
565 })
566 .collect(),
567 }
568 }
569}
570
571#[derive(Debug, Clone, PartialEq, Eq)]
572pub struct ColorStructure {
573 pub color_structure: Vec<Atom>,
574}
575
576impl ColorStructure {
577 pub(crate) fn iter(&'_ self) -> std::slice::Iter<'_, Atom> {
578 self.color_structure.iter()
579 }
580}
581
582impl FromIterator<Atom> for ColorStructure {
583 fn from_iter<T: IntoIterator<Item = Atom>>(iter: T) -> Self {
584 ColorStructure {
585 color_structure: iter.into_iter().collect(),
586 }
587 }
588}
589
590#[derive(Debug, Clone)]
591pub struct VertexRule {
592 pub name: SmartString<LazyCompact>,
593 pub particles: Vec<ArcParticle>,
594 pub color_structures: ColorStructure,
595 pub lorentz_structures: Vec<Arc<LorentzStructure>>,
596 pub couplings: Vec<Vec<Option<CouplingName>>>,
597 pub dod: i32,
598}
599
600impl Eq for VertexRule {}
601
602impl PartialEq for VertexRule {
603 fn eq(&self, other: &Self) -> bool {
604 self.name == other.name
605 }
606}
607
608impl PartialOrd for VertexRule {
609 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
610 Some(self.cmp(other))
611 }
612}
613
614impl Ord for VertexRule {
615 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
616 self.name.cmp(&other.name)
617 }
618}
619
620impl std::hash::Hash for VertexRule {
621 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
622 self.name.hash(state);
623 }
624}
625
626impl VertexRule {
627 pub(crate) fn tensors(
628 &self,
629 i: Aind,
630 j: Aind,
631 ) -> [ParamTensor<OrderedStructure<Euclidean, Aind>>; 3] {
632 let spin_structure = self
633 .lorentz_structures
634 .iter()
635 .map(|ls| ls.structure.clone())
636 .collect_vec();
637
638 let color_structure: Vec<Atom> = self.color_structures.color_structure.clone();
639
640 let i = Euclidean {}.new_slot(color_structure.len(), i);
641 let j = Euclidean {}.new_slot(spin_structure.len(), j);
642
643 let color_structure: ParamTensor<OrderedStructure<Euclidean, Aind>> =
644 ParamTensor::composite(DataTensor::Dense(
645 DenseTensor::from_storage_data(
646 color_structure,
647 Canonicalized::from_iter([i]).into_canonical(),
648 )
649 .unwrap(),
650 ));
651
652 let spin_structure: ParamTensor<OrderedStructure<Euclidean, Aind>> =
653 ParamTensor::composite(DataTensor::Dense(
654 DenseTensor::from_storage_data(
655 spin_structure,
656 Canonicalized::from_iter([j]).into_canonical(),
657 )
658 .unwrap(),
659 ));
660
661 let coupling_structure = Canonicalized::from_iter([i, j]);
662 let coupling_layout = TensorDataLayout::from_canonicalized(&coupling_structure)
663 .expect("vertex coupling dimensions are concrete");
664 let mut couplings: ParamTensor<OrderedStructure<Euclidean, Aind>> =
665 ParamTensor::composite(DataTensor::Sparse(SparseTensor::empty(
666 coupling_structure.into_canonical(),
667 Atom::Zero,
668 )));
669
670 for (i, row) in self.couplings.iter().enumerate() {
671 for (j, col) in row.iter().enumerate() {
672 if let Some(atom) = col {
673 let storage_indices = coupling_layout
674 .logical_expanded_to_storage_expanded(&[i, j])
675 .expect("vertex coupling coordinates match their dimensions");
676 couplings.set(&storage_indices, atom.0.into()).unwrap();
677 }
678 }
679 }
680
681 [color_structure, couplings, spin_structure]
682 }
683
684 }
696
697impl VertexRule {
698 pub(crate) fn coupling_orders(
699 &self,
700 model: &Model,
701 ) -> AHashMap<SmartString<LazyCompact>, usize> {
702 let mut node_coupling_orders = AHashMap::default();
703 self.couplings.iter().for_each(|cs| {
704 cs.iter().for_each(|c_opt| {
705 if let Some(c) = c_opt {
706 model.couplings[c]
707 .orders
708 .iter()
709 .for_each(|(coupling_order, &weight)| {
710 let w = node_coupling_orders
711 .entry(coupling_order.clone())
712 .or_insert(weight);
713 if *w < weight {
714 *w = weight;
715 }
716 });
717 }
718 })
719 });
720 node_coupling_orders
721 }
722
723 pub(crate) fn from_serializable_vertex_rule(
724 model: &Model,
725 vertex_rule: &SerializableVertexRule,
726 ) -> VertexRule {
727 let lorentz_structures: Vec<Arc<LorentzStructure>> = vertex_rule
728 .lorentz_structures
729 .iter()
730 .map(|lorentz_structure_name| {
731 model.get_lorentz_structure(lorentz_structure_name).clone()
732 })
733 .collect();
734
735 let dod = lorentz_structures
736 .iter()
737 .map(|a| a.dod())
738 .reduce(|a, b| a.max(b))
739 .unwrap();
740 VertexRule {
741 name: vertex_rule.name.clone(),
742 particles: vertex_rule
743 .particles
744 .iter()
745 .map(|particle_name| model.get_particle(particle_name).clone())
746 .collect(),
747 color_structures: vertex_rule
748 .color_structures
749 .iter()
750 .map(|color_structure_name| {
751 utils::parse_python_expression(color_structure_name.as_str())
752 })
753 .collect(),
754 lorentz_structures: vertex_rule
755 .lorentz_structures
756 .iter()
757 .map(|lorentz_structure_name| {
758 model.get_lorentz_structure(lorentz_structure_name).clone()
759 })
760 .collect(),
761 couplings: vertex_rule
762 .couplings
763 .iter()
764 .map(|coupling_names| {
765 coupling_names
766 .iter()
767 .map(|coupling_name| {
768 coupling_name
769 .as_ref()
770 .map(|cpl_name| CouplingName(UFOSymbol::from(cpl_name)))
771 })
772 .collect()
773 })
774 .collect(),
775 dod,
776 }
777 }
778}
779
780#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct SerializablePropagator {
782 pub name: SmartString<LazyCompact>,
783 pub particle: SmartString<LazyCompact>,
784 pub numerator: SmartString<LazyCompact>,
785 pub denominator: SmartString<LazyCompact>,
786}
787
788impl SerializablePropagator {
789 pub(crate) fn from_propagator(propagator: &Propagator) -> SerializablePropagator {
790 SerializablePropagator {
791 name: propagator.name.clone(),
792 particle: propagator.particle.0.name.clone(),
793 numerator: propagator.numerator.to_canonical_string().into(),
794 denominator: propagator.denominator.to_canonical_string().into(),
795 }
796 }
797}
798
799#[derive(Debug, Clone, Encode)]
800pub struct Propagator {
801 #[bincode(with_serde)]
802 pub name: SmartString<LazyCompact>,
803 pub particle: ArcParticle,
804 pub numerator: Atom,
805 pub denominator: Atom,
806 pub dod: i32,
807}
808
809impl Propagator {
810 pub(crate) fn from_serializable_propagator(
811 model: &Model,
812 propagator: &SerializablePropagator,
813 zero_mass_replacements: &[Replacement],
814 ) -> Propagator {
815 let numerator = utils::parse_python_expression(propagator.numerator.as_str());
816 let denominator = utils::parse_python_expression(propagator.denominator.as_str());
817 let numerator = numerator.replace_multiple(zero_mass_replacements);
818 let denominator = denominator.replace_multiple(zero_mass_replacements);
819 let dod = (&numerator / &denominator).all_dod();
820 Propagator {
821 name: propagator.name.clone(),
822 particle: model.get_particle(&propagator.particle).clone(),
823 numerator,
824 denominator,
825 dod,
826 }
827 }
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct SerializableCoupling {
832 name: SmartString<LazyCompact>,
833 expression: SmartString<LazyCompact>,
834 #[serde(with = "crate::utils::serde_utils::map_as_pairs")]
835 orders: BTreeMap<SmartString<LazyCompact>, usize>,
836 value: Option<(f64, f64)>,
837}
838
839impl SerializableCoupling {
840 pub(crate) fn from_coupling(coupling: &Coupling) -> SerializableCoupling {
841 SerializableCoupling {
842 name: coupling.name.namespaceless_string().into(),
843 expression: coupling.expression.to_canonical_string().into(),
844 orders: coupling.orders.clone(),
845 value: coupling.value.map(|value| (value.re, value.im)),
846 }
847 }
848}
849
850#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
851pub struct CouplingName(pub UFOSymbol);
852
853impl Deref for CouplingName {
854 type Target = UFOSymbol;
855 fn deref(&self) -> &Self::Target {
856 &self.0
857 }
858}
859
860#[derive(Debug, Clone)]
861pub struct Coupling {
862 pub name: UFOSymbol,
863 pub expression: Atom,
864 pub orders: BTreeMap<SmartString<LazyCompact>, usize>,
865 pub value: Option<Complex<f64>>,
866}
867
868impl Coupling {
869 pub(crate) fn from_serializable_coupling(coupling: &SerializableCoupling) -> Coupling {
870 Coupling {
871 name: (&coupling.name).into(),
872 expression: utils::parse_python_expression(coupling.expression.as_str()),
873 orders: coupling.orders.clone(),
874 value: coupling.value.map(|value| Complex::new(value.0, value.1)),
875 }
876 }
877
878 pub(crate) fn rep_rule(&self) -> [Atom; 2] {
879 let lhs = self.name.into();
880 let rhs = self.expression.clone();
882
883 [lhs, rhs]
884 }
885}
886
887#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct SerializableParticle {
889 pdg_code: isize,
890 name: SmartString<LazyCompact>,
891 antiname: SmartString<LazyCompact>,
892 spin: isize,
893 color: isize,
894 mass: SmartString<LazyCompact>,
895 width: SmartString<LazyCompact>,
896 texname: SmartString<LazyCompact>,
897 antitexname: SmartString<LazyCompact>,
898 charge: SerializedCharge,
899 ghost_number: isize,
900 lepton_number: isize,
901 y_charge: Option<SerializedCharge>,
902 y_charge_right: Option<SerializedCharge>,
903 #[serde(default, alias = "goldstoneboson", alias = "GoldstoneBoson")]
904 goldstone: bool,
905 chemical_potential: Option<SmartString<LazyCompact>>,
906}
907
908impl SerializableParticle {
909 pub(crate) fn from_particle(particle: &Particle) -> SerializableParticle {
910 SerializableParticle {
911 pdg_code: particle.pdg_code,
912 name: particle.name.clone(),
913 antiname: particle.antiname.clone(),
914 spin: particle.spin,
915 color: particle.color,
916 mass: particle.mass.namespaceless_string().into(),
917 width: particle.width.namespaceless_string().into(),
918 texname: particle.texname.clone(),
919 antitexname: particle.antitexname.clone(),
920 charge: SerializedCharge(particle.charge.clone()),
921 ghost_number: particle.ghost_number,
922 lepton_number: particle.lepton_number,
923 y_charge: particle.y_charge.clone().map(SerializedCharge),
924 y_charge_right: particle.y_charge_right.clone().map(SerializedCharge),
925 goldstone: particle.goldstone,
926 chemical_potential: particle
927 .chemical_potential
928 .as_ref()
929 .map(|chemical_potential| chemical_potential.namespaceless_string().into()),
930 }
931 }
932}
933
934#[derive(Debug, Clone)]
935pub struct Particle {
936 pub pdg_code: isize,
937 pub name: SmartString<LazyCompact>,
938 pub antiname: SmartString<LazyCompact>,
939 pub spin: isize,
940 pub color: isize,
941 pub mass: ParameterName,
942 pub width: ParameterName,
943 pub texname: SmartString<LazyCompact>,
944 pub antitexname: SmartString<LazyCompact>,
945 pub charge: Rational,
946 pub ghost_number: isize,
947 pub lepton_number: isize,
948 pub y_charge: Option<Rational>,
949 pub y_charge_right: Option<Rational>,
950 pub goldstone: bool,
951 pub chemical_potential: Option<ParameterName>,
952}
953
954impl Particle {
955 fn fermion_polarization_sum_rhs(&self, eid: EdgeIndex) -> Atom {
956 assert!(self.is_spinor());
957 let mu: Slot<Minkowski, Aind> = Minkowski {}.new_rep(4).slot(Aind::new_dummy());
958 let mass_sign = if self.is_antiparticle() { -1 } else { 1 };
959
960 GS.emr_mom(eid, mu.to_atom()) * function!(AGS.gamma, W_.a_, W_.b_, mu.to_atom())
961 + Atom::num(mass_sign) * Atom::from(self.mass.0) * function!(ETS.metric, W_.a_, W_.b_)
962 }
963
964 fn polarization_average_factor(&self) -> Result<Atom> {
965 match self.spin {
966 1 => Ok(Atom::one()),
967 2 => Ok(Atom::num(1) / Atom::num(2)),
968 3 => Ok(if self.is_massive() {
969 Atom::num(1) / Atom::num(3)
970 } else {
971 Atom::num(1) / Atom::num(2)
972 }),
973 4 => Ok(Atom::num(1) / Atom::num(4)),
974 5 => Ok(if self.is_massive() {
975 Atom::num(1) / Atom::num(5)
976 } else {
977 Atom::num(1) / Atom::num(4)
978 }),
979 spin => Err(eyre!(
980 "Polarization averaging for particle '{}' (PDG {}, spin {}) is not supported yet.",
981 self.name,
982 self.pdg_code,
983 spin
984 )),
985 }
986 }
987
988 fn vector_polarization_sum_rhs(
989 &self,
990 eid: EdgeIndex,
991 gauge: VectorPolarizationSumGauge,
992 ) -> Atom {
993 assert!(self.is_vector());
994
995 let minus_metric = -function!(ETS.metric, W_.a_, W_.b_);
996
997 match gauge {
998 VectorPolarizationSumGauge::Feynman => minus_metric,
999 VectorPolarizationSumGauge::LightLikeAxial => {
1000 if self.is_massive() {
1001 let mass_squared = Atom::from(self.mass.0).pow(2);
1002
1003 minus_metric + GS.emr_mom(eid, W_.a_) * GS.emr_mom(eid, W_.b_) / mass_squared
1004 } else {
1005 let temporal_component = GS.emr_mom(eid, GS.cind(0));
1006 let n_a = temporal_component.clone() * GS.energy_delta(W_.a_)
1007 - GS.emr_vec(eid, W_.a_);
1008 let n_b = temporal_component.clone() * GS.energy_delta(W_.b_)
1009 - GS.emr_vec(eid, W_.b_);
1010 let minkowski = Minkowski {}.new_rep(4).to_symbolic([]);
1013 let q_dot_n = temporal_component.pow(2)
1014 - function!(
1015 SPENSO_TAG.dot,
1016 GS.emr_vec(eid, minkowski.as_view()),
1017 GS.emr_vec(eid, minkowski.as_view())
1018 );
1019
1020 minus_metric
1021 + (GS.emr_mom(eid, W_.a_) * n_b + n_a * GS.emr_mom(eid, W_.b_)) / q_dot_n
1022 }
1023 }
1024 }
1025 }
1026
1027 pub(crate) fn polarization_sum(
1028 &self,
1029 eid: EdgeIndex,
1030 average: bool,
1031 vector_polarization_sum_gauge: VectorPolarizationSumGauge,
1032 ) -> Result<Option<Replacement>> {
1033 let average_factor = if average {
1034 self.polarization_average_factor()?
1035 } else {
1036 Atom::one()
1037 };
1038 Ok(match self.spin {
1039 1 => None,
1040 2 => Some(if !self.is_antiparticle() {
1041 (function!(GS.u, eid.0, W_.a_) * function!(GS.ubar, eid.0, W_.b_))
1042 .replace_with(self.fermion_polarization_sum_rhs(eid) * average_factor)
1043 } else {
1044 (function!(GS.v, eid.0, W_.a_) * function!(GS.vbar, eid.0, W_.b_))
1045 .replace_with(self.fermion_polarization_sum_rhs(eid) * average_factor)
1046 }),
1047 3 => Some(
1048 (function!(GS.epsilon, eid.0, W_.a_) * function!(GS.epsilonbar, eid.0, W_.b_))
1049 .replace_with(
1050 self.vector_polarization_sum_rhs(eid, vector_polarization_sum_gauge)
1051 * average_factor,
1052 ),
1053 ),
1054 spin => {
1055 return Err(eyre!(
1056 "Polarization sum replacement for particle '{}' (PDG {}, spin {}) is not implemented yet.",
1057 self.name,
1058 self.pdg_code,
1059 spin
1060 ));
1061 }
1062 })
1063 }
1064 pub(crate) fn random_helicity(&self, seed: u64) -> Helicity {
1065 let mut rng = SmallRng::seed_from_u64(seed);
1066 if self.is_spinor() {
1067 if rng.random_bool(0.5) {
1068 Helicity::PLUS
1069 } else {
1070 Helicity::MINUS
1071 }
1072 } else if self.is_vector() {
1073 Helicity::try_from(rng.random_range(1..=1)).unwrap()
1074 } else {
1075 Helicity::ZERO
1076 }
1077 }
1078
1079 pub(crate) fn is_massless(&self) -> bool {
1080 !self.is_massive()
1081 }
1082
1083 pub(crate) fn resolved_mass_value(&self, model: &Model) -> Result<Complex<F<f64>>> {
1084 model
1085 .parameters
1086 .get(&self.mass)
1087 .and_then(|parameter| parameter.value)
1088 .ok_or_else(|| {
1089 eyre!(
1090 "Particle '{}' (PDG {}) requires a resolved value for mass parameter '{}' in model '{}'.",
1091 self.name,
1092 self.pdg_code,
1093 self.mass.0,
1094 model.name
1095 )
1096 })
1097 }
1098
1099 pub(crate) fn has_zero_resolved_mass(&self, model: &Model) -> Result<bool> {
1100 let mass_value = self.resolved_mass_value(model)?;
1101 Ok(mass_value.re == mass_value.re.zero() && mass_value.im == mass_value.im.zero())
1102 }
1103
1104 pub(crate) fn is_qcd_charged(&self) -> bool {
1105 self.color != 1
1106 }
1107 pub fn is_fermion(&self) -> bool {
1124 self.spin % 2 == 0
1125 }
1126
1127 pub fn is_vector(&self) -> bool {
1128 self.spin == 3
1129 }
1130
1131 pub fn is_tensor(&self) -> bool {
1132 self.spin == 5
1133 }
1134
1135 pub fn is_scalar(&self) -> bool {
1136 self.spin == 1
1137 }
1138
1139 pub fn is_spinor(&self) -> bool {
1140 self.spin == 2
1141 }
1142
1143 pub fn is_ghost(&self) -> bool {
1144 self.ghost_number != 0
1145 }
1146
1147 pub fn is_anticommutating(&self) -> bool {
1148 self.is_fermion() || self.is_ghost()
1149 }
1150
1151 pub fn is_goldstone(&self) -> bool {
1152 self.goldstone
1153 }
1154
1155 pub fn symbolic_mass(&self) -> Atom {
1156 self.mass.0.into()
1157 }
1158}
1159
1160impl Ord for Particle {
1161 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1162 self.pdg_code.cmp(&other.pdg_code)
1163 }
1164}
1165
1166impl PartialOrd for Particle {
1167 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1168 Some(self.cmp(other))
1169 }
1170}
1171
1172impl std::hash::Hash for Particle {
1173 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1174 self.pdg_code.hash(state);
1175 }
1176}
1177impl Eq for Particle {}
1178impl PartialEq for Particle {
1179 fn eq(&self, other: &Self) -> bool {
1180 if self.pdg_code == other.pdg_code {
1181 true
1249 } else {
1250 false
1251 }
1252 }
1253}
1254
1255#[derive(Debug, Clone, Serialize, Deserialize)]
1256pub struct InOutIndex {
1257 incoming: Slot<LibraryRep>,
1258 outgoing: Slot<LibraryRep>,
1259}
1260
1261#[derive(Debug, Clone, bincode_trait_derive::Encode, bincode_trait_derive::Decode)]
1262#[trait_decode(trait = symbolica::state::HasStateMap)]
1263pub struct EdgeSlots<LorRep: RepName> {
1264 pub lorentz: Vec<Slot<LorRep>>,
1265 spin: Vec<Slot<Bispinor>>,
1266 pub color: Vec<Slot<LibraryRep>>,
1267}
1268
1269impl From<EdgeSlots<Minkowski>> for OrderedStructure {
1270 fn from(value: EdgeSlots<Minkowski>) -> Self {
1271 Canonicalized::<OrderedStructure>::from(
1272 value
1273 .lorentz
1274 .into_iter()
1275 .map(|x| x.to_lib())
1276 .chain(value.spin.into_iter().map(|x| x.to_lib()))
1277 .chain(value.color)
1278 .collect_vec(),
1279 )
1280 .into_canonical()
1281 }
1282}
1283
1284impl<LorRep: BaseRepName> Display for EdgeSlots<LorRep> {
1285 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1286 write!(f, "Lorentz: ")?;
1287 for l in &self.lorentz {
1288 write!(f, "{} ", l)?;
1289 }
1290 write!(f, "Spin: ")?;
1291 for s in &self.spin {
1292 write!(f, "{} ", s)?;
1293 }
1294 write!(f, "Color: ")?;
1295 for c in &self.color {
1296 write!(f, "{} ", c)?;
1297 }
1298 Ok(())
1299 }
1300}
1301
1302impl From<EdgeSlots<Lorentz>> for OrderedStructure {
1303 fn from(value: EdgeSlots<Lorentz>) -> Self {
1304 Canonicalized::<OrderedStructure>::from(
1305 value
1306 .lorentz
1307 .into_iter()
1308 .map(|x| x.to_lib())
1309 .chain(value.spin.into_iter().map(|a| a.to_lib()))
1310 .chain(value.color)
1311 .collect_vec(),
1312 )
1313 .into_canonical()
1314 }
1315}
1316
1317impl Particle {
1318 pub fn is_antiparticle(&self) -> bool {
1319 self.pdg_code < 0
1320 }
1321
1322 pub(crate) fn get_anti_particle(&self, model: &Model) -> ArcParticle {
1323 model.get_particle(&self.antiname)
1324 }
1325
1326 pub fn is_self_antiparticle(&self) -> bool {
1327 self.name == self.antiname
1328 }
1329
1330 pub(crate) fn spin_reps(&self) -> IndexLess<LibraryRep, Aind> {
1331 Canonicalized::<IndexLess<LibraryRep, Aind>>::from_iter(match self.spin {
1332 -1..=1 => vec![],
1333 a => {
1334 if a > 0 {
1335 if a % 2 == 0 {
1336 vec![Bispinor {}.new_rep(4).cast(); a.div_euclid(2) as usize]
1337 } else {
1338 vec![Minkowski {}.new_rep(4).cast(); a.div_euclid(2) as usize]
1339 }
1340 } else {
1341 vec![]
1342 }
1343 }
1344 })
1345 .into_canonical()
1346 }
1347
1348 pub(crate) fn is_massive(&self) -> bool {
1349 !self.mass.is_zero()
1350 }
1351
1352 pub fn generate_edge_typst_dict(&self) -> String {
1354 let label = format!("mi(`{}`)", self.texname);
1355 let thickness = if self.is_massive() {
1357 "massive"
1358 } else {
1359 "massless"
1360 };
1361
1362 let color = if !self.charge.is_zero() {
1364 "palette.accent"
1365 } else {
1366 "palette.ink"
1367 };
1368
1369 let base_source = format!("source-stroke(c: {}, thickness: {})", color, thickness);
1371 let base_sink = format!("sink-stroke(c: {}, thickness: {})", color, thickness);
1372
1373 let (source, sink) = if self.is_ghost() {
1374 (
1375 format!("source-stroke(c: {color}, thickness: {thickness}, dash: dotted)",),
1376 format!("sink-stroke(c: {color}, thickness: {thickness}, dash: dotted)"),
1377 )
1378 } else if self.is_fermion() {
1379 (base_source, base_sink)
1380 } else if self.is_vector() {
1381 if self.charge.is_zero() && self.color == 1 {
1383 (
1385 format!("{} + wave", base_source),
1386 format!("{} + wave", base_sink),
1387 )
1388 } else if self.charge.is_zero() && self.color == 8 {
1389 (
1391 format!("{} + coil", base_source),
1392 format!("{} + coil", base_sink),
1393 )
1394 } else {
1395 (
1397 format!("{} + zigzag", base_source),
1398 format!("{} + zigzag", base_sink),
1399 )
1400 }
1401 } else if self.is_scalar() {
1402 (
1404 format!("source-stroke(c: {color}, thickness: {thickness}, dash: dashed)",),
1405 format!("sink-stroke(c: {color}, thickness: {thickness}, dash: dashed)"),
1406 )
1407 } else {
1408 (base_source, base_sink)
1410 };
1411
1412 let flow_marker = if self.is_fermion() && !self.is_ghost() {
1413 " + fermion-flow"
1414 } else {
1415 ""
1416 };
1417
1418 format!("(source:{source}, sink:{sink}, label:{label}){flow_marker}")
1419 }
1420
1421 pub(crate) fn color_reps(&self, flow: Flow) -> IndexLess {
1422 let reps = match flow {
1423 Flow::Source => match self.color {
1424 3 => vec![ColorFundamental {}.new_rep(3).cast()],
1425
1426 -3 => vec![ColorFundamental {}.dual().new_rep(3).cast()],
1427 6 => vec![ColorSextet {}.new_rep(6).cast()],
1428 -6 => vec![ColorSextet {}.dual().new_rep(6).cast()],
1429 8 => vec![ColorAdjoint {}.new_rep(8).cast()],
1430 _ => vec![],
1431 },
1432 Flow::Sink => match self.color {
1433 -3 => vec![ColorFundamental {}.new_rep(3).cast()],
1434 3 => vec![ColorFundamental {}.dual().new_rep(3).cast()],
1435 -6 => vec![ColorSextet {}.new_rep(6).cast()],
1436 6 => vec![ColorSextet {}.dual().new_rep(6).cast()],
1437 8 => vec![ColorAdjoint {}.new_rep(8).cast()],
1438 _ => vec![],
1439 },
1440 };
1441 Canonicalized::<IndexLess>::from_iter(reps).into_canonical()
1442 }
1443
1444 pub(crate) fn from_serializable_particle(particle: &SerializableParticle) -> Particle {
1445 Particle {
1446 pdg_code: particle.pdg_code,
1447 name: particle.name.clone(),
1448 antiname: particle.antiname.clone(),
1449 spin: particle.spin,
1450 color: particle.color,
1451 mass: ParameterName((&particle.mass).into()),
1452 width: ParameterName((&particle.width).into()),
1453 texname: particle.texname.clone(),
1454 antitexname: particle.antitexname.clone(),
1455 charge: particle.charge.0.clone(),
1456 ghost_number: particle.ghost_number,
1457 lepton_number: particle.lepton_number,
1458 y_charge: particle.y_charge.as_ref().map(|charge| charge.0.clone()),
1459 y_charge_right: particle
1460 .y_charge_right
1461 .as_ref()
1462 .map(|charge| charge.0.clone()),
1463 goldstone: particle.goldstone,
1464 chemical_potential: particle
1465 .chemical_potential
1466 .as_ref()
1467 .map(|chemical_potential| ParameterName(chemical_potential.into())),
1468 }
1469 }
1470}
1471
1472#[derive(Debug, Clone, Serialize, Deserialize)]
1473pub struct SerializableLorentzStructure {
1474 name: SmartString<LazyCompact>,
1475 spins: Vec<isize>,
1476 structure: SmartString<LazyCompact>,
1477}
1478
1479impl SerializableLorentzStructure {
1480 pub(crate) fn from_lorentz_structure(ls: &LorentzStructure) -> SerializableLorentzStructure {
1481 SerializableLorentzStructure {
1482 name: ls.name.clone(),
1483 spins: ls.spins.clone(),
1484 structure: ls.structure.to_canonical_string().into(),
1485 }
1486 }
1487}
1488
1489#[derive(Debug, Clone, PartialEq, Eq)]
1490pub struct LorentzStructure {
1491 pub name: SmartString<LazyCompact>,
1492 pub spins: Vec<isize>,
1493 pub structure: Atom,
1494}
1495
1496impl LorentzStructure {
1497 pub(crate) fn from_serializable_lorentz_structure(
1498 ls: &SerializableLorentzStructure,
1499 ) -> LorentzStructure {
1500 LorentzStructure {
1501 name: ls.name.clone(),
1502 spins: ls.spins.clone(),
1503 structure: utils::parse_python_expression(ls.structure.as_str()),
1504 }
1505 }
1506
1507 pub(crate) fn dod(&self) -> i32 {
1508 self.structure.all_dod()
1509 }
1510}
1511
1512#[derive(Debug, Clone, Serialize, Deserialize)]
1513pub struct SerializableParameter {
1514 name: SmartString<LazyCompact>,
1515 lhablock: Option<SmartString<LazyCompact>>,
1516 lhacode: Option<Vec<usize>>,
1517 nature: ParameterNature,
1518 parameter_type: ParameterType,
1519 value: Option<(F<f64>, F<f64>)>,
1520 expression: Option<SmartString<LazyCompact>>,
1521}
1522
1523impl SerializableParameter {
1524 pub(crate) fn from_parameter(param: &Parameter) -> SerializableParameter {
1525 SerializableParameter {
1526 name: param.name.namespaceless_string().into(),
1527 lhablock: param.lhablock.clone(),
1528 lhacode: param.lhacode.clone(),
1529 nature: param.nature.clone(),
1530 parameter_type: param.parameter_type.clone(),
1531 value: param.value.map(|value| (value.re, value.im)),
1532 expression: param
1533 .expression
1534 .as_ref()
1535 .map(|a| a.to_canonical_string())
1536 .map(SmartString::from),
1537 }
1538 }
1539}
1540
1541#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1542pub struct ParameterName(pub UFOSymbol);
1543
1544impl Deref for ParameterName {
1545 type Target = UFOSymbol;
1546
1547 fn deref(&self) -> &Self::Target {
1548 &self.0
1549 }
1550}
1551
1552#[derive(Debug, Clone)]
1553pub struct Parameter {
1554 pub name: UFOSymbol,
1555 pub lhablock: Option<SmartString<LazyCompact>>,
1556 pub lhacode: Option<Vec<usize>>,
1557 pub nature: ParameterNature,
1558 pub parameter_type: ParameterType,
1559 pub value: Option<Complex<F<f64>>>,
1560 pub expression: Option<Atom>,
1561}
1562
1563impl std::fmt::Display for Parameter {
1564 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1565 write!(f, "{}", self.name)
1566 }
1567}
1568
1569fn parameter_display_sort_key(parameter: &Parameter) -> (u8, String) {
1570 let nature_rank = match parameter.nature {
1571 ParameterNature::External => 0,
1572 ParameterNature::Internal => 1,
1573 };
1574 (nature_rank, parameter.name.to_string())
1575}
1576
1577impl PartialEq for Parameter {
1578 fn eq(&self, other: &Self) -> bool {
1579 self.name == other.name
1580 && self.nature == other.nature
1581 && self.parameter_type == other.parameter_type
1582 && self.expression == other.expression
1584 && self.lhablock == other.lhablock
1585 && self.lhacode == other.lhacode
1586 }
1587}
1588
1589impl Eq for Parameter {}
1590
1591impl Parameter {
1592 pub(crate) fn from_serializable_parameter(param: &SerializableParameter) -> Parameter {
1593 Parameter {
1594 name: (¶m.name).into(),
1595 lhablock: param.lhablock.clone(),
1596 lhacode: param.lhacode.clone(),
1597 nature: param.nature.clone(),
1598 parameter_type: param.parameter_type.clone(),
1599 value: param.value.map(|value| Complex::new(value.0, value.1)),
1600 expression: param
1601 .expression
1602 .as_ref()
1603 .map(|expr| utils::parse_python_expression(expr.as_str())),
1604 }
1605 }
1606
1607 pub(crate) fn rep_rule(&self) -> Option<[Atom; 2]> {
1608 let lhs = self.name.into();
1609 let rhs = self.expression.clone();
1610
1611 Some([lhs, rhs?])
1613 }
1614}
1615#[derive(Debug, Clone, Serialize, Deserialize)]
1616pub struct Order {
1617 pub name: SmartString<LazyCompact>,
1618 pub expansion_order: isize,
1619 pub hierarchy: isize,
1620}
1621
1622#[derive(Debug, Clone, Serialize, Deserialize)]
1623pub struct SerializableModel {
1624 pub name: SmartString<LazyCompact>,
1625 pub restriction: Option<SmartString<LazyCompact>>,
1626 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1627 covariant_cut_multiplets: BTreeMap<i64, [i64; 4]>,
1628 orders: Vec<Order>,
1629 parameters: Vec<SerializableParameter>,
1630 particles: Vec<SerializableParticle>,
1631 propagators: Vec<SerializablePropagator>,
1632 lorentz_structures: Vec<SerializableLorentzStructure>,
1633 couplings: Vec<SerializableCoupling>,
1634 vertex_rules: Vec<SerializableVertexRule>,
1635}
1636
1637impl SerializableModel {
1638 pub(crate) fn from_file(file_path: impl AsRef<Path>) -> Result<SerializableModel, Report> {
1639 SmartSerde::from_file(file_path, "model")
1640 }
1641 pub(crate) fn from_str(s: String, format: &str) -> Result<SerializableModel, Report> {
1642 SmartSerde::from_str(s, format, "model")
1643 }
1644
1645 pub(crate) fn from_model(model: &Model) -> SerializableModel {
1646 SerializableModel {
1647 name: model.name.clone(),
1648 restriction: model.restriction.clone(),
1649 covariant_cut_multiplets: model.covariant_cut_multiplets.clone(),
1650 orders: model
1651 .orders
1652 .iter()
1653 .map(|order| order.as_ref().clone())
1654 .collect(),
1655 parameters: model
1656 .parameters
1657 .values()
1658 .map(SerializableParameter::from_parameter)
1659 .collect(),
1660 particles: model
1661 .particles
1662 .iter()
1663 .map(|particle| SerializableParticle::from_particle(particle.0.as_ref()))
1664 .collect(),
1665 propagators: model
1666 .propagators
1667 .iter()
1668 .map(|propagator| SerializablePropagator::from_propagator(propagator.as_ref()))
1669 .collect(),
1670 lorentz_structures: model
1671 .lorentz_structures
1672 .iter()
1673 .map(|lorentz_structure| {
1674 SerializableLorentzStructure::from_lorentz_structure(lorentz_structure.as_ref())
1675 })
1676 .collect(),
1677 couplings: model
1678 .couplings
1679 .values()
1680 .map(SerializableCoupling::from_coupling)
1681 .collect(),
1682 vertex_rules: model
1683 .vertex_rules
1684 .iter()
1685 .map(|vertex_rule| SerializableVertexRule::from_vertex_rule(vertex_rule.0.as_ref()))
1686 .collect(),
1687 }
1688 }
1689}
1690
1691#[derive(Debug, Clone)]
1692pub struct Model {
1693 pub name: SmartString<LazyCompact>,
1694 pub restriction: Option<SmartString<LazyCompact>>,
1695 pub covariant_cut_multiplets: BTreeMap<i64, [i64; 4]>,
1698 pub orders: Vec<Arc<Order>>,
1699 pub parameters: BTreeMap<ParameterName, Parameter>,
1700 pub particles: Vec<ArcParticle>,
1701 pub propagators: Vec<Arc<Propagator>>,
1702 pub lorentz_structures: Vec<Arc<LorentzStructure>>,
1703 pub couplings: BTreeMap<CouplingName, Coupling>,
1704 pub vertex_rules: Vec<ArcVertexRule>,
1705 pub unresolved_particles: HashMap<SmartString<LazyCompact>, HashSet<ArcParticle>>,
1706 pub particle_set_to_vertex_rules_map: HashMap<Vec<ArcParticle>, Vec<ArcVertexRule>>,
1707 pub order_name_to_position: HashMap<SmartString<LazyCompact>, usize, RandomState>,
1708 pub lorentz_structure_name_to_position: HashMap<SmartString<LazyCompact>, usize, RandomState>,
1709 pub particle_name_to_position: HashMap<SmartString<LazyCompact>, usize, RandomState>,
1710 pub particle_pdg_to_position: HashMap<isize, usize, RandomState>,
1711 pub propagator_name_to_position: HashMap<SmartString<LazyCompact>, usize, RandomState>,
1712 pub vertex_rule_name_to_position: HashMap<SmartString<LazyCompact>, usize, RandomState>,
1713 pub particle_name_to_propagator_position: HashMap<SmartString<LazyCompact>, usize, RandomState>,
1714}
1715
1716impl Default for Model {
1717 fn default() -> Self {
1718 Model {
1719 name: SmartString::<LazyCompact>::from("ModelNotLoaded"),
1720 restriction: None,
1721 covariant_cut_multiplets: BTreeMap::new(),
1722 orders: vec![],
1723 parameters: BTreeMap::new(),
1724 particles: vec![],
1725 propagators: vec![],
1726 lorentz_structures: vec![],
1727 couplings: BTreeMap::new(),
1728 vertex_rules: vec![],
1729 order_name_to_position:
1730 HashMap::<SmartString<LazyCompact>, usize, RandomState>::default(),
1731 lorentz_structure_name_to_position: HashMap::<
1732 SmartString<LazyCompact>,
1733 usize,
1734 RandomState,
1735 >::default(),
1736 unresolved_particles: HashMap::new(),
1737 particle_set_to_vertex_rules_map: HashMap::new(),
1738 particle_name_to_position:
1739 HashMap::<SmartString<LazyCompact>, usize, RandomState>::default(),
1740 particle_pdg_to_position: HashMap::<isize, usize, RandomState>::default(),
1741 propagator_name_to_position:
1742 HashMap::<SmartString<LazyCompact>, usize, RandomState>::default(),
1743 vertex_rule_name_to_position:
1744 HashMap::<SmartString<LazyCompact>, usize, RandomState>::default(),
1745 particle_name_to_propagator_position: HashMap::<
1746 SmartString<LazyCompact>,
1747 usize,
1748 RandomState,
1749 >::default(),
1750 }
1751 }
1752}
1753impl Model {
1754 pub fn apply_param_card(
1755 &mut self,
1756 input_param_card: &InputParamCard<F<f64>>,
1757 ) -> Result<(), Report> {
1758 input_param_card.apply_to_model(self)?;
1759 Ok(())
1760 }
1761
1762 pub fn get_description(
1763 &self,
1764 show_particles: bool,
1765 show_parameters: bool,
1766 show_vertices: bool,
1767 show_couplings: bool,
1768 ) -> String {
1769 let name = self.name.clone().green().to_string();
1770 let restriction = match &self.restriction {
1771 Some(r) => r.clone().blue().to_string(),
1772 None => "None".into(),
1773 };
1774 let coupling_orders_value = if self.orders.is_empty() {
1775 "None".into()
1776 } else {
1777 self.orders
1778 .iter()
1779 .map(|order| {
1780 format!(
1781 "{} (expansion order: {}, hierarchy: {})",
1782 order.name.green(),
1783 order.expansion_order,
1784 order.hierarchy
1785 )
1786 })
1787 .collect::<Vec<_>>()
1788 .join(", ")
1789 };
1790
1791 let particle_list = if !show_particles {
1792 "[ hiddem ]".blue().to_string()
1793 } else {
1794 let mut particle_table = Builder::new();
1795
1796 particle_table.push_record([
1797 "Name".green().to_string(),
1798 "PDG code".normal().to_string(),
1799 "Mass name".blue().to_string(),
1800 "Mass value".normal().to_string(),
1801 "Width name".blue().to_string(),
1802 "Width value".normal().to_string(),
1803 ]);
1804 for p in &self.particles {
1805 let mass_value = if let Some(value) = self.get_parameter(p.mass.0.to_string()).value
1806 {
1807 format!("{:.6}", value.re)
1808 } else {
1809 "None".into()
1810 };
1811 let width_value =
1812 if let Some(value) = self.get_parameter(p.width.0.to_string()).value {
1813 format!("{:.6}", value.re)
1814 } else {
1815 "None".into()
1816 };
1817
1818 particle_table.push_record(&[
1819 p.name.to_string().green().to_string(),
1820 format!("{:+}", p.pdg_code).normal().to_string(),
1821 p.mass.0.to_string().blue().to_string(),
1822 mass_value.normal().to_string(),
1823 p.width.0.to_string().blue().to_string(),
1824 width_value.normal().to_string(),
1825 ]);
1826 }
1827 let mut particle_table_built = particle_table.build();
1828
1829 format!("\n{}\n", particle_table_built.with(Style::rounded()))
1830 };
1831
1832 let parameter_list = if !show_parameters {
1833 "[ hidden ]".blue().to_string()
1834 } else {
1835 let mut parameter_table = Builder::new();
1836 parameter_table.push_record([
1837 "Name".green().to_string(),
1838 "Nature".normal().to_string(),
1839 "Type".normal().to_string(),
1840 "Value".normal().to_string(),
1841 "Expression".to_string(),
1842 ]);
1843 let mut parameters = self.parameters.values().collect_vec();
1844 parameters.sort_by_key(|parameter| parameter_display_sort_key(parameter));
1845
1846 for param in parameters {
1847 parameter_table.push_record(&[
1848 param.name.to_string().green().to_string(),
1849 if param.nature == ParameterNature::External {
1850 format!("{:?}", param.nature).green().to_string()
1851 } else {
1852 format!("{:?}", param.nature).yellow().to_string()
1853 },
1854 if param.parameter_type == ParameterType::Real {
1855 format!("{:?}", param.parameter_type).green().to_string()
1856 } else {
1857 format!("{:?}", param.parameter_type).yellow().to_string()
1858 },
1859 if let Some(value) = param.value {
1860 format!("{:.6}", value.re)
1861 } else {
1862 "".into()
1863 },
1864 if let Some(expr) = ¶m.expression {
1865 expr.to_string()
1866 } else {
1867 "".into()
1868 },
1869 ]);
1870 }
1871 format!("\n{}\n", parameter_table.build().with(Style::rounded()))
1872 };
1873
1874 let vertex_list = if !show_vertices {
1875 "[ hidden ]".blue().to_string()
1876 } else {
1877 let mut vertex_table = Builder::new();
1878
1879 let max_n_particles = self
1880 .vertex_rules
1881 .iter()
1882 .map(|vr| vr.particles.len())
1883 .max()
1884 .unwrap_or(3);
1885 let mut header = vec!["Name".green().to_string(), "Particles".blue().to_string()];
1886 for _ in 0..max_n_particles - 1 {
1887 header.push("".to_string());
1888 }
1889 vertex_table.push_record(header);
1890 for vr in &self.vertex_rules {
1901 let mut record = vec![vr.name.to_string().green().to_string()];
1902 for p in &vr.particles {
1903 record.push(p.name.to_string().blue().to_string());
1904 }
1905 while record.len() < max_n_particles + 1 {
1907 record.push("".to_string());
1908 }
1909 vertex_table.push_record(&record);
1910 }
1911 let mut vertex_table_built = vertex_table.build();
1912
1913 vertex_table_built
1914 .with(
1915 Style::rounded()
1916 .remove_vertical()
1917 .verticals([(1, VerticalLine::inherit(Style::rounded()))]),
1919 )
1920 .with(Modify::new(Cell::new(0, 1)).with(Span::column(max_n_particles as isize)));
1921 format!("\n{}\n", vertex_table_built)
1922 };
1923
1924 let coupling_list = if !show_couplings {
1925 "[ hidden ]".blue().to_string()
1926 } else {
1927 let mut coupling_table = Builder::new();
1928 coupling_table.push_record([
1929 "Name".green().to_string(),
1930 "Orders".blue().to_string(),
1931 "Expression".normal().to_string(),
1932 ]);
1933 for c in self.couplings.values() {
1934 coupling_table.push_record(&[
1935 c.name.to_string().green().to_string(),
1936 c.orders
1937 .iter()
1938 .map(|(k, v)| format!("{}={}", k.blue(), v))
1939 .collect::<Vec<_>>()
1940 .join(" "),
1941 c.expression.to_string(),
1942 ]);
1943 }
1944 format!("\n{}\n", coupling_table.build().with(Style::rounded()),)
1945 };
1946
1947 #[rustfmt::skip]
1948 return format!("
1949{model_name_label:<30}: {name}
1950{restriction_label:<30}: {restriction}
1951{coupling_orders_label:<30}: {coupling_orders_value}
1952
1953{n_particles} particles : {particle_list}
1954{n_parameters} parameters :{parameter_list}
1955{n_vertices} vertices : {vertex_list}
1956{n_couplings} couplings : {coupling_list}
1957",
1958model_name_label = "Model name",
1959restriction_label = "Restriction",
1960coupling_orders_label = "Coupling orders",
1961n_particles = format!("{}", self.particles.len()).green(),
1962n_parameters = format!("{}", self.parameters.len()).green(),
1963n_vertices = format!("{}", self.vertex_rules.len()).green(),
1964n_couplings = format!("{}", self.couplings.len()).green(),
1965);
1966 }
1967
1968 pub fn generate_edge_style_template(
1970 &self,
1971 template_path: impl AsRef<std::path::Path>,
1972 ) -> Result<(), std::io::Error> {
1973 use std::fs;
1974
1975 if let Some(parent) = template_path.as_ref().parent() {
1977 fs::create_dir_all(parent)?;
1978 }
1979
1980 let mut edge_style_content = String::new();
1981 edge_style_content.push_str(
1982 r#"#import "physics-edge-style.typ": mi, palette, massive, massless, dashed, dotted, stroke-style, source-stroke, sink-stroke, fermion-flow, wave, coil, zigzag, default-edge, default-map, style as physics-style
1983
1984// Auto-generated particle styles from model (computed in Rust). The reusable
1985// physics drawing callbacks live in physics-edge-style.typ; this file only
1986// supplies the model-specific particle map and GammaLoop-compatible wrappers.
1987#let generated-map = (
1988"#,
1989 );
1990
1991 for particle in self.particles.iter() {
1993 edge_style_content.push_str(&format!(
1994 r#" "{}": {},
1995"#,
1996 particle.name,
1997 particle.generate_edge_typst_dict()
1998 ));
1999 }
2000
2001 edge_style_content.push_str(
2002 r#")
2003
2004#let map = generated-map
2005
2006// The model map extends GammaLoop's neutral particle aliases, while a caller-provided map
2007// is merged last. Keeping that merge in one wrapper also prevents a spread
2008// `map` argument from colliding with a separately named one.
2009#let style(map: (:), typst-fields: "plain", ..options) = physics-style(
2010 map: default-map + generated-map + map,
2011 typst-fields: typst-fields,
2012 ..options.named(),
2013)
2014
2015#let source-style(edge, typst-fields: "plain", ..options) = {
2016 let callbacks = style(typst-fields: typst-fields, ..options.named())
2017 (callbacks.source-style)(edge)
2018}
2019
2020#let sink-style(edge, typst-fields: "plain", ..options) = {
2021 let callbacks = style(typst-fields: typst-fields, ..options.named())
2022 (callbacks.sink-style)(edge)
2023}
2024
2025#let edge-label(edge, typst-fields: "plain", ..options) = {
2026 let callbacks = style(typst-fields: typst-fields, ..options.named())
2027 (callbacks.edge-label)(edge)
2028}
2029"#,
2030 );
2031
2032 fs::write(&template_path, edge_style_content)?;
2033 info!(
2034 "Generated dynamic edge styles for {} particles",
2035 self.particles.len()
2036 );
2037
2038 Ok(())
2039 }
2040
2041 pub fn simplify(
2042 &mut self,
2043 model_parameters: &mut InputParamCard<F<f64>>,
2044 ) -> Result<(), Report> {
2045 self.apply_param_card(model_parameters)?;
2046 self.recompute_dependents()?;
2047
2048 model_parameters.data = model_parameters
2050 .data
2051 .iter()
2052 .filter(|(_, v)| v.re != F::<f64>::from_f64(0.0) || v.im != F::<f64>::from_f64(0.0))
2053 .map(|(k, v)| (*k, *v))
2054 .collect::<HashMap<UFOSymbol, Complex<F<f64>>>>();
2055
2056 let mut removed_parameters = vec![];
2058 for param in self.parameters.values_mut() {
2059 if param.nature == ParameterNature::External
2060 && let Some(value) = param.value
2061 && value == Complex::new(F(0.0), F(0.0))
2062 {
2063 param.value = Some(Complex::new(F(0.0), F(0.0)));
2064 param.expression = Some(parse!("UFO::ZERO"));
2065 param.nature = ParameterNature::Internal;
2066 removed_parameters.push(param.name);
2067 }
2068 }
2069
2070 if !removed_parameters.is_empty() {
2071 info!(
2072 "The following {} external parameters were forced to zero by the restriction card:\n{}",
2073 format!("{}", removed_parameters.len()).green(),
2074 removed_parameters
2075 .iter()
2076 .map(|p| p.to_string())
2077 .collect::<Vec<_>>()
2078 .join(", ")
2079 .to_string()
2080 .blue(),
2081 );
2082 }
2083
2084 let mut retained_vertex_rules = vec![];
2086 let mut removed_vertex_rules = vec![];
2087 for v in self.vertex_rules.iter() {
2088 let mut new_vr = v.0.as_ref().clone();
2089 for row in new_vr.couplings.iter_mut() {
2090 *row = row
2091 .iter()
2092 .map(|c_opt| {
2093 if let Some(c) = c_opt {
2094 if let Some(cpl) = self.couplings.get(c) {
2095 if let Some(value) = cpl.value {
2096 if value == Complex::new(0.0, 0.0) {
2097 None
2098 } else {
2099 Some(*c)
2100 }
2101 } else {
2102 Some(*c)
2103 }
2104 } else {
2105 Some(*c)
2106 }
2107 } else {
2108 None
2109 }
2110 })
2111 .collect::<Vec<_>>();
2112 }
2113 if new_vr
2114 .couplings
2115 .iter()
2116 .all(|row| row.iter().all(|c| c.is_none()))
2117 {
2118 removed_vertex_rules.push(new_vr);
2119 } else {
2120 retained_vertex_rules.push(new_vr);
2121 }
2122 }
2123
2124 self.vertex_rules = retained_vertex_rules
2125 .into_iter()
2126 .map(|vr| ArcVertexRule(Arc::new(vr)))
2127 .collect::<Vec<_>>();
2128
2129 if !removed_vertex_rules.is_empty() {
2130 info!(
2131 "The following {} vertex rules were removed by the restriction card:\n{}",
2132 format!("{}", removed_vertex_rules.len()).green(),
2133 removed_vertex_rules
2134 .iter()
2135 .map(|v| format!(
2136 "{} -> ({})",
2137 v.name,
2138 v.particles
2139 .iter()
2140 .map(|p| p.name.to_string())
2141 .collect::<Vec<_>>()
2142 .join(", ")
2143 ))
2144 .collect::<Vec<_>>()
2145 .join(" | ")
2146 .to_string()
2147 .blue(),
2148 );
2149 }
2150
2151 let removed_couplings = self
2152 .couplings
2153 .iter()
2154 .filter(|(_, cpl)| cpl.value == Some(Complex::new(0.0, 0.0)))
2155 .map(|(name, _)| *name)
2156 .collect::<Vec<_>>();
2157
2158 self.couplings
2160 .retain(|_, cpl| cpl.value != Some(Complex::new(0.0, 0.0)));
2161
2162 if !removed_couplings.is_empty() {
2163 info!(
2164 "The following {} couplings were removed by the restriction card:\n{}",
2165 format!("{}", removed_couplings.len()).green(),
2166 removed_couplings
2167 .iter()
2168 .map(|c| c.to_string())
2169 .collect::<Vec<_>>()
2170 .join(", ")
2171 .to_string()
2172 .blue(),
2173 );
2174 }
2175
2176 *self = Self::from_serializable_model(self.to_serializable());
2179 self.validate_covariant_cut_multiplets()?;
2180
2181 Ok(())
2182 }
2183
2184 pub fn default_param_card(&self) -> InputParamCard<F<f64>> {
2185 InputParamCard::default_from_model(self)
2186 }
2187
2188 pub fn contains_symbol(&self, symbol: &UFOSymbol) -> bool {
2189 self.couplings.contains_key(&CouplingName(*symbol))
2190 || self.parameters.contains_key(&ParameterName(*symbol))
2191 }
2192 pub fn get_symbol_value(&self, symbol: UFOSymbol) -> Option<Complex<F<f64>>> {
2193 if let Some(cpl) = self.couplings.get(&CouplingName(symbol)) {
2194 return cpl.value.map(|a| a.map(F));
2195 }
2196 if let Some(param) = self.parameters.get(&ParameterName(symbol))
2197 && let Some(value) = param.value
2198 {
2199 return Some(value);
2200 }
2201 None
2202 }
2203
2204 fn parameters_to_empty_fns(&self) -> Vec<Replacement> {
2205 let mut reps = vec![];
2206 for n in self.couplings.keys() {
2207 reps.push(Replacement::new(
2208 Atom::from(n.0).to_pattern(),
2209 function!(n.0.0),
2210 ))
2211 }
2212 for n in self.parameters.keys() {
2213 reps.push(Replacement::new(
2214 Atom::from(n.0).to_pattern(),
2215 function!(n.0.0),
2216 ))
2217 }
2218
2219 reps
2220 }
2221
2222 pub fn recompute_dependents(&mut self) -> Result<()> {
2223 let mut fn_map = FunctionMap::new();
2224 let reps = self.parameters_to_empty_fns();
2225
2226 let mut expr = vec![];
2227 let mut new_values_len = 0;
2228 let mut dependent_parameter_names = vec![];
2229
2230 for (n, c) in &self.couplings {
2231 let key = n.0.0;
2232 expr.push(function!(key));
2233
2234 fn_map
2235 .add_function::<Symbol, Symbol>(
2236 key,
2237 Vec::new(),
2238 c.expression.replace_multiple(&reps),
2239 )
2240 .map_err(|e| eyre!(" {}", e))?;
2241 new_values_len += 1;
2242 }
2243
2244 let mut params = vec![];
2245 let mut param_values = vec![];
2246
2247 for (n, p) in &self.parameters {
2248 let key = function!(n.0.0);
2249 match p.nature {
2250 ParameterNature::External => {
2251 params.push(key);
2252 if let Some(value) = p.value {
2253 param_values.push(value);
2254 } else {
2255 return Err(eyre!("External parameter {} has no value", p.name));
2256 }
2257 }
2258 ParameterNature::Internal => {
2259 if p.name.is_zero() {
2260 continue;
2261 }
2262
2263 new_values_len += 1;
2264 expr.push(key.clone());
2265 dependent_parameter_names.push(*n);
2266 if let Some(body) = p.expression.clone() {
2267 fn_map
2268 .add_function::<Symbol, Symbol>(
2269 n.0.0,
2270 Vec::new(),
2271 body.replace_multiple(&reps),
2272 )
2273 .map_err(|e| eyre!(" {}", e))?;
2274 } else {
2275 let value = p
2276 .value
2277 .ok_or(eyre!("internal param {} has no expression or value", key))?;
2278 let value_rat = symbolica::domains::float::Complex::new(
2279 Fraction::<IntegerRing>::try_from(value.re.0).unwrap(),
2280 Fraction::<IntegerRing>::try_from(value.im.0).unwrap(),
2281 );
2282 fn_map
2283 .add_aliases([(key, Atom::num(value_rat))])
2284 .map_err(|e| eyre!(" {}", e))?;
2285 }
2286 }
2287 }
2288 }
2289
2290 fn_map
2291 .add_aliases([(
2292 Atom::var(Symbol::PI),
2293 Atom::num(symbolica::domains::float::Complex::new(
2294 Rational::try_from(0.0.pi()).unwrap(),
2295 Rational::zero(),
2296 )),
2297 )])
2298 .map_err(|e| eyre!(" {}", e))?;
2299
2300 let evaluator = AtomView::to_eval_tree_multiple(&expr, &fn_map, ¶ms)
2301 .unwrap()
2302 .linearize(
2303 &OptimizationSettings::new()
2304 .cpe_iterations(Some(1))
2305 .verbose(false),
2306 );
2307 let mut evaluator =
2308 evaluator.map_coeff(&|f| Complex::new(F(f.re.to_f64()), F(f.im.to_f64())));
2309
2310 let mut new_values = vec![Complex::new(F(0.0), F(0.0)); new_values_len];
2311 evaluator.evaluate(¶m_values, &mut new_values);
2312
2313 for (i, c) in self.couplings.values_mut().enumerate() {
2314 c.value = Some(new_values[i].map(|f| f.0));
2315 }
2316 for (i, name) in (self.couplings.len()..).zip(dependent_parameter_names) {
2317 if let Some(c) = self.parameters.get_mut(&name) {
2318 c.value = Some(new_values[i]);
2319 }
2320 }
2321
2322 Ok(())
2323 }
2324
2325 pub(crate) fn generate_params(&self) -> Vec<Atom> {
2326 let mut params = vec![];
2327
2328 for cpl in self.couplings.values().filter(|c| c.value.is_some()) {
2329 if cpl.value.is_some() {
2330 params.push(cpl.name.into());
2331 }
2332 }
2333 for param in self.parameters.values().filter(|p| p.value.is_some()) {
2334 if param.value.is_some() {
2335 let name = param.name.into();
2336 params.push(name);
2337 }
2338 }
2339
2340 params
2341 }
2342
2343 pub fn is_empty(&self) -> bool {
2344 self.name == "ModelNotLoaded" || self.particles.is_empty()
2345 }
2346
2347 pub fn apply_coupling_replacement_rules(&self, a: &Atom) -> Atom {
2348 let mut reps = vec![];
2349 for cpl in self.couplings.values() {
2350 let [a, b] = cpl.rep_rule();
2351 reps.push(Replacement::new(a.to_pattern(), b));
2352 }
2353
2354 a.replace_multiple(&reps)
2355 }
2356 pub fn apply_parameter_replacement_rules(&self, a: &Atom) -> Atom {
2357 let mut reps = vec![];
2358 for p in self.parameters.values() {
2359 let Some([a, b]) = p.rep_rule() else {
2360 continue;
2361 };
2362 reps.push(Replacement::new(a.to_pattern(), b));
2363 }
2364
2365 a.replace_multiple(&reps)
2366 }
2367
2368 pub fn export_coupling_replacement_rules(
2369 &self,
2370 export_root: &str,
2371 print_ops: PrintOptions,
2372 ) -> Result<(), Report> {
2373 let path = Path::new(export_root).join("sources").join("model");
2374
2375 if !path.exists() {
2376 fs::create_dir_all(&path)?;
2377 }
2378 let mut reps = Vec::new();
2379
2380 for cpl in self.couplings.values() {
2381 reps.push(cpl.rep_rule().map(|a| {
2382 format!(
2383 "{}",
2384 AtomPrinter::new_with_options(a.as_view(), print_ops.clone())
2385 )
2386 }));
2387 }
2388
2389 for para in self.parameters.values() {
2390 if let Some(rule) = para.rep_rule() {
2391 reps.push(rule.map(|a| {
2392 format!(
2393 "{}",
2394 AtomPrinter::new_with_options(a.as_view(), print_ops.clone())
2395 )
2396 }));
2397 }
2398 }
2399
2400 fs::write(
2401 path.join("model_replacements.json"),
2402 serde_json::to_string_pretty(&reps)?,
2403 )?;
2404
2405 Ok(())
2406 }
2407
2408 fn generate_particle_set_to_vertex_rules_map(&mut self) {
2409 let mut map = HashMap::new();
2410
2411 for vertex in self.vertex_rules.iter() {
2412 let mut vertex_particles = vertex.0.particles.clone();
2413 vertex_particles.sort();
2414 map.entry(vertex_particles)
2415 .and_modify(|l: &mut Vec<ArcVertexRule>| l.push(vertex.clone()))
2416 .or_insert(vec![vertex.clone()]);
2417 }
2418 self.particle_set_to_vertex_rules_map = map;
2419 }
2420
2421 fn generate_unresolved_particles(&mut self) {
2422 let mut map = HashMap::new();
2423
2424 for v in &self.vertex_rules {
2425 let mut set = HashSet::default();
2426 for p in &v.0.particles {
2427 if p.0.is_massless() {
2428 set.insert(p.clone());
2429 }
2430 }
2431 for (k, _) in v.0.coupling_orders(self) {
2432 let current_set = map.entry(k).or_insert(HashSet::<ArcParticle>::default());
2433
2434 set.iter().for_each(|d| {
2435 current_set.insert(d.clone());
2436 });
2437 }
2438 }
2439
2440 self.unresolved_particles = map;
2441 }
2442
2443 pub(crate) fn from_serializable_model(serializable_model: SerializableModel) -> Model {
2444 let _ = *ETS;
2448
2449 for name in serializable_model
2452 .parameters
2453 .iter()
2454 .map(|parameter| ¶meter.name)
2455 .chain(
2456 serializable_model
2457 .couplings
2458 .iter()
2459 .map(|coupling| &coupling.name),
2460 )
2461 {
2462 let _ = UFOSymbol::from(name);
2463 }
2464
2465 let mut model: Model = Model::default();
2466 model.name = serializable_model.name;
2467 model.restriction = serializable_model.restriction;
2468 model.covariant_cut_multiplets = serializable_model.covariant_cut_multiplets;
2469
2470 model.orders = serializable_model
2472 .orders
2473 .iter()
2474 .enumerate()
2475 .map(|(i_order, serializable_order)| {
2476 let order = Arc::new(Order {
2477 name: serializable_order.name.clone(),
2478 expansion_order: serializable_order.expansion_order,
2479 hierarchy: serializable_order.hierarchy,
2480 });
2481 model
2482 .order_name_to_position
2483 .insert(order.name.clone(), i_order);
2484 order
2485 })
2486 .collect();
2487
2488 model.parameters = serializable_model
2490 .parameters
2491 .iter()
2492 .map(|serializable_param| {
2493 let parameter = Parameter::from_serializable_parameter(serializable_param);
2494
2495 (ParameterName(parameter.name), parameter)
2496 })
2497 .collect();
2498
2499 let mut zero_parameters = [UFOSymbol::zero()].into_iter().collect::<HashSet<_>>();
2504 loop {
2505 let previous_len = zero_parameters.len();
2506 let replacements = zero_parameters
2507 .iter()
2508 .map(|name| Replacement::new(Atom::from(*name).to_pattern(), Atom::Zero))
2509 .collect::<Vec<_>>();
2510 zero_parameters.extend(
2511 model
2512 .parameters
2513 .values()
2514 .filter(|parameter| {
2515 parameter.nature == ParameterNature::Internal
2516 && parameter.expression.as_ref().is_some_and(|expression| {
2517 expression.replace_multiple(&replacements).is_zero()
2518 })
2519 })
2520 .map(|parameter| parameter.name),
2521 );
2522 if zero_parameters.len() == previous_len {
2523 break;
2524 }
2525 }
2526 let zero_mass_parameters = serializable_model
2527 .particles
2528 .iter()
2529 .map(|particle| UFOSymbol::from(&particle.mass))
2530 .filter(|mass| zero_parameters.contains(mass))
2531 .collect::<HashSet<_>>();
2532 let zero_mass_replacements = zero_mass_parameters
2533 .iter()
2534 .map(|mass| Replacement::new(Atom::from(*mass).to_pattern(), Atom::Zero))
2535 .collect::<Vec<_>>();
2536
2537 model.particles = serializable_model
2539 .particles
2540 .iter()
2541 .enumerate()
2542 .map(|(i_part, serializable_particle)| {
2543 let mut particle = Particle::from_serializable_particle(serializable_particle);
2544 if zero_mass_parameters.contains(&particle.mass.0) {
2545 particle.mass = ParameterName(UFOSymbol::zero());
2546 }
2547 if particle
2548 .chemical_potential
2549 .is_some_and(|mu| zero_parameters.contains(&mu.0))
2550 {
2551 particle.chemical_potential = Some(ParameterName(UFOSymbol::zero()));
2552 }
2553 let particle = Arc::new(particle);
2554 model
2555 .particle_name_to_position
2556 .insert(particle.name.clone(), i_part);
2557 model
2558 .particle_pdg_to_position
2559 .insert(particle.pdg_code, i_part);
2560 ArcParticle(particle)
2561 })
2562 .collect();
2563
2564 model.propagators = serializable_model
2567 .propagators
2568 .iter()
2569 .enumerate()
2570 .map(|(i_prop, serializable_propagator)| {
2571 let propagator = Arc::new(Propagator::from_serializable_propagator(
2572 &model,
2573 serializable_propagator,
2574 &zero_mass_replacements,
2575 ));
2576 model
2577 .propagator_name_to_position
2578 .insert(propagator.name.clone(), i_prop);
2579 propagator
2580 })
2581 .collect();
2582
2583 model.lorentz_structures = serializable_model
2585 .lorentz_structures
2586 .iter()
2587 .enumerate()
2588 .map(|(i_lor, serializable_lorentz_structure)| {
2589 let lorentz_structure =
2590 Arc::new(LorentzStructure::from_serializable_lorentz_structure(
2591 serializable_lorentz_structure,
2592 ));
2593 model
2594 .lorentz_structure_name_to_position
2595 .insert(lorentz_structure.name.clone(), i_lor);
2596 lorentz_structure
2597 })
2598 .collect();
2599
2600 model.couplings = serializable_model
2602 .couplings
2603 .iter()
2604 .map(|serializable_coupling| {
2605 let coupling = Coupling::from_serializable_coupling(serializable_coupling);
2606
2607 (CouplingName(coupling.name), coupling)
2608 })
2609 .collect();
2610
2611 model.vertex_rules = serializable_model
2613 .vertex_rules
2614 .iter()
2615 .enumerate()
2616 .map(|(i_vr, serializable_vertex_rule)| {
2617 let vertex_rule = ArcVertexRule(Arc::new(
2618 VertexRule::from_serializable_vertex_rule(&model, serializable_vertex_rule),
2619 ));
2620 model
2621 .vertex_rule_name_to_position
2622 .insert(vertex_rule.0.name.clone(), i_vr);
2623 vertex_rule
2624 })
2625 .collect();
2626
2627 model.particle_name_to_propagator_position =
2629 serializable_model.propagators.iter().enumerate().fold(
2630 HashMap::<SmartString<LazyCompact>, usize, RandomState>::default(),
2631 |mut map, (i_prop, serializable_propagator)| {
2632 map.insert(serializable_propagator.particle.clone(), i_prop);
2633 map
2634 },
2635 );
2636
2637 model.generate_unresolved_particles();
2638 model.generate_particle_set_to_vertex_rules_map();
2639
2640 model
2641 }
2642
2643 pub fn to_serializable(&self) -> SerializableModel {
2644 SerializableModel::from_model(self)
2645 }
2646
2647 pub fn from_file(file_path: impl AsRef<Path>) -> Result<Model, Report> {
2648 let mut model =
2649 SerializableModel::from_file(file_path).map(Model::from_serializable_model)?;
2650
2651 model.recompute_dependents()?;
2652 model.validate_covariant_cut_multiplets()?;
2653 Ok(model)
2654 }
2655
2656 pub fn from_str(s: String, format: &str) -> Result<Model, Report> {
2657 let mut model =
2658 SerializableModel::from_str(s, format).map(Model::from_serializable_model)?;
2659
2660 model.recompute_dependents()?;
2661 model.validate_covariant_cut_multiplets()?;
2662 Ok(model)
2663 }
2664
2665 pub(crate) fn validate_covariant_cut_multiplets(&self) -> Result<()> {
2666 let mut declared = HashSet::default();
2667 for (&physical, members) in &self.covariant_cut_multiplets {
2668 let particles = members
2669 .iter()
2670 .map(|pdg| {
2671 self.particle_pdg_to_position
2672 .get(&(*pdg as isize))
2673 .map(|position| &self.particles[*position])
2674 .ok_or_else(|| {
2675 eyre!("Covariant cut multiplet {physical} contains unknown PDG {pdg}")
2676 })
2677 })
2678 .collect::<Result<Vec<_>>>()?;
2679 let vector = particles[0];
2680 let denominator = self
2681 .get_propagator_for_particle(&vector.name)
2682 .denominator
2683 .clone();
2684 if members[0] != physical
2685 || !vector.is_vector()
2686 || !vector.is_massive()
2687 || vector.ghost_number != 0
2688 || particles[1].spin != 1
2689 || !particles[1].is_goldstone()
2690 || particles[1].ghost_number != 0
2691 || particles[2].spin != -1
2692 || particles[2].ghost_number != 1
2693 || particles[3].spin != -1
2694 || particles[3].ghost_number != -1
2695 || particles.iter().any(|p| {
2696 p.mass != vector.mass
2697 || p.charge != vector.charge
2698 || p.color != vector.color
2699 || self.get_propagator_for_particle(&p.name).denominator != denominator
2700 })
2701 || members.iter().any(|pdg| !declared.insert(*pdg))
2702 {
2703 return Err(eyre!(
2704 "Covariant cut multiplet {physical} must contain one massive vector, its Goldstone, ghost and antighost with the same symbolic pole mass, charge and color"
2705 ));
2706 }
2707 let conjugate_vector = vector.get_anti_particle(self).pdg_code as i64;
2708 let conjugate_members = particles
2709 .iter()
2710 .map(|p| p.get_anti_particle(self).pdg_code as i64)
2711 .sorted()
2712 .collect_vec();
2713 if self
2714 .covariant_cut_multiplets
2715 .get(&conjugate_vector)
2716 .map(|m| m.iter().copied().sorted().collect_vec())
2717 != Some(conjugate_members)
2718 {
2719 return Err(eyre!(
2720 "Covariant cut multiplet {physical} is not closed under particle conjugation"
2721 ));
2722 }
2723 }
2724 Ok(())
2725 }
2726
2727 pub(crate) fn covariant_cut_states(&self, states: &[Vec<i64>]) -> Result<Vec<Vec<i64>>> {
2731 self.validate_covariant_cut_multiplets()?;
2732 if !states
2733 .iter()
2734 .flatten()
2735 .any(|pdg| self.covariant_cut_multiplets.contains_key(pdg))
2736 {
2737 return Ok(states.to_vec());
2738 }
2739 Ok(states
2740 .iter()
2741 .flat_map(|state| {
2742 state
2743 .iter()
2744 .map(|pdg| {
2745 self.covariant_cut_multiplets
2746 .get(pdg)
2747 .map_or_else(|| vec![*pdg], |members| members.to_vec())
2748 })
2749 .multi_cartesian_product()
2750 .map(|members| members.into_iter().sorted().collect_vec())
2751 })
2752 .sorted()
2753 .dedup()
2754 .collect())
2755 }
2756
2757 #[inline]
2758 pub(crate) fn get_propagator_for_particle<S: AsRef<str>>(&self, name: S) -> Arc<Propagator> {
2759 if let Some(position) = self.particle_name_to_propagator_position.get(name.as_ref()) {
2760 self.propagators[*position].clone()
2761 } else {
2762 panic!(
2763 "Propagator for particle '{}' not found in model '{}'. Valid entries are:\n{}",
2764 name.as_ref(),
2765 self.name,
2766 self.particle_name_to_propagator_position.keys().join(", ")
2767 );
2768 }
2769 }
2770
2771 #[inline]
2772 pub fn get_particle<S: AsRef<str>>(&self, name: S) -> ArcParticle {
2773 if let Some(position) = self.particle_name_to_position.get(name.as_ref()) {
2774 self.particles[*position].clone()
2775 } else {
2776 panic!(
2777 "Particle '{}' not found in model '{}'. Valid entries are:\n{}",
2778 name.as_ref(),
2779 self.name,
2780 self.particle_name_to_position.keys().join(", ")
2781 );
2782 }
2783 }
2784
2785 #[inline]
2786 pub fn try_get_particle<S: AsRef<str>>(&self, name: S) -> Result<ArcParticle> {
2787 if let Some(position) = self.particle_name_to_position.get(name.as_ref()) {
2788 Ok(self.particles[*position].clone())
2789 } else {
2790 Err(eyre!(
2791 "Particle '{}' not found in model '{}'. Valid entries are:\n{}",
2792 name.as_ref(),
2793 self.name,
2794 self.particle_name_to_position.keys().join(", ")
2795 ))
2796 }
2797 }
2798 #[inline]
2799 pub(crate) fn get_particle_from_pdg(&self, pdg: isize) -> ArcParticle {
2800 if let Some(position) = self.particle_pdg_to_position.get(&pdg) {
2801 self.particles[*position].clone()
2802 } else {
2803 panic!(
2804 "Particle with PDG {} not found in model '{}'.",
2805 pdg, self.name
2806 );
2807 }
2808 }
2809
2810 #[inline]
2811 pub(crate) fn try_get_particle_from_pdg(&self, pdg: isize) -> Result<ArcParticle> {
2812 if let Some(position) = self.particle_pdg_to_position.get(&pdg) {
2813 Ok(self.particles[*position].clone())
2814 } else {
2815 Err(eyre!(
2816 "Particle with PDG {} not found in model '{}'.",
2817 pdg,
2818 self.name
2819 ))
2820 }
2821 }
2822
2823 #[inline]
2824 pub fn get_propagator<S: AsRef<str>>(&self, name: S) -> ArcPropagator {
2825 if let Some(position) = self.propagator_name_to_position.get(name.as_ref()) {
2826 ArcPropagator(self.propagators[*position].clone())
2827 } else {
2828 panic!(
2829 "Propagator '{}' not found in model '{}'.",
2830 name.as_ref(),
2831 self.name
2832 );
2833 }
2834 }
2835
2836 #[inline]
2837 pub fn get_parameter_opt<S: AsRef<str>>(&self, name: S) -> Option<&Parameter> {
2838 if let Some(position) = self
2839 .parameters
2840 .get(&ParameterName(UFOSymbol::from(name.as_ref())))
2841 {
2842 Some(position)
2843 } else {
2844 None
2845 }
2846 }
2847
2848 #[inline]
2849 pub fn get_parameter<S: AsRef<str>>(&self, name: S) -> &Parameter {
2850 if let Some(position) = self
2851 .parameters
2852 .get(&ParameterName(UFOSymbol::from(name.as_ref())))
2853 {
2854 position
2855 } else {
2856 panic!(
2857 "Parameter '{}' not found in model '{}'.",
2858 name.as_ref(),
2859 self.name
2860 );
2861 }
2862 }
2863
2864 #[inline]
2865 pub fn get_parameter_mut_opt<S: AsRef<str>>(&mut self, name: S) -> Option<&mut Parameter> {
2866 if let Some(position) = self
2867 .parameters
2868 .get_mut(&ParameterName(UFOSymbol::from(name.as_ref())))
2869 {
2870 Some(position)
2871 } else {
2872 None
2873 }
2874 }
2875
2876 #[inline]
2877 pub fn get_parameter_mut<S: AsRef<str>>(&mut self, name: S) -> Result<&mut Parameter> {
2878 if let Some(position) = self
2879 .parameters
2880 .get_mut(&ParameterName(UFOSymbol::from(name.as_ref())))
2881 {
2882 Ok(position)
2883 } else {
2884 Err(eyre!(
2885 "Parameter '{}' not found in model '{}'.",
2886 name.as_ref(),
2887 self.name
2888 ))
2889 }
2890 }
2891
2892 #[inline]
2893 pub fn get_order<S: AsRef<str>>(&self, name: S) -> Arc<Order> {
2894 if let Some(position) = self.order_name_to_position.get(name.as_ref()) {
2895 self.orders[*position].clone()
2896 } else {
2897 panic!(
2898 "Coupling order '{}' not found in model '{}'.",
2899 name.as_ref(),
2900 self.name
2901 );
2902 }
2903 }
2904 #[inline]
2905 pub fn get_lorentz_structure<S: AsRef<str>>(&self, name: S) -> Arc<LorentzStructure> {
2906 if let Some(position) = self.lorentz_structure_name_to_position.get(name.as_ref()) {
2907 self.lorentz_structures[*position].clone()
2908 } else {
2909 panic!(
2910 "Lorentz structure '{}' not found in model '{}'.",
2911 name.as_ref(),
2912 self.name
2913 );
2914 }
2915 }
2916 #[inline]
2917 pub fn get_coupling<S: AsRef<str>>(&self, name: S) -> &Coupling {
2918 if let Some(coupling) = self
2919 .couplings
2920 .get(&CouplingName(UFOSymbol::from(name.as_ref())))
2921 {
2922 coupling
2923 } else {
2924 panic!(
2925 "Coupling '{}' not found in model '{}'.",
2926 name.as_ref(),
2927 self.name
2928 );
2929 }
2930 }
2931 #[inline]
2932 pub fn get_vertex_rule<S: AsRef<str>>(&self, name: S) -> ArcVertexRule {
2933 if let Some(position) = self.vertex_rule_name_to_position.get(name.as_ref()) {
2934 self.vertex_rules[*position].clone()
2935 } else {
2936 panic!(
2937 "Vertex rule '{}' not found in model '{}'.",
2938 name.as_ref(),
2939 self.name
2940 );
2941 }
2942 }
2943}
2944
2945#[cfg(test)]
2946#[path = "test_polarization_sums.rs"]
2947mod test_polarization_sums;
2948
2949#[cfg(test)]
2950mod tests {
2951 use std::sync::Arc;
2952
2953 use spenso::algebra::complex::Complex;
2954 use symbolica::atom::{Atom, AtomCore};
2955
2956 use crate::{
2957 model::{
2958 ArcPropagator, ArcVertexRule, Parameter, ParameterName, ParameterNature, ParameterType,
2959 },
2960 momentum::{Helicity, ThreeMomentum},
2961 utils::{F, load_generic_model, parse_python_expression},
2962 };
2963
2964 use super::{ArcParticle, Model, UFOSymbol, parameter_display_sort_key};
2965
2966 #[test]
2967 fn restricted_zero_masses_match_literal_zero_model() -> eyre::Result<()> {
2968 let mut full = load_generic_model("sm");
2969 for (name, value) in [("MU", 0.0022), ("MD", 0.0047), ("MS", 0.095), ("MC", 1.27)] {
2970 full.get_parameter_mut(name)?.value = Some(Complex::new(F(value), F(0.0)));
2971 }
2972 let mut literal_zero = full.to_serializable();
2973 for particle in &mut literal_zero.particles {
2974 if matches!(particle.mass.as_str(), "MU" | "MD" | "MS" | "MC") {
2975 particle.mass = "ZERO".into();
2976 }
2977 }
2978 for propagator in &mut literal_zero.propagators {
2981 let source = match propagator.particle.as_str() {
2982 "u" | "d" | "s" | "c" => full.get_propagator_for_particle("ve"),
2983 "u~" | "d~" | "s~" | "c~" => full.get_propagator_for_particle("ve~"),
2984 _ => continue,
2985 };
2986 let source = super::SerializablePropagator::from_propagator(&source);
2987 propagator.numerator = source.numerator;
2988 propagator.denominator = source.denominator;
2989 }
2990 let mut literal_zero = Model::from_serializable_model(literal_zero);
2991 let mut card = full.default_param_card();
2992 for name in ["MU", "MD", "MS", "MC"] {
2993 card.insert(UFOSymbol::from(name), Complex::new(F(0.0), F(0.0)));
2994 }
2995 literal_zero.simplify(&mut card.clone())?;
2996 full.simplify(&mut card)?;
2997 let expected = serde_json::to_value(literal_zero.to_serializable())?;
2998 assert_eq!(serde_json::to_value(full.to_serializable())?, expected);
2999
3000 for name in ["u", "u~", "d", "d~", "s", "s~", "c", "c~"] {
3001 let particle = full.get_particle(name);
3002 assert!(particle.is_massless(), "{name}");
3003 assert!(
3004 full.unresolved_particles["QCD"].contains(&particle),
3005 "{name}"
3006 );
3007 }
3008 for name in ["MU", "MD", "MS", "MC"] {
3009 assert_eq!(full.get_parameter(name).nature, ParameterNature::Internal);
3010 assert!(!card.contains_key(&UFOSymbol::from(name)));
3011 }
3012 for particle in &full.particles {
3015 if let Some(mu) = particle.chemical_potential {
3016 assert_eq!(mu.0, UFOSymbol::zero(), "{}", particle.name);
3017 }
3018 }
3019
3020 full.simplify(&mut card)?;
3022 assert_eq!(serde_json::to_value(full.to_serializable())?, expected);
3023 let reloaded = Model::from_str(serde_json::to_string(&full.to_serializable())?, "json")?;
3024 assert_eq!(serde_json::to_value(reloaded.to_serializable())?, expected);
3025 for model in [&full, &reloaded] {
3026 for particle in model
3027 .propagators
3028 .iter()
3029 .map(|propagator| &propagator.particle)
3030 .chain(
3031 model
3032 .vertex_rules
3033 .iter()
3034 .flat_map(|vertex| &vertex.particles),
3035 )
3036 .chain(model.unresolved_particles.values().flatten())
3037 .chain(model.particle_set_to_vertex_rules_map.keys().flatten())
3038 {
3039 assert!(Arc::ptr_eq(
3040 &particle.0,
3041 &model.get_particle(&particle.name).0,
3042 ));
3043 }
3044 for vertices in model.particle_set_to_vertex_rules_map.values() {
3045 for vertex in vertices {
3046 assert!(Arc::ptr_eq(
3047 &vertex.0,
3048 &model.get_vertex_rule(&vertex.name).0,
3049 ));
3050 }
3051 }
3052 }
3053 Ok(())
3054 }
3055
3056 #[test]
3057 fn imported_internal_zero_masses_are_structurally_massless() -> eyre::Result<()> {
3058 let mut full = load_generic_model("sm");
3059 full.get_parameter_mut("MC")?.value = Some(Complex::new_re(F(0.0)));
3061 for expression in ["0", "0.0", "UFO::ZERO"] {
3062 let mut serialized = full.to_serializable();
3063 for name in ["MS", "muQ", "muLe"] {
3064 let parameter = serialized
3065 .parameters
3066 .iter_mut()
3067 .find(|parameter| parameter.name == name)
3068 .unwrap();
3069 parameter.nature = ParameterNature::Internal;
3070 parameter.expression = Some(expression.into());
3071 parameter.value = None;
3072 }
3073 serialized
3075 .parameters
3076 .iter_mut()
3077 .find(|parameter| parameter.name == "mue")
3078 .unwrap()
3079 .expression = Some("UFO::muve-UFO::muQ".into());
3080 let imported = Model::from_str(serde_json::to_string(&serialized)?, "json")?;
3081 for name in ["e-", "e+", "ve", "ve~"] {
3082 assert_eq!(
3083 imported.get_particle(name).chemical_potential,
3084 Some(ParameterName(UFOSymbol::zero())),
3085 "{name}: {expression}"
3086 );
3087 }
3088 for name in ["mu-", "mu+", "u", "u~"] {
3090 assert_eq!(
3091 imported.get_particle(name).chemical_potential,
3092 full.get_particle(name).chemical_potential,
3093 "{name}: {expression}"
3094 );
3095 }
3096 for (name, control) in [("s", "ve"), ("s~", "ve~")] {
3097 assert!(imported.get_particle(name).is_massless(), "{expression}");
3098 let propagator = imported.get_propagator_for_particle(name);
3099 let expected = full.get_propagator_for_particle(control);
3100 assert_eq!(propagator.numerator, expected.numerator, "{expression}");
3101 assert_eq!(propagator.denominator, expected.denominator, "{expression}");
3102 assert!(
3103 imported.unresolved_particles["QCD"].contains(&imported.get_particle(name))
3104 );
3105 }
3106 assert!(!imported.get_particle("c").is_massless());
3108 }
3109 Ok(())
3110 }
3111
3112 #[test]
3113 fn external_zero_and_nonzero_restricted_masses_remain_symbolic() -> eyre::Result<()> {
3114 let mut model = load_generic_model("sm");
3115 let original = model.get_propagator_for_particle("s");
3116 let mut card = model.default_param_card();
3117 for value in [0.0, 0.095, 0.0] {
3118 card.insert(UFOSymbol::from("MS"), Complex::new(F(value), F(0.0)));
3119 card.insert(UFOSymbol::from("muLe"), Complex::new(F(value), F(0.0)));
3120 model.apply_param_card(&card)?;
3121 let reloaded =
3122 Model::from_str(serde_json::to_string(&model.to_serializable())?, "json")?;
3123 for model in [&model, &reloaded] {
3124 for name in ["s", "s~"] {
3125 assert_eq!(model.get_particle(name).mass.0, UFOSymbol::from("MS"));
3126 assert!(!model.unresolved_particles["QCD"].contains(&model.get_particle(name)));
3127 }
3128 assert_eq!(model.get_parameter("MS").nature, ParameterNature::External);
3129 for (name, mu, sign) in [("e-", "mue", 1.0), ("e+", "minus_mue", -1.0)] {
3130 assert_eq!(
3131 model.get_particle(name).chemical_potential,
3132 Some(ParameterName(UFOSymbol::from(mu)))
3133 );
3134 assert_eq!(
3135 model.get_parameter(mu).value,
3136 Some(Complex::new_re(F(sign * value)))
3137 );
3138 }
3139 assert_eq!(
3140 model.get_propagator_for_particle("s").numerator,
3141 original.numerator
3142 );
3143 assert_eq!(
3144 model.get_propagator_for_particle("s").denominator,
3145 original.denominator
3146 );
3147 }
3148 }
3149 card.insert(UFOSymbol::from("MS"), Complex::new(F(1.0e-30), F(0.0)));
3150 model.simplify(&mut card)?;
3151 assert_eq!(model.get_particle("s").mass.0, UFOSymbol::from("MS"));
3152 assert_eq!(model.get_parameter("MS").nature, ParameterNature::External);
3153 assert!(card.contains_key(&UFOSymbol::from("MS")));
3154 assert_eq!(
3155 model.get_propagator_for_particle("s").numerator,
3156 original.numerator
3157 );
3158 assert_eq!(
3159 model.get_propagator_for_particle("s").denominator,
3160 original.denominator
3161 );
3162 Ok(())
3163 }
3164
3165 #[test]
3166 fn test_encode_decode_arc_particle() {
3167 let model = load_generic_model("scalars");
3168 let particle = model.get_particle("scalar_0");
3169 let particle_encoded =
3170 bincode::encode_to_vec(&particle, bincode::config::standard()).unwrap();
3171
3172 let particle_decoded: ArcParticle = bincode::decode_from_slice_with_context(
3173 &particle_encoded,
3174 bincode::config::standard(),
3175 model,
3176 )
3177 .unwrap()
3178 .0;
3179
3180 assert_eq!(particle, particle_decoded);
3181 }
3182
3183 #[test]
3184 fn test_encode_decode_vertex_rule() {
3185 let model = load_generic_model("sm");
3186 let vertex = model.get_vertex_rule("V_141");
3187 let vertex_encoded = bincode::encode_to_vec(&vertex, bincode::config::standard()).unwrap();
3188
3189 let vertex_decoded: ArcVertexRule = bincode::decode_from_slice_with_context(
3190 &vertex_encoded,
3191 bincode::config::standard(),
3192 model,
3193 )
3194 .unwrap()
3195 .0;
3196 assert_eq!(vertex, vertex_decoded);
3197 }
3198
3199 #[test]
3200 fn autogenerated_model_dod_counts_raw_ufo_momenta() {
3201 let model = load_generic_model("sm");
3202
3203 assert_eq!(model.get_vertex_rule("V_35").dod, 1);
3204 assert_eq!(model.get_vertex_rule("V_36").dod, 1);
3205 assert_eq!(model.get_propagator_for_particle("ghG").dod, -2);
3206 assert_eq!(model.get_propagator_for_particle("g").dod, -2);
3207 assert_eq!(model.get_propagator_for_particle("d").dod, -1);
3208 }
3209
3210 #[test]
3211 fn sm_lorentz_structures_use_the_scalar_and_vector_axial_basis() {
3212 let model = load_generic_model("sm");
3213 let identity =
3214 parse_python_expression("UFO::{}::Identity(UFO::{}::idx(1,1),UFO::{}::idx(1,2))");
3215 let gamma5 =
3216 parse_python_expression("UFO::{}::Gamma5(UFO::{}::idx(1,1),UFO::{}::idx(1,2))");
3217 let vector = parse_python_expression(
3218 "UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,1),UFO::{}::idx(1,2))",
3219 );
3220 let axial = parse_python_expression(
3221 "UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,1),UFO::{}::dummy(1))*UFO::{}::Gamma5(UFO::{}::dummy(1),UFO::{}::idx(1,2))",
3222 );
3223
3224 for (even, odd, weights) in [
3228 (
3229 identity.clone(),
3230 gamma5.clone(),
3231 &[
3232 ("FFS1", 0, 1),
3233 ("FFS2", -1, 1),
3234 ("FFS3", 1, 0),
3235 ("FFS4", 1, 1),
3236 ][..],
3237 ),
3238 (
3239 vector,
3240 axial,
3241 &[
3242 ("FFV1", 1, 1),
3243 ("FFV2", 1, 0),
3244 ("FFV3", 1, -2),
3245 ("FFV4", 1, 2),
3246 ("FFV5", 1, 4),
3247 ][..],
3248 ),
3249 ] {
3250 for &(name, left, right) in weights {
3251 let expected =
3252 &even * Atom::num((left + right, 2)) + &odd * Atom::num((right - left, 2));
3253 assert_eq!(
3254 model.get_lorentz_structure(name).structure,
3255 expected,
3256 "{name}"
3257 );
3258 }
3259 }
3260
3261 let right = model.get_lorentz_structure("FFS1");
3264 let left = model.get_lorentz_structure("FFS3");
3265 for (scalar, pseudoscalar) in [(1, 0), (0, 1)] {
3268 for (combination, expected) in [
3269 (&right.structure + &left.structure, scalar),
3270 (&right.structure - &left.structure, pseudoscalar),
3271 ] {
3272 assert_eq!(
3273 combination
3274 .replace(identity.to_pattern())
3275 .with(Atom::num(scalar))
3276 .replace(gamma5.to_pattern())
3277 .with(Atom::num(pseudoscalar)),
3278 Atom::num(expected)
3279 );
3280 }
3281 }
3282 assert_eq!(model.get_lorentz_structure("FFS4").structure, identity);
3283 for lorentz in &model.lorentz_structures {
3284 for projector in ["ProjM", "ProjP"] {
3285 let projector = parse_python_expression(&format!("UFO::{{}}::{projector}"))
3286 .get_symbol()
3287 .unwrap();
3288 assert!(
3289 !lorentz.structure.contains_symbol(projector),
3290 "{} retains a chiral projector: {}",
3291 lorentz.name,
3292 lorentz.structure
3293 );
3294 }
3295 }
3296 }
3297
3298 #[test]
3299 fn test_pol_limit_vector() {
3300 let vals = [0., 3., 4.].into_iter().map(F).collect::<Vec<_>>();
3301 let mom = ThreeMomentum::new(vals[0], vals[1], vals[2]).into_on_shell_four_momentum(None); let hel = Helicity::MINUS;
3304 println!("mom:{mom}");
3305 println!("hel{hel}");
3306 println!("{}", mom.eps_pol(hel));
3307 println!("{}", mom.eps_pol(hel).bar());
3308 let hel = Helicity::PLUS;
3312 println!("mom:{mom}");
3313 println!("hel{hel}");
3314 println!("{}", mom.eps_pol(hel));
3315 println!("{}", mom.eps_pol(hel).bar());
3316 let hel = Helicity::MINUS;
3317
3318 println!("hel{hel}");
3319 println!("{}", mom.eps_pol(hel));
3320 println!("{}", mom.eps_pol(hel).bar());
3321
3322 let vals = [0., 0., -1.].into_iter().map(F).collect::<Vec<_>>();
3323 let mom = ThreeMomentum::new(vals[0], vals[1], vals[2]).into_on_shell_four_momentum(None);
3324
3325 let hel = Helicity::PLUS;
3326
3327 println!("mom:{mom}");
3328 println!("hel{hel}");
3329
3330 println!("{}", mom.eps_pol(hel));
3331 println!("{}", mom.eps_pol(hel).bar());
3332
3333 let hel = Helicity::MINUS;
3334
3335 println!("hel{hel}");
3336 println!("{}", mom.eps_pol(hel));
3337 println!("{}", mom.eps_pol(hel).bar());
3338 }
3339
3340 #[test]
3341 fn display_orders_external_parameters_first() {
3342 let mut model = Model::default();
3343 let external = Parameter {
3344 name: UFOSymbol::from("zeta"),
3345 lhablock: None,
3346 lhacode: None,
3347 nature: ParameterNature::External,
3348 parameter_type: ParameterType::Real,
3349 value: None,
3350 expression: None,
3351 };
3352 let internal = Parameter {
3353 name: UFOSymbol::from("alpha"),
3354 lhablock: None,
3355 lhacode: None,
3356 nature: ParameterNature::Internal,
3357 parameter_type: ParameterType::Real,
3358 value: None,
3359 expression: None,
3360 };
3361 model
3362 .parameters
3363 .insert(ParameterName(external.name), external.clone());
3364 model
3365 .parameters
3366 .insert(ParameterName(internal.name), internal.clone());
3367
3368 assert!(parameter_display_sort_key(&external) < parameter_display_sort_key(&internal));
3369
3370 let description = model.get_description(false, true, false, false);
3371 let external_index = description.find("zeta").unwrap();
3372 let internal_index = description.find("alpha").unwrap();
3373 assert!(external_index < internal_index);
3374 }
3375
3376 #[test]
3377 fn test_encode_decode_arc_propagator() {
3378 let model = load_generic_model("sm");
3379 let propagator = model.get_propagator("t_propFeynman");
3380 let propagator_encoded =
3381 bincode::encode_to_vec(&propagator, bincode::config::standard()).unwrap();
3382
3383 let propagator_decoded: ArcPropagator = bincode::decode_from_slice_with_context(
3384 &propagator_encoded,
3385 bincode::config::standard(),
3386 model,
3387 )
3388 .unwrap()
3389 .0;
3390
3391 assert_eq!(propagator.name, propagator_decoded.name);
3392 }
3393}