use maryada::EnclosureOps;
#[derive(Clone, Copy, Debug)]
pub struct NamedMomentum<'a> {
pub name: &'a str,
pub p4: RealVec4,
}
#[derive(Clone, Copy, Debug)]
pub struct NamedMass<'a> {
pub name: &'a str,
pub mass: f64,
}
#[derive(Clone, Debug)]
pub struct ProposalResult {
pub outgoing: Vec<RealVec4>,
pub weight: f64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum VertexProposal {
#[default]
TwoBodyDecay,
TwoBodyScattering {
proposal: TwoBodyScattering,
},
}
impl VertexProposal {
pub fn isotropic_decay() -> Self {
Self::TwoBodyDecay
}
pub fn t_exchange(
pairing: (impl Into<String>, impl Into<String>),
distribution: TDistribution,
) -> Self {
Self::TwoBodyScattering {
proposal: TwoBodyScattering::t_exchange(pairing, distribution),
}
}
pub fn propose(
&self,
incoming: &[NamedMomentum<'_>],
outgoing: &[NamedMass<'_>],
rng: &mut ProposalRng,
) -> LadduPhysicsResult<ProposalResult> {
match self {
Self::TwoBodyDecay => propose_two_body_decay(incoming, outgoing, rng),
Self::TwoBodyScattering { proposal } => proposal.propose(incoming, outgoing, rng),
}
}
#[doc(hidden)]
pub fn proven_domain_metadata(&self) -> (usize, usize) {
match self {
Self::TwoBodyDecay => (2, 1),
Self::TwoBodyScattering { proposal } => proposal.proven_domain_metadata(),
}
}
}
fn propose_two_body_decay(
incoming: &[NamedMomentum<'_>],
outgoing: &[NamedMass<'_>],
rng: &mut ProposalRng,
) -> LadduPhysicsResult<ProposalResult> {
let kinematics = TwoBodyDecayKinematics::new("isotropic", incoming, outgoing)?;
let direction = rng.isotropic_direction();
kinematics.finish(direction, 1.0)
}
struct TwoBodyDecayKinematics {
parent: RealVec4,
daughter_masses: [f64; 2],
momentum: f64,
phase_space: f64,
}
impl TwoBodyDecayKinematics {
fn new(
family: &str,
incoming: &[NamedMomentum<'_>],
outgoing: &[NamedMass<'_>],
) -> LadduPhysicsResult<Self> {
if incoming.len() != 1 || outgoing.len() != 2 {
return Err(LadduPhysicsError::invalid_relation(format!(
"{family} decay requires one incoming and two outgoing edges, got {} incoming and {} outgoing",
incoming.len(),
outgoing.len()
)));
}
let parent = incoming[0].p4;
let mass = parent.m()?;
let daughter_masses = [outgoing[0].mass, outgoing[1].mass];
let momentum = two_body_momentum(mass, daughter_masses[0], daughter_masses[1])?;
Ok(Self {
parent,
daughter_masses,
momentum,
phase_space: momentum / (4.0 * PI * mass),
})
}
fn finish(
self,
direction: RealVec3,
angular_correction: f64,
) -> LadduPhysicsResult<ProposalResult> {
let first = on_shell(direction, self.momentum, self.daughter_masses[0]);
let second = on_shell(-direction, self.momentum, self.daughter_masses[1]);
let beta = self.parent.beta()?;
Ok(ProposalResult {
outgoing: vec![first.boost(&beta), second.boost(&beta)],
weight: self.phase_space * angular_correction,
})
}
}
#[derive(Clone, Debug)]
#[doc(hidden)]
pub struct AdaptiveTwoBodyDecay {
density: PiecewiseDensity,
defensive_fraction: f64,
}
impl AdaptiveTwoBodyDecay {
pub fn new(counts: Arc<[f64]>, defensive_fraction: f64) -> LadduPhysicsResult<Self> {
let total: f64 = counts.iter().sum();
if counts.is_empty()
|| counts
.iter()
.any(|count| !count.is_finite() || *count < 0.0)
|| !total.is_finite()
|| total <= 0.0
|| !defensive_fraction.is_finite()
|| !(0.0..=1.0).contains(&defensive_fraction)
{
return Err(LadduPhysicsError::invalid_relation(
"adaptive decay requires nonnegative finite counts with positive total and a defensive fraction in [0, 1]",
));
}
let density = PiecewiseDensity::uniform(-1.0, 1.0, counts).map_err(|_| LadduPhysicsError::invalid_relation("adaptive decay requires nonnegative finite counts with positive total and a defensive fraction in [0, 1]"))?;
Ok(Self {
density,
defensive_fraction,
})
}
fn sample_costheta(&self, rng: &mut ProposalRng) -> (f64, f64) {
let costheta = if rng.uniform() < self.defensive_fraction {
2.0 * rng.uniform() - 1.0
} else {
self.density
.sample_indexed(rng)
.expect("validated adaptive density is sampleable")
};
let learned_density = self.density.density(-1.0, 1.0, costheta);
(
costheta,
self.defensive_fraction * 0.5 + (1.0 - self.defensive_fraction) * learned_density,
)
}
pub fn propose(
&self,
incoming: &[NamedMomentum<'_>],
outgoing: &[NamedMass<'_>],
rng: &mut ProposalRng,
) -> LadduPhysicsResult<ProposalResult> {
let kinematics = TwoBodyDecayKinematics::new("adaptive", incoming, outgoing)?;
let (cos_theta, density) = self.sample_costheta(rng);
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
let phi = 2.0 * PI * rng.uniform();
let direction = RealVec3::new(sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta);
kinematics.finish(direction, 0.5 / density)
}
}
pub(super) fn two_body_momentum(parent: f64, first: f64, second: f64) -> LadduPhysicsResult<f64> {
if !parent.is_finite()
|| !first.is_finite()
|| !second.is_finite()
|| parent <= 0.0
|| first < 0.0
|| second < 0.0
{
return Err(LadduPhysicsError::invalid_relation(format!(
"two-body masses must be finite, with a positive parent and nonnegative daughters; got parent={parent}, first={first}, second={second}"
)));
}
if parent < first + second {
return Err(LadduPhysicsError::invalid_relation(format!(
"two-body threshold is closed: {parent} < {}",
first + second
)));
}
let lambda =
(parent * parent - (first + second).powi(2)) * (parent * parent - (first - second).powi(2));
Ok(lambda.max(0.0).sqrt() / (2.0 * parent))
}
#[doc(hidden)]
pub fn proven_two_body_decay_weight(
parent: Interval,
first: Interval,
second: Interval,
) -> Interval {
let sum = first + second;
let difference = first - second;
let parent_squared = parent.sqr();
let radicand = (parent_squared - sum.sqr()) * (parent_squared - difference.sqr());
let momentum = radicand.sqrt() / (2.0 * parent);
let result = momentum / (4.0 * PI * parent);
Interval::new(0.0, result.sup())
}
pub(super) fn on_shell(direction: RealVec3, momentum: f64, mass: f64) -> RealVec4 {
(direction * momentum).with_mass(mass)
}