Skip to main content

gammalooprs/model/
mod.rs

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;
18// use linnet::half_edge::drawing::Decoration;
19use 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
34// use log::{info, trace};
35use 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    /// Create empty card
130    pub fn new() -> Self {
131        Self {
132            data: HashMap::new(),
133        }
134    }
135
136    /// Insert a parameter
137    pub fn insert(&mut self, sym: UFOSymbol, value: Complex<T>) -> Option<Complex<T>> {
138        self.data.insert(sym, value)
139    }
140
141    /// Get immutable reference
142    pub fn get(&self, sym: &UFOSymbol) -> Option<&Complex<T>> {
143        self.data.get(sym)
144    }
145
146    /// Get mutable reference
147    pub fn get_mut(&mut self, sym: &UFOSymbol) -> Option<&mut Complex<T>> {
148        self.data.get_mut(sym)
149    }
150
151    /// Remove a parameter
152    pub fn remove(&mut self, sym: &UFOSymbol) -> Option<Complex<T>> {
153        self.data.remove(sym)
154    }
155
156    /// Iterate over all parameters
157    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
228/// Let it behave like a map if desired
229impl<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    // assert_eq!(UFOSymbol::zero().namespaceless_string(), "ZERO");
281}
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    // #[allow(clippy::complexity)]
685    // pub fn get_coupling_orders(&self) -> Vec<Vec<Option<CouplingName>>> {
686    //     self.couplings
687    //         .iter()
688    //         .map(|row| {
689    //             row.iter()
690    //                 .map(|co| co.clone().map(|c| c.clone()))
691    //                 .collect::<Vec<_>>()
692    //         })
693    //         .collect::<Vec<_>>()
694    // }
695}
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 = normalise_complex(&self.expression);
881        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                    // Q3 is a spatial vector embedded in Minkowski space, so
1011                    // Q3·Q3 = -|q⃗|² and q·n = q₀² - Q3·Q3.
1012                    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 decoration(&self) -> Decoration {
1108    //     match self.spin {
1109    //         0 => Decoration::Dashed,
1110    //         1 => Decoration::None,
1111    //         2 => Decoration::Arrow,
1112    //         3 => {
1113    //             if self.pdg_code.abs() == 9 || self.pdg_code.abs() == 21 {
1114    //                 Decoration::Coil
1115    //             } else {
1116    //                 Decoration::Wave
1117    //             }
1118    //         }
1119    //         _ => Decoration::None,
1120    //     }
1121    // }
1122
1123    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            // The checks below are too slow
1182            // if self.name != other.name {
1183            //     panic!(
1184            //         "Particle with same pdg code but different names: {} and {}",
1185            //         self.name, other.name
1186            //     );
1187            // }
1188            // if self.spin != other.spin {
1189            //     panic!(
1190            //         "Particle with same pdg code but different spins: {} and {}",
1191            //         self.spin, other.spin
1192            //     );
1193            // }
1194            // if self.color != other.color {
1195            //     panic!(
1196            //         "Particle with same pdg code but different colors: {} and {}",
1197            //         self.color, other.color
1198            //     );
1199            // }
1200            // if self.mass != other.mass {
1201            //     panic!(
1202            //         "Particle with same pdg code but different masses: {} and {}",
1203            //         self.mass, other.mass
1204            //     );
1205            // }
1206            // if self.width != other.width {
1207            //     panic!(
1208            //         "Particle with same pdg code but different widths: {} and {}",
1209            //         self.width, other.width
1210            //     );
1211            // }
1212            // if self.texname != other.texname {
1213            //     panic!(
1214            //         "Particle with same pdg code but different texnames: {} and {}",
1215            //         self.texname, other.texname
1216            //     );
1217            // }
1218            // if self.antitexname != other.antitexname {
1219            //     panic!(
1220            //         "Particle with same pdg code but different antitexnames: {} and {}",
1221            //         self.antitexname, other.antitexname
1222            //     );
1223            // }
1224            // if self.charge != other.charge {
1225            //     panic!(
1226            //         "Particle with same pdg code but different charges: {} and {}",
1227            //         self.charge, other.charge
1228            //     );
1229            // }
1230            // if self.ghost_number != other.ghost_number {
1231            //     panic!(
1232            //         "Particle with same pdg code but different ghost_numbers: {} and {}",
1233            //         self.ghost_number, other.ghost_number
1234            //     );
1235            // }
1236            // if self.lepton_number != other.lepton_number {
1237            //     panic!(
1238            //         "Particle with same pdg code but different lepton_numbers: {} and {}",
1239            //         self.lepton_number, other.lepton_number
1240            //     );
1241            // }
1242            // if self.y_charge != other.y_charge {
1243            //     panic!(
1244            //         "Particle with same pdg code but different y_charges: {} and {}",
1245            //         self.y_charge, other.y_charge
1246            //     );
1247            // }
1248            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    /// Generate edge styles for visualization based on particle properties
1353    pub fn generate_edge_typst_dict(&self) -> String {
1354        let label = format!("mi(`{}`)", self.texname);
1355        // Determine line thickness based on mass
1356        let thickness = if self.is_massive() {
1357            "massive"
1358        } else {
1359            "massless"
1360        };
1361
1362        // Determine color based on charge using the shared physics palette.
1363        let color = if !self.charge.is_zero() {
1364            "palette.accent"
1365        } else {
1366            "palette.ink"
1367        };
1368
1369        // Generate base styles
1370        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            // Vector bosons: differentiate based on charge and color properties
1382            if self.charge.is_zero() && self.color == 1 {
1383                // Neutral color singlet (photon): wavy line
1384                (
1385                    format!("{} + wave", base_source),
1386                    format!("{} + wave", base_sink),
1387                )
1388            } else if self.charge.is_zero() && self.color == 8 {
1389                // Neutral color octet (gluon): coiled line
1390                (
1391                    format!("{} + coil", base_source),
1392                    format!("{} + coil", base_sink),
1393                )
1394            } else {
1395                // Charged vector bosons (W+/W-/Z with mass): zigzag line
1396                (
1397                    format!("{} + zigzag", base_source),
1398                    format!("{} + zigzag", base_sink),
1399                )
1400            }
1401        } else if self.is_scalar() {
1402            // Scalar particles: dashed lines
1403            (
1404                format!("source-stroke(c: {color}, thickness: {thickness}, dash: dashed)",),
1405                format!("sink-stroke(c: {color}, thickness: {thickness}, dash: dashed)"),
1406            )
1407        } else {
1408            // Default: solid line
1409            (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.value == other.value
1583            && 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: (&param.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, normalise_complex(&rhs?)])
1612        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    /// Physical vector PDG -> [vector, Goldstone, ghost, antighost]. The model
1696    /// declares this BRST quartet; equal numerical masses never infer one.
1697    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) = &param.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 {
1891            //     vertex_table.push_record(&[
1892            //         vr.name.to_string().green().to_string(),
1893            //         vr.particles
1894            //             .iter()
1895            //             .map(|p| format!("{:<6}", p.name.to_string()).blue().to_string())
1896            //             .collect::<Vec<_>>()
1897            //             .join(", "),
1898            //     ]);
1899            // }
1900            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                // Pad the record with empty strings if necessary
1906                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                        // keep only the split after column 0 (between col 0 and 1)
1918                        .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    /// Generate edge-style.typ template file with styles for all particles in the model
1969    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        // Create the directory if it doesn't exist
1976        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        // Generate styles for all particles in the model
1992        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        // Remove zero parameters from the input card
2049        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        // Set all external parameters with value 0 to constant internal parameters with expression zero.
2057        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        // Remove all vertices with zero couplings
2085        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        // Now remove all couplings that are zero
2159        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        // Rebuild shared particles and the mass-dependent caches through the same
2177        // constructor used for already-restricted UFO imports.
2178        *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, &params)
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(&param_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        //initialize the UFO and ETS symbols
2445
2446        // let _ = *UFO;
2447        let _ = *ETS;
2448
2449        // Expression parsing can create forward references without their UFO print callbacks.
2450        // Register every declared name first; Symbolica cannot attach callbacks afterward.
2451        for name in serializable_model
2452            .parameters
2453            .iter()
2454            .map(|parameter| &parameter.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        // Extract coupling orders
2471        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        // Extract parameters
2489        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        // Only exact internal expressions establish structural zeros. Propagate
2500        // them to a fixed point so derived chemical potentials also disappear
2501        // when their external roots were fixed to zero by a restriction card.
2502        // Numerical values never establish this: external roots remain tunable.
2503        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        // Extract particles
2538        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        // Extract propagators
2565
2566        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        // Extract Lorentz structures
2584        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        // Extract couplings
2601        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        // Extract vertex rules
2612        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        // Set propagator mapping
2628        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    /// Covariant completeness acts on whole final-state multisets. Mixed
2728    /// vector/Goldstone states and the two ghost charges must remain distinct;
2729    /// graph automorphisms supply their symmetry factors exactly once.
2730    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        // Neutrinos remain literal-ZERO fermions even when every quark mass is free.
2979        // Their propagators supply an independent oracle for both fermion flows.
2980        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        // This card also fixes every chemical-potential root to zero, including
3013        // the derived particle and antiparticle assignments.
3014        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        // Repeated restriction and serialization must preserve the structural result.
3021        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        // Keep a numerically zero external mass as the adjustable control.
3060        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            // An extra dependency level requires transitive zero propagation.
3074            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            // These internal zero-valued potentials still depend on free roots.
3089            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            // A numerically zero external mass remains adjustable at the same import boundary.
3107            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        // PL = (1-gamma5)/2 and PR = (1+gamma5)/2. Scalar entries use the
3225        // Goldstone chirality fixed by the Higgs-doublet action; vector entries
3226        // retain their original left/right weights under the basis change.
3227        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        // The CP-even Higgs and projector sum must be gamma5-free before
3262        // dimensional Dirac algebra, while the projector difference is gamma5.
3263        let right = model.get_lorentz_structure("FFS1");
3264        let left = model.get_lorentz_structure("FFS3");
3265        // Probe the two independent coefficients of the structurally certified
3266        // linear basis without distributing either Lorentz expression.
3267        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); //Some(F(8.66025)));
3302
3303        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 vals = [0.01, 0., 1.].into_iter().map(F).collect::<Vec<_>>();
3309        // let mom = ThreeMomentum::new(vals[0], vals[1], vals[2]).into_on_shell_four_momentum(None);
3310
3311        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}