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
114pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
161pub enum Sheet {
162 Physical,
164 Unphysical,
166}
167
168pub 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
183pub 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
194pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
208pub enum BarrierKind {
209 Full,
211 Tensor,
213}
214
215pub const BLATT_WEISSKOPF_MAX_L: usize = 8;
217
218pub const QR_DEFAULT: f64 = 0.1973;
220
221pub 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
247pub 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(¶ms).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}