Skip to main content

laddu_physics/math/
functions.rs

1use std::f64::consts::PI;
2
3use laddu_expr::Expr;
4use num::complex::Complex64;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    LadduPhysicsError, LadduPhysicsResult,
9    quantum::{L, M},
10};
11
12#[inline]
13fn validate_finite_real(name: &str, value: f64) -> LadduPhysicsResult<()> {
14    if !value.is_finite() {
15        return Err(LadduPhysicsError::invalid_value(name, "finite", value));
16    }
17    Ok(())
18}
19
20#[inline]
21fn validate_spherical_harmonic_quantum_numbers(l: L, m: M) -> LadduPhysicsResult<usize> {
22    if !m.is_integer() {
23        return Err(LadduPhysicsError::invalid_relation(format!(
24            "m must be an integer, got {m}"
25        )));
26    }
27    let abs_m = (m.doubled() / 2).unsigned_abs();
28
29    if abs_m > l.value() {
30        return Err(LadduPhysicsError::invalid_relation(format!(
31            "|m| <= l, got l = {l}, m = {m}"
32        )));
33    }
34
35    if l.value().checked_add(abs_m).is_none() {
36        return Err(LadduPhysicsError::numeric_overflow(format!(
37            "l + |m| for l = {l}, m = {m}"
38        )));
39    }
40
41    Ok(abs_m as usize)
42}
43
44#[inline]
45fn validate_blatt_weisskopf_l(l: L) -> LadduPhysicsResult<()> {
46    if l.value() as usize > BLATT_WEISSKOPF_MAX_L {
47        return Err(LadduPhysicsError::unsupported_value(
48            "l",
49            format!("l <= {BLATT_WEISSKOPF_MAX_L}"),
50            l,
51        ));
52    }
53
54    Ok(())
55}
56
57#[inline]
58fn validate_q_r(q_r: f64) -> LadduPhysicsResult<()> {
59    validate_finite_real("q_r", q_r)?;
60
61    if q_r <= 0.0 {
62        return Err(LadduPhysicsError::invalid_value("q_r", "positive", q_r));
63    }
64
65    Ok(())
66}
67
68#[inline]
69fn associated_legendre_pos_m(l: usize, m: usize, x: Expr) -> Expr {
70    let mut p: Expr = 1.0.into();
71
72    if l == 0 && m == 0 {
73        return p;
74    }
75
76    let y = (1.0 - x.powi(2)).sqrt();
77
78    for m_p in 0..m {
79        p *= -((2 * m_p + 1) as f64) * &y;
80    }
81
82    if l == m {
83        return p;
84    }
85
86    let mut p_min_2 = p;
87    let mut p_min_1 = (2 * m + 1) as f64 * &x * &p_min_2;
88
89    if l == m + 1 {
90        return p_min_1;
91    }
92
93    for l_p in (m + 1)..l {
94        let next = ((2 * l_p + 1) as f64 * &x * &p_min_1 - (l_p + m) as f64 * &p_min_2)
95            / (l_p - m + 1) as f64;
96        p_min_2 = p_min_1;
97        p_min_1 = next;
98    }
99
100    p_min_1
101}
102
103#[inline]
104fn factorial_ratio_l_minus_over_l_plus(l: usize, abs_m: usize) -> f64 {
105    let mut ratio = 1.0;
106
107    for k in (l - abs_m + 1)..=(l + abs_m) {
108        ratio /= k as f64;
109    }
110
111    ratio
112}
113
114/// Construct the complex spherical harmonic $`Y_\ell^m(\theta,\phi)`$.
115///
116/// # Errors
117///
118/// Returns [`LadduPhysicsError`] when `l` or `m` cannot be converted to the
119/// required quantum-number type, or when `m` is not an integer projection with
120/// `|m| <= l`.
121pub fn spherical_harmonic(
122    l: impl TryInto<L>,
123    m: impl TryInto<M>,
124    costheta: impl Into<Expr>,
125    phi: impl Into<Expr>,
126) -> LadduPhysicsResult<Expr> {
127    let l = l
128        .try_into()
129        .map_err(|_| LadduPhysicsError::ConversionError("L"))?;
130    let m = m
131        .try_into()
132        .map_err(|_| LadduPhysicsError::ConversionError("M"))?;
133    validate_spherical_harmonic_quantum_numbers(l, m)?;
134
135    Ok(spherical_harmonic_expr(
136        l.value() as usize,
137        m.doubled() as isize / 2,
138        costheta.into(),
139        phi.into(),
140    ))
141}
142
143#[inline(always)]
144fn spherical_harmonic_expr(l: usize, m: isize, costheta: Expr, phi: Expr) -> Expr {
145    let abs_m = m.unsigned_abs();
146
147    let mut res = associated_legendre_pos_m(l, abs_m, costheta);
148
149    res *=
150        f64::sqrt((2 * l + 1) as f64 / (4.0 * PI) * factorial_ratio_l_minus_over_l_plus(l, abs_m));
151
152    if m < 0 && !abs_m.is_multiple_of(2) {
153        res = -res;
154    }
155
156    &res * (m as f64 * &phi).cos() + Complex64::I * res * (m as f64 * phi).sin()
157}
158
159/// Selects the Riemann sheet used for analytic continuation of two-body kinematic functions.
160#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
161pub enum Sheet {
162    /// The physical sheet of the complex energy plane.
163    Physical,
164    /// The unphysical sheet obtained by flipping the sign of the channel momentum.
165    Unphysical,
166}
167
168/// Construct the analytic two-body breakup momentum on a selected Riemann sheet.
169pub fn q(s: impl Into<Expr>, mass1: impl Into<Expr>, mass2: impl Into<Expr>, sheet: Sheet) -> Expr {
170    let s = s.into();
171    let m1 = mass1.into();
172    let m2 = mass2.into();
173    let sp = (&m1 + &m2).powi(2);
174    let sm = (m1 - m2).powi(2);
175    let q_phys = ((&s - sp) * (&s - sm)).sqrt() / (2.0 * s.sqrt());
176
177    match sheet {
178        Sheet::Physical => q_phys,
179        Sheet::Unphysical => -q_phys,
180    }
181}
182
183/// Construct the two-body phase-space factor $`\rho(s) = 2q/\sqrt{s}`$.
184pub fn rho(
185    s: impl Into<Expr>,
186    mass1: impl Into<Expr>,
187    mass2: impl Into<Expr>,
188    sheet: Sheet,
189) -> Expr {
190    let s = s.into();
191    2.0 * q(s.clone(), mass1, mass2, sheet) / s.sqrt()
192}
193
194/// Construct the Chew-Mandelstam function for a two-body channel.
195pub fn chew_mandelstam(s: impl Into<Expr>, mass1: impl Into<Expr>, mass2: impl Into<Expr>) -> Expr {
196    let s = s.into();
197    let mass1 = mass1.into();
198    let mass2 = mass2.into();
199    let chi_plus = 1.0 - (&mass1 + &mass2).powi(2) / &s;
200    let rho = rho(&s, &mass1, &mass2, Sheet::Physical);
201    (rho.clone() * ((&chi_plus + &rho) / (&chi_plus - rho)).log()
202        - &chi_plus * ((&mass2 - &mass1) / (&mass1 + &mass2)) * (&mass2 / mass1).log())
203        / PI
204}
205
206/// Selects which form of the Blatt-Weisskopf barrier factor is returned.
207#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
208pub enum BarrierKind {
209    /// The full barrier factor, including threshold powers of $`q`$.
210    Full,
211    /// The tensor barrier factor, with the explicit $`q^\ell`$ dependence removed.
212    Tensor,
213}
214
215/// Highest orbital angular momentum implemented by the Blatt-Weisskopf factors.
216pub const BLATT_WEISSKOPF_MAX_L: usize = 8;
217
218/// Default Blatt-Weisskopf radius parameter $`q_R`$ in GeV.
219pub const QR_DEFAULT: f64 = 0.1973;
220
221/// Construct a Blatt-Weisskopf factor with an explicit radius parameter.
222///
223/// # Errors
224///
225/// Returns [`LadduPhysicsError`] when `l` cannot be converted, exceeds
226/// [`BLATT_WEISSKOPF_MAX_L`], or `q_r` is not positive and finite.
227pub fn blatt_weisskopf_custom(
228    q: impl Into<Expr>,
229    l: impl TryInto<L>,
230    kind: BarrierKind,
231    q_r: f64,
232) -> LadduPhysicsResult<Expr> {
233    let q = q.into();
234    let l = l
235        .try_into()
236        .map_err(|_| LadduPhysicsError::ConversionError("L"))?;
237    validate_blatt_weisskopf_l(l)?;
238    validate_q_r(q_r)?;
239    let z = q.powi(2) / q_r.powi(2);
240    let full = blatt_weisskopf_polynomial_expr(z, l);
241    Ok(match kind {
242        BarrierKind::Full => full,
243        BarrierKind::Tensor => full / q.powi(l.value() as i32),
244    })
245}
246
247/// Construct a Blatt-Weisskopf factor using [`QR_DEFAULT`].
248///
249/// # Errors
250///
251/// Returns [`LadduPhysicsError`] when `l` cannot be converted or exceeds
252/// [`BLATT_WEISSKOPF_MAX_L`].
253pub fn blatt_weisskopf(
254    q: impl Into<Expr>,
255    l: impl TryInto<L>,
256    kind: BarrierKind,
257) -> LadduPhysicsResult<Expr> {
258    let q = q.into();
259    let l = l
260        .try_into()
261        .map_err(|_| LadduPhysicsError::ConversionError("L"))?;
262    validate_blatt_weisskopf_l(l)?;
263    validate_q_r(QR_DEFAULT)?;
264    let z = q.powi(2) / QR_DEFAULT.powi(2);
265    let full = blatt_weisskopf_polynomial_expr(z, l);
266    Ok(match kind {
267        BarrierKind::Full => full,
268        BarrierKind::Tensor => full / q.powi(l.value() as i32),
269    })
270}
271
272#[inline(always)]
273fn blatt_weisskopf_polynomial_expr(z: Expr, l: L) -> Expr {
274    let value = match l.value() {
275        0 => Complex64::new(1.0, 0.0).into(),
276        1 => (2.0 * &z) / (z + 1.0),
277        2 => (13.0 * z.powi(2)) / ((&z - 3.0).powi(2) + 9.0 * z),
278        3 => (277.0 * z.powi(3)) / (z.powi(3) + 6.0 * z.powi(2) + 45.0 * z + 225.0),
279        4 => {
280            (12746.0 * z.powi(4))
281                / (z.powi(4) + 10.0 * z.powi(3) + 135.0 * z.powi(2) + 1575.0 * z + 11025.0)
282        }
283        5 => {
284            (998881.0 * z.powi(5))
285                / (z.powi(5)
286                    + 15.0 * z.powi(4)
287                    + 315.0 * z.powi(3)
288                    + 6300.0 * z.powi(2)
289                    + 99225.0 * z
290                    + 893025.0)
291        }
292        6 => {
293            (118394977.0 * z.powi(6))
294                / (z.powi(6)
295                    + 21.0 * z.powi(5)
296                    + 630.0 * z.powi(4)
297                    + 18900.0 * z.powi(3)
298                    + 496125.0 * z.powi(2)
299                    + 9823275.0 * z
300                    + 18261468225.0)
301        }
302        7 => {
303            (19727003738.0 * z.powi(7))
304                / (z.powi(7)
305                    + 28.0 * z.powi(6)
306                    + 1134.0 * z.powi(5)
307                    + 47250.0 * z.powi(4)
308                    + 1819125.0 * z.powi(3)
309                    + 58939650.0 * z.powi(2)
310                    + 1404728325.0 * z
311                    + 18261468225.0)
312        }
313        8 => {
314            (4392846440677.0 * z.powi(8))
315                / (z.powi(8)
316                    + 36.0 * z.powi(7)
317                    + 1890.0 * z.powi(6)
318                    + 103950.0 * z.powi(5)
319                    + 5457375.0 * z.powi(4)
320                    + 255405150.0 * z.powi(3)
321                    + 9833098275.0 * z.powi(2)
322                    + 273922023375.0 * z
323                    + 4108830350625.0)
324        }
325        _ => {
326            unreachable!("l must be validated before calling blatt_weisskopf_polynomial_expr")
327        }
328    };
329
330    value.sqrt()
331}
332
333#[cfg(test)]
334mod test {
335    use std::f64::consts::PI;
336
337    use approx::assert_relative_eq;
338    use laddu_compile::CompiledModel;
339    use laddu_runtime::CpuBackend;
340    use num::complex::Complex64;
341
342    use super::*;
343    use crate::{
344        channel::Channel,
345        l, m,
346        vectors::{RealVec4, Vec3},
347    };
348
349    const EPS: f64 = 1.0e-12;
350    const LOOSE_EPS: f64 = 1.0e-8;
351
352    fn evaluate(expr: Expr) -> Complex64 {
353        let model = CompiledModel::from_expr(&expr).unwrap();
354        let params = model.params().default_values();
355        CpuBackend.prepare(&model).evaluate(&params).unwrap()
356    }
357
358    fn assert_complex_relative_eq(actual: Complex64, expected: Complex64, epsilon: f64) {
359        assert_relative_eq!(actual.re, expected.re, epsilon = epsilon);
360        assert_relative_eq!(actual.im, expected.im, epsilon = epsilon);
361    }
362
363    fn p4(px: f64, py: f64, pz: f64, e: f64) -> crate::vectors::Vec4 {
364        RealVec4::new(e, px, py, pz).into()
365    }
366
367    fn expected_spherical_harmonic(l: usize, m: isize, costheta: f64, phi: f64) -> Complex64 {
368        match (l, m) {
369            (0, 0) => Complex64::from(f64::sqrt(1.0 / (4.0 * PI))),
370            (1, -1) => Complex64::from_polar(
371                f64::sqrt(3.0 / (8.0 * PI)) * f64::sin(f64::acos(costheta)),
372                -phi,
373            ),
374            (1, 0) => Complex64::from(f64::sqrt(3.0 / (4.0 * PI)) * costheta),
375            (1, 1) => Complex64::from_polar(
376                -f64::sqrt(3.0 / (8.0 * PI)) * f64::sin(f64::acos(costheta)),
377                phi,
378            ),
379            (2, -2) => Complex64::from_polar(
380                f64::sqrt(15.0 / (32.0 * PI)) * f64::sin(f64::acos(costheta)).powi(2),
381                -2.0 * phi,
382            ),
383            (2, -1) => Complex64::from_polar(
384                f64::sqrt(15.0 / (8.0 * PI)) * f64::sin(f64::acos(costheta)) * costheta,
385                -phi,
386            ),
387            (2, 0) => {
388                Complex64::from(f64::sqrt(5.0 / (16.0 * PI)) * (3.0 * costheta.powi(2) - 1.0))
389            }
390            (2, 1) => Complex64::from_polar(
391                -f64::sqrt(15.0 / (8.0 * PI)) * f64::sin(f64::acos(costheta)) * costheta,
392                phi,
393            ),
394            (2, 2) => Complex64::from_polar(
395                f64::sqrt(15.0 / (32.0 * PI)) * f64::sin(f64::acos(costheta)).powi(2),
396                2.0 * phi,
397            ),
398            _ => unreachable!("test only covers l <= 2"),
399        }
400    }
401
402    #[test]
403    fn spherical_harmonics_match_known_values() {
404        let modes = [
405            (l!(0), m!(0)),
406            (l!(1), m!(-1)),
407            (l!(1), m!(0)),
408            (l!(1), m!(1)),
409            (l!(2), m!(-2)),
410            (l!(2), m!(-1)),
411            (l!(2), m!(0)),
412            (l!(2), m!(1)),
413            (l!(2), m!(2)),
414        ];
415        let costhetas = [-1.0, -0.8, -0.3, 0.0, 0.3, 0.8, 1.0];
416        let phis = [0.0, 0.3, 0.5, 0.8, 1.0].map(|v| v * 2.0 * PI);
417
418        for (l, m) in modes {
419            for costheta in costhetas {
420                for phi in phis {
421                    let expected = expected_spherical_harmonic(
422                        l.value() as usize,
423                        m.doubled() as isize / 2,
424                        costheta,
425                        phi,
426                    );
427
428                    assert_complex_relative_eq(
429                        evaluate(spherical_harmonic(l, m, costheta, phi).unwrap()),
430                        expected,
431                        EPS,
432                    );
433                }
434            }
435        }
436    }
437
438    #[test]
439    fn spherical_harmonic_accepts_vertex_angle_expressions() {
440        let mut channel = Channel::new("decay");
441        channel.edge("x").p4(p4(0.0, 0.0, 0.0, 2.0));
442        channel.edge("a").p4(p4(1.0, 0.0, 0.0, 1.0));
443        channel.edge("b").p4(p4(-1.0, 0.0, 0.0, 1.0));
444        channel
445            .vertex("x_decay")
446            .incoming(["x"])
447            .outgoing(["a", "b"])
448            .validate()
449            .unwrap();
450        let vertex = channel.get_vertex("x_decay").unwrap();
451        let costheta = vertex.costheta("a", Vec3::z(), Vec3::y()).unwrap();
452        let phi = vertex.phi("a", Vec3::z(), Vec3::y()).unwrap();
453
454        assert_complex_relative_eq(
455            evaluate(spherical_harmonic(l!(1), m!(1), costheta, phi).unwrap()),
456            evaluate(spherical_harmonic(l!(1), m!(1), 0.0, 0.0).unwrap()),
457            EPS,
458        );
459    }
460
461    #[test]
462    fn two_body_kinematics_match_known_values() {
463        let m1 = 0.51;
464        let m2 = 0.62;
465        let s = 1.3;
466        let m = f64::sqrt(s);
467
468        assert_complex_relative_eq(evaluate(Expr::from(m1) + m2), 1.13.into(), EPS);
469        assert_complex_relative_eq(evaluate((&Expr::from(m1) + m2).powi(2)), 1.2769.into(), EPS);
470        assert_complex_relative_eq(
471            evaluate((&Expr::from(m1) - m2).powi(2).sqrt()),
472            0.11.into(),
473            EPS,
474        );
475        assert_complex_relative_eq(evaluate((&Expr::from(m1) - m2).powi(2)), 0.0121.into(), EPS);
476        assert_complex_relative_eq(
477            evaluate(1.0 - (&Expr::from(m1) + m2).powi(2) / s),
478            0.01776923076923098.into(),
479            EPS,
480        );
481        assert_complex_relative_eq(
482            evaluate(1.0 - (&Expr::from(m1) - m2).powi(2) / s),
483            0.9906923076923076.into(),
484            EPS,
485        );
486
487        let rho_expected = Complex64::new(0.13267946426138, 0.0);
488        assert_complex_relative_eq(
489            evaluate(rho(Complex64::from(s), m1, m2, Sheet::Physical)),
490            rho_expected,
491            EPS,
492        );
493
494        let q_expected = Complex64::new(0.3954823004889093, 0.0);
495        assert_complex_relative_eq(
496            evaluate(q(Complex64::from(1.44), 0.4, 0.5, Sheet::Physical)),
497            q_expected,
498            EPS,
499        );
500        assert_complex_relative_eq(
501            evaluate(q(1.44, 0.4, 0.5, Sheet::Unphysical)),
502            -q_expected,
503            EPS,
504        );
505
506        assert_complex_relative_eq(
507            evaluate(rho(m.powi(2), 1.23, 0.62, Sheet::Physical)),
508            Complex64::new(0.0, 1.0795209736472833),
509            EPS,
510        );
511        assert_complex_relative_eq(
512            evaluate(q(1.44, 1.4, 1.5, Sheet::Physical)),
513            Complex64::new(0.0, 1.3154464282347478),
514            EPS,
515        );
516    }
517
518    #[test]
519    fn blatt_weisskopf_matches_known_values() {
520        let above_expected = [
521            (l!(0), 1.0),
522            (l!(1), 1.2654752018685698),
523            (l!(2), 2.375285855793918),
524            (l!(3), 5.62658768678507),
525            (l!(4), 12.747554064467208),
526        ];
527
528        let below_expected = [
529            (l!(0), 1.0),
530            (l!(1), 1.430394249144933),
531            (l!(2), 3.724659004227952),
532            (l!(3), 17.689297320491015),
533            (l!(4), 124.05258418258987),
534            (l!(5), 1138.5868292398761),
535            (l!(6), 6211.480561374802),
536            (l!(7), 172727.17381791578),
537            (l!(8), 2630882.804294494),
538        ];
539
540        for (l, expected_re) in above_expected {
541            assert_complex_relative_eq(
542                evaluate(
543                    blatt_weisskopf(q(1.44, 0.4, 0.5, Sheet::Physical), l, BarrierKind::Full)
544                        .unwrap(),
545                ),
546                Complex64::new(expected_re, 0.0),
547                LOOSE_EPS,
548            );
549            assert_complex_relative_eq(
550                evaluate(
551                    blatt_weisskopf_custom(
552                        q(1.44, 0.4, 0.5, Sheet::Physical),
553                        l,
554                        BarrierKind::Full,
555                        QR_DEFAULT,
556                    )
557                    .unwrap(),
558                ),
559                Complex64::new(expected_re, 0.0),
560                LOOSE_EPS,
561            );
562        }
563
564        for (l, expected_re) in below_expected {
565            assert_complex_relative_eq(
566                evaluate(
567                    blatt_weisskopf(q(1.44, 1.4, 1.5, Sheet::Physical), l, BarrierKind::Full)
568                        .unwrap(),
569                ),
570                Complex64::new(expected_re, 0.0),
571                LOOSE_EPS,
572            );
573        }
574    }
575
576    #[test]
577    fn tensor_barrier_is_full_barrier_with_explicit_q_power_removed() {
578        let q = Complex64::new(0.3, 0.2);
579
580        for l_raw in 0..=BLATT_WEISSKOPF_MAX_L {
581            let l = L::try_from(l_raw).unwrap();
582            let full = blatt_weisskopf(q, l, BarrierKind::Full).unwrap();
583            let tensor = blatt_weisskopf(q, l, BarrierKind::Tensor).unwrap();
584
585            assert_complex_relative_eq(
586                evaluate(tensor) * q.powu(l_raw as u32),
587                evaluate(full),
588                LOOSE_EPS,
589            );
590        }
591
592        let tensor_l0_at_threshold =
593            blatt_weisskopf(Complex64::new(0.0, 0.0), l!(0), BarrierKind::Tensor).unwrap();
594        assert_complex_relative_eq(
595            evaluate(tensor_l0_at_threshold),
596            Complex64::new(1.0, 0.0),
597            EPS,
598        );
599    }
600
601    #[test]
602    fn constructors_reject_invalid_static_configuration() {
603        assert!(spherical_harmonic(l!(1), m!(2), 0.0, 0.0).is_err());
604        assert!(spherical_harmonic(l!(1), m!(1 / 2), 0.0, 0.0).is_err());
605        assert!(blatt_weisskopf(1.0, BLATT_WEISSKOPF_MAX_L + 1, BarrierKind::Full).is_err());
606        assert!(blatt_weisskopf_custom(1.0, l!(0), BarrierKind::Full, 0.0).is_err());
607        assert!(blatt_weisskopf_custom(1.0, l!(0), BarrierKind::Full, f64::NAN).is_err());
608    }
609}