1use std::f64::consts::PI;
13
14use super::{MAX_ORDER, SERIES_RADIUS, log_factorial};
15use crate::{
16 Complex, Error, Result,
17 numerics::{
18 Jet,
19 broadcast::{self, element},
20 finite,
21 parallel::Parallel,
22 ratio,
23 },
24};
25
26const PARALLEL: Parallel = Parallel::AtLeast(64);
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Bessel {
35 J,
37 Y,
39 H1,
41 H2,
43}
44
45impl From<Radial> for Bessel {
46 fn from(radial: Radial) -> Self {
48 match radial {
49 Radial::Regular => Self::J,
50 Radial::Singular => Self::H1,
51 }
52 }
53}
54
55fn amos_error(error: complex_bessel::Error) -> Error {
56 Error::SpecialFunction(error.to_string())
57}
58
59fn spherical_prefactor(z: Complex) -> Complex {
68 let mut w = PI / (2.0 * z);
69 if z.im == 0.0 && z.re < 0.0 {
70 w.im = -0.0;
71 }
72 crate::numerics::complex_sqrt(w)
73}
74
75#[inline]
77fn bessel_raw(order: f64, z: Complex, kind: Bessel, spherical: bool) -> Result<Complex> {
78 let order = if spherical { order + 0.5 } else { order };
79 let value = match kind {
80 Bessel::J => complex_bessel::besselj(order, z),
81 Bessel::Y => complex_bessel::bessely(order, z),
82 Bessel::H1 => complex_bessel::hankel1(order, z),
83 Bessel::H2 => complex_bessel::hankel2(order, z),
84 }
85 .map_err(amos_error)?;
86 Ok(if spherical {
87 spherical_prefactor(z) * value
88 } else {
89 value
90 })
91}
92
93fn sequence(kind: Bessel, start: f64, z: Complex, length: usize) -> Result<Vec<Complex>> {
95 let unscaled = complex_bessel::Scaling::Unscaled;
96 match kind {
97 Bessel::J => complex_bessel::besselj_seq(start, z, length, unscaled),
98 Bessel::Y => complex_bessel::bessely_seq(start, z, length, unscaled),
99 Bessel::H1 => complex_bessel::hankel1_seq(start, z, length, unscaled),
100 Bessel::H2 => complex_bessel::hankel2_seq(start, z, length, unscaled),
101 }
102 .map(|sequence| sequence.values)
103 .map_err(amos_error)
104}
105
106fn pair(values: Vec<Complex>) -> Result<[Complex; 2]> {
108 <[Complex; 2]>::try_from(values)
109 .map_err(|_| Error::SpecialFunction("invalid Bessel sequence length".into()))
110}
111
112pub(crate) fn hankel_below(order: u32, z: Complex) -> Result<[Complex; 2]> {
116 if order == 0 {
117 let [value, upper] = pair(sequence(Bessel::H1, 0.0, z, 2)?)?;
118 return Ok([-upper, value]);
119 }
120 pair(sequence(Bessel::H1, f64::from(order - 1), z, 2)?)
121}
122
123fn spherical_pair(kind: Bessel, order: f64, z: Complex) -> Result<[Complex; 2]> {
127 let values = if matches!(kind, Bessel::H1 | Bessel::H2) {
128 pair(sequence(kind, order + 0.5, z, 2)?)?
129 } else {
130 let single = |order: f64| bessel_raw(order + 0.5, z, kind, false);
131 [single(order)?, single(order + 1.0)?]
132 };
133 let prefactor = spherical_prefactor(z);
134 Ok(values.map(|value| prefactor * value))
135}
136
137fn checked(jet: RadialJet) -> Result<RadialJet> {
141 if finite(jet.value) && finite(jet.first) && finite(jet.second) {
142 Ok(jet)
143 } else {
144 Err(Error::SpecialFunction(
145 "radial function or derivative overflow".into(),
146 ))
147 }
148}
149
150fn jet_from_next_order(order: f64, z: Complex, [value, next]: [Complex; 2]) -> RadialJet {
153 let first = order * ratio(value, z) - next;
154 let second = -2.0 * ratio(first, z) + order * (order + 1.0) * ratio(ratio(value, z), z) - value;
155 RadialJet {
156 value,
157 first,
158 second,
159 }
160}
161
162#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[inline]
174pub fn bessel(
175 order: f64,
176 z: Complex,
177 kind: Bessel,
178 spherical: bool,
179 derivative: u8,
180) -> Result<Complex> {
181 if !order.is_finite() || !finite(z) || derivative > 2 {
182 return Err(Error::InvalidInput(
183 "finite order/argument and derivative 0, 1 or 2 required".into(),
184 ));
185 }
186 let order = if order.is_subnormal() { 0.0 } else { order };
190 let jet = |jet: RadialJet| match derivative {
191 0 => jet.value,
192 1 => jet.first,
193 _ => jet.second,
194 };
195 let evaluate = |v| bessel_raw(v, z, kind, spherical);
196 let value = if spherical
197 && kind == Bessel::J
198 && z.norm() < SERIES_RADIUS
199 && (0.0..=f64::from(MAX_ORDER)).contains(&order)
200 && order.fract() == 0.0
201 {
202 jet(spherical_radial(order as u32, z, Radial::Regular)?)
203 } else if derivative == 0 {
204 evaluate(order)?
205 } else if spherical {
206 jet(jet_from_next_order(
207 order,
208 z,
209 spherical_pair(kind, order, z)?,
210 ))
211 } else if matches!(kind, Bessel::H1 | Bessel::H2) && (order != 0.0 || derivative == 2) {
212 let length = usize::from(2 * derivative + 1);
213 match sequence(kind, order - f64::from(derivative), z, length)?.as_slice() {
214 [left, _, right] => 0.5 * (left - right),
215 [left, _, middle, _, right] => 0.25 * (left - 2.0 * middle + right),
216 _ => {
217 return Err(Error::SpecialFunction(
218 "invalid Hankel sequence length".into(),
219 ));
220 }
221 }
222 } else if derivative == 1 {
223 if order == 0.0 {
224 -evaluate(1.0)?
225 } else {
226 0.5 * (evaluate(order - 1.0)? - evaluate(order + 1.0)?)
227 }
228 } else {
229 0.25 * (evaluate(order - 2.0)? - 2.0 * evaluate(order)? + evaluate(order + 2.0)?)
230 };
231 if !finite(value) {
232 return Err(Error::SpecialFunction("non-finite Bessel result".into()));
233 }
234 Ok(value)
235}
236
237#[derive(Debug)]
240pub struct BesselResidual {
241 orders: Vec<f64>,
242 arguments: Vec<Complex>,
243 kind: Bessel,
244 spherical: bool,
245 derivative: u8,
246 size: usize,
247}
248
249pub fn bessel_array(
256 orders: Vec<f64>,
257 arguments: Vec<Complex>,
258 kind: Bessel,
259 spherical: bool,
260 derivative: u8,
261) -> Result<(Vec<Complex>, BesselResidual)> {
262 let residual = BesselResidual::new(orders, arguments, kind, spherical, derivative)?;
263 let value = broadcast::map(residual.size, PARALLEL, |i| {
264 residual.evaluate(i, derivative)
265 })?;
266 Ok((value, residual))
267}
268
269impl BesselResidual {
270 pub fn new(
272 orders: Vec<f64>,
273 arguments: Vec<Complex>,
274 kind: Bessel,
275 spherical: bool,
276 derivative: u8,
277 ) -> Result<Self> {
278 let message =
279 "Bessel arrays must have equal lengths or scalar inputs, and derivative 0 or 1";
280 if derivative > 1 {
281 return Err(Error::InvalidInput(message.into()));
282 }
283 let size = broadcast::size(&[orders.len(), arguments.len()], message)?;
284 Ok(Self {
285 orders,
286 arguments,
287 kind,
288 spherical,
289 derivative,
290 size,
291 })
292 }
293
294 fn evaluate(&self, i: usize, derivative: u8) -> Result<Complex> {
295 bessel(
296 element(&self.orders, i),
297 element(&self.arguments, i),
298 self.kind,
299 self.spherical,
300 derivative,
301 )
302 }
303
304 pub fn pushforward(&self, tangents: [&[Complex]; 1]) -> Result<Vec<Complex>> {
306 broadcast::pushforward(
307 tangents,
308 self.size,
309 "Bessel tangent must be finite and broadcast to output",
310 PARALLEL,
311 |i, [tangent]| {
312 if tangent == Complex::default() {
313 Ok(tangent)
314 } else {
315 Ok(tangent * self.evaluate(i, self.derivative + 1)?)
316 }
317 },
318 )
319 }
320
321 pub fn pullback(&self, cotangent: &[Complex]) -> Result<Vec<Complex>> {
326 let [gradient] = broadcast::pullback(
327 cotangent,
328 self.size,
329 "Bessel cotangent must be finite and match output",
330 [self.arguments.len()],
331 PARALLEL,
332 |i, &g| {
333 if g == Complex::default() {
334 Ok([g])
335 } else {
336 Ok([g * self.evaluate(i, self.derivative + 1)?.conj()])
337 }
338 },
339 )?;
340 Ok(gradient)
341 }
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
348pub enum Radial {
349 Regular,
352 #[doc(alias = "outgoing")]
355 Singular,
356}
357
358#[derive(Clone, Copy, Debug)]
361pub struct RadialJet {
362 pub value: Complex,
364 pub first: Complex,
366 pub second: Complex,
368}
369
370const SERIES_TERMS: u32 = 16;
374
375fn even_series(power: u32, leading: f64, z: Complex, factor: impl Fn(f64) -> f64) -> RadialJet {
379 let mut term = RadialJet {
380 value: leading * z.powu(power),
381 first: if power > 0 {
382 leading * f64::from(power) * z.powu(power - 1)
383 } else {
384 Complex::default()
385 },
386 second: if power > 1 {
387 leading * f64::from(power * (power - 1)) * z.powu(power - 2)
388 } else {
389 Complex::default()
390 },
391 };
392 let mut sum = term;
393 let square = z * z;
394 for q in 1..SERIES_TERMS {
395 let factor = factor(f64::from(q));
396 term = RadialJet {
397 value: factor * square * term.value,
398 first: factor * (square * term.first + 2.0 * z * term.value),
399 second: factor * (square * term.second + 4.0 * z * term.first + 2.0 * term.value),
400 };
401 sum.value += term.value;
402 sum.first += term.first;
403 sum.second += term.second;
404 }
405 sum
406}
407
408pub fn spherical_radial(l: u32, z: Complex, radial: Radial) -> Result<RadialJet> {
419 if l > MAX_ORDER.unsigned_abs() || !finite(z) {
420 return Err(Error::InvalidInput(
421 "require finite argument and 0 <= l <= 256".into(),
423 ));
424 }
425 if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
426 let double_factorial: f64 = (0..=l).map(|i| f64::from(2 * i + 1)).product();
428 let degree = f64::from(l);
429 return Ok(even_series(l, double_factorial.recip(), z, |q| {
430 -1.0 / (2.0 * q * (2.0 * (degree + q) + 1.0))
431 }));
432 }
433 if z == Complex::default() {
434 return Err(Error::InvalidInput(
435 "outgoing spherical waves are singular at zero".into(),
436 ));
437 }
438 let order = f64::from(l);
439 checked(jet_from_next_order(
440 order,
441 z,
442 spherical_pair(radial.into(), order, z)?,
443 ))
444}
445
446pub(crate) fn spherical_radial_sequence(
454 order: u32,
455 z: Complex,
456 radial: Radial,
457) -> Result<Vec<RadialJet>> {
458 if order > MAX_ORDER.unsigned_abs() || !finite(z) {
459 return Err(Error::InvalidInput(
460 "require finite argument and 0 <= l <= 256".into(),
462 ));
463 }
464 if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
465 return (0..=order)
466 .map(|l| spherical_radial(l, z, radial))
467 .collect();
468 }
469 if z == Complex::default() {
470 return Err(Error::InvalidInput(
471 "outgoing spherical waves are singular at zero".into(),
472 ));
473 }
474 let values = match radial {
475 Radial::Regular => {
476 let prefactor = spherical_prefactor(z);
477 (0..=order + 1)
478 .map(|l| Ok(prefactor * bessel_raw(f64::from(l) + 0.5, z, Bessel::J, false)?))
479 .collect::<Result<Vec<_>>>()?
480 }
481 Radial::Singular => spherical_hankels(0, order + 1, z)?,
482 };
483 (0..=order)
484 .zip(values.iter().zip(values.iter().skip(1)))
485 .map(|(l, (&value, &next))| checked(jet_from_next_order(f64::from(l), z, [value, next])))
486 .collect()
487}
488
489pub(crate) fn spherical_hankels(first: u32, last: u32, z: Complex) -> Result<Vec<Complex>> {
496 let count = usize::try_from(last.saturating_sub(first) + 1)
497 .map_err(|_| Error::InvalidInput("invalid radial order".into()))?;
498 if first <= last && last <= 64 && z.im == 0.0 && (0.5..=1e4).contains(&z.re) {
499 let inverse = z.re.recip();
500 let (sin, cos) = z.re.sin_cos();
501 let mut previous = Complex::new(cos * inverse, sin * inverse);
503 let mut current = Complex::new(sin * inverse, -cos * inverse);
504 let mut values = Vec::with_capacity(count);
505 for degree in 0..=last {
506 if degree >= first {
507 values.push(current);
508 }
509 if degree < last {
510 let next = (f64::from(2 * degree + 1) * inverse) * current - previous;
511 previous = current;
512 current = next;
513 }
514 }
515 return Ok(values);
516 }
517 let prefactor = spherical_prefactor(z);
518 Ok(sequence(Bessel::H1, f64::from(first) + 0.5, z, count)?
519 .into_iter()
520 .map(|h| prefactor * h)
521 .collect())
522}
523
524pub fn cylindrical_radial(order: i32, z: Complex, radial: Radial) -> Result<RadialJet> {
529 if order.unsigned_abs() > MAX_ORDER.unsigned_abs() || !finite(z) {
530 return Err(Error::InvalidInput(
531 "require finite argument and |m| <= 256".into(),
533 ));
534 }
535 let m = order.unsigned_abs();
536 let sign = if order < 0 && m % 2 == 1 { -1.0 } else { 1.0 };
537 if z == Complex::default() {
538 if radial == Radial::Singular {
539 return Err(Error::InvalidInput(
540 "outgoing cylindrical waves are singular at zero".into(),
541 ));
542 }
543 return Ok(RadialJet {
544 value: Complex::new(if m == 0 { 1.0 } else { 0.0 }, 0.0),
545 first: Complex::new(if m == 1 { 0.5 * sign } else { 0.0 }, 0.0),
546 second: Complex::new(
547 if m == 0 {
548 -0.5
549 } else if m == 2 {
550 0.25
551 } else {
552 0.0
553 },
554 0.0,
555 ),
556 });
557 }
558 let degree = f64::from(m);
559 if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
560 let leading = sign * (-log_factorial(order.abs())).exp() / 2.0_f64.powf(degree);
562 return Ok(even_series(m, leading, z, |q| {
563 -1.0 / (4.0 * q * (degree + q))
564 }));
565 }
566 let [value, upper] = pair(sequence(radial.into(), degree, z, 2)?)?;
567 let value = sign * value;
568 let first = degree * value / z - sign * upper;
569 let second = (degree.powi(2) / z.powu(2) - 1.0) * value - first / z;
570 Ok(RadialJet {
571 value,
572 first,
573 second,
574 })
575}
576
577pub(crate) fn radial_jet<const N: usize>(
581 order: i32,
582 z: Jet<N>,
583 radial: Radial,
584 spherical: bool,
585) -> Result<Jet<N>> {
586 if N == 0 {
587 let value = bessel(f64::from(order), z.value, radial.into(), spherical, 0)?;
588 return Ok(Jet::constant(value));
589 }
590 let jet = if spherical {
591 spherical_radial(order.unsigned_abs(), z.value, radial)?
592 } else {
593 cylindrical_radial(order, z.value, radial)?
594 };
595 Ok(z.chain(jet.value, jet.first))
596}
597
598#[cfg(test)]
599mod tests {
600 use proptest::{prelude::*, test_runner::TestCaseError};
604
605 use super::{
606 Bessel, Radial, SERIES_RADIUS, bessel, cylindrical_radial, spherical_hankels,
607 spherical_radial, spherical_radial_sequence,
608 };
609 use crate::{
610 Complex, Error,
611 numerics::ratio,
612 test_support::{ALGEBRA_CASES, log_polar, prop_assert_close, radial, table},
613 };
614
615 proptest! {
616 #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
617
618 #[test]
619 fn spherical_radial_sequences_match_single_degrees(
620 order in 0_u32..=40,
621 z in prop_oneof![
623 log_polar(-3.0..(0.996 * SERIES_RADIUS).log10(), -3.0..3.0),
624 log_polar(SERIES_RADIUS.log10()..2.0, -1.2..1.2),
625 ],
626 radial in radial(),
627 ) {
628 check_spherical_radial_sequence(order, z, radial)?;
629 }
630
631 #[test]
632 fn cylindrical_jets_match_bessel(
633 order in -20_i32..=20,
634 z in log_polar(-5.0..1.3, -3.0..3.0),
635 radial in radial(),
636 ) {
637 check_cylindrical_jet(order, z, radial)?;
638 }
639 }
640
641 fn check_spherical_radial_sequence(
647 order: u32,
648 z: Complex,
649 radial: Radial,
650 ) -> Result<(), TestCaseError> {
651 let sequence = spherical_radial_sequence(order, z, radial).unwrap();
652 prop_assert_eq!(sequence.len(), usize::try_from(order + 1).unwrap());
653 for (l, jet) in (0..).zip(sequence) {
654 let single = spherical_radial(l, z, radial).unwrap();
655 let pairs = [
656 (jet.value, single.value),
657 (jet.first, single.first),
658 (jet.second, single.second),
659 ];
660 let size = single.value.norm() + single.first.norm() + single.second.norm();
661 for (d, (actual, expected)) in pairs.into_iter().enumerate() {
662 if radial == Radial::Regular {
663 prop_assert_eq!(actual, expected, "degree {} derivative {}", l, d);
664 } else {
665 let tolerance = 1e-13 * size;
666 prop_assert_close!(
667 actual,
668 expected,
669 tolerance,
670 "degree {} derivative {}",
671 l,
672 d
673 );
674 }
675 }
676 }
677 Ok(())
678 }
679
680 #[test]
681 fn real_hankel_sequences_match_amos_at_domain_edges_and_turning_points() {
682 for x in [
683 0.5_f64.next_down(),
684 0.5,
685 0.5_f64.next_up(),
686 1.0,
687 3.0,
688 7.4,
689 16.0,
690 31.5,
691 59.6,
692 64.0,
693 128.0,
694 1e3,
695 1e4,
696 1e4_f64.next_up(),
697 ] {
698 for last in [0, 3, 16, 64, 65] {
699 for first in [0, last / 2, last] {
700 let z = Complex::new(x, 0.0);
701 let values = spherical_hankels(first, last, z).unwrap();
702 assert_eq!(values.len(), (last - first + 1) as usize);
703 for (degree, actual) in (first..=last).zip(values) {
704 let expected = bessel(f64::from(degree), z, Bessel::H1, true, 0).unwrap();
705 assert!(
706 (actual - expected).norm() <= 1e-13 * expected.norm(),
707 "h_{degree}({x}): {actual} != {expected}"
708 );
709 }
710 }
711 }
712 }
713 }
714
715 #[test]
716 fn real_hankel_sequences_match_high_precision() {
717 let cases = table::<String, f64>(include_str!(concat!(
720 env!("CARGO_MANIFEST_DIR"),
721 "/references/spherical_hankel.txt"
722 )));
723 assert_eq!(cases.len(), 6);
724 for (key, value) in cases {
725 let degree = key[0].parse().unwrap();
726 let x = key[1].parse().unwrap();
727 let actual = spherical_hankels(degree, degree, Complex::new(x, 0.0)).unwrap()[0];
728 let expected = Complex::new(value[0], value[1]);
729 assert!(
730 (actual - expected).norm() <= 1e-13 * expected.norm(),
731 "h_{degree}({x}): {actual} != {expected}"
732 );
733 }
734 }
735
736 fn check_cylindrical_jet(order: i32, z: Complex, radial: Radial) -> Result<(), TestCaseError> {
740 let jet = cylindrical_radial(order, z, radial).unwrap();
741 let kind = Bessel::from(radial);
742 let tolerance = if radial == Radial::Regular && z.norm() < SERIES_RADIUS {
744 2e-13
745 } else {
746 2e-12
747 };
748 for (d, actual) in [0, 1, 2]
749 .into_iter()
750 .zip([jet.value, jet.first, jet.second])
751 {
752 let expected = bessel(f64::from(order), z, kind, false, d).unwrap();
753 prop_assert_close!(
754 actual,
755 expected,
756 tolerance * expected.norm(),
757 "derivative {}",
758 d
759 );
760 }
761 Ok(())
762 }
763
764 #[test]
765 fn cylindrical_tiny_nonzero_arguments_keep_representable_terms() {
766 for z in [Complex::new(1e-200, 0.0), Complex::new(1e-200, -2e-200)] {
767 let j0 = cylindrical_radial(0, z, Radial::Regular).unwrap();
768 let j1 = cylindrical_radial(1, z, Radial::Regular).unwrap();
769 let j2 = cylindrical_radial(2, z, Radial::Regular).unwrap();
770 assert!((ratio(j1.value, 0.5 * z) - 1.0).norm() < 1e-14);
771 assert!((ratio(j0.first, -0.5 * z) - 1.0).norm() < 1e-14);
772 assert!((ratio(j2.first, 0.25 * z) - 1.0).norm() < 1e-14);
773 assert!((ratio(j1.second, -0.375 * z) - 1.0).norm() < 1e-14);
774 }
775 }
776
777 #[test]
778 fn regular_jets_at_the_origin_and_singular_outgoing_waves() {
779 let exact = [
781 [1.0, 0.0, -1.0 / 3.0],
782 [0.0, 1.0 / 3.0, 0.0],
783 [0.0, 0.0, 2.0 / 15.0],
784 ];
785 for (l, expected) in (0..).zip(exact) {
786 let jet = spherical_radial(l, Complex::default(), Radial::Regular).unwrap();
787 for (actual, expected) in [jet.value, jet.first, jet.second].into_iter().zip(expected) {
788 assert!(
789 (actual - expected).norm() < 1e-16,
790 "l = {l}: {actual} != {expected}"
791 );
792 }
793 }
794 let exact = [[1.0, 0.0, -0.5], [0.0, 0.5, 0.0], [0.0, 0.0, 0.25]];
796 for (m, expected) in (0..).zip(exact) {
797 for (order, sign) in [(m, 1.0), (-m, if m % 2 == 0 { 1.0 } else { -1.0 })] {
798 let jet = cylindrical_radial(order, Complex::default(), Radial::Regular).unwrap();
799 for (actual, expected) in
800 [jet.value, jet.first, jet.second].into_iter().zip(expected)
801 {
802 assert_eq!(actual, Complex::new(sign * expected, 0.0), "m = {order}");
803 }
804 }
805 }
806 assert!(spherical_radial(1, Complex::default(), Radial::Singular).is_err());
807 assert!(cylindrical_radial(1, Complex::default(), Radial::Singular).is_err());
808 let overflows = |result: crate::Result<()>, case: &str| {
812 let Err(Error::SpecialFunction(message)) = result else {
813 panic!("{case}: expected an overflow, got {result:?}");
814 };
815 assert!(message.contains("overflow"), "{case}: {message}");
816 };
817 for (radius, phase) in [(1e-200, -0.8), (1e-200, 2.9), (1e-120, 0.3), (1e-120, -2.2)] {
818 let tiny = Complex::from_polar(radius, phase);
819 for l in 0..=2 {
820 let case = format!("h_{l}({tiny:e})");
821 overflows(spherical_radial(l, tiny, Radial::Singular).map(drop), &case);
822 overflows(
823 spherical_radial_sequence(l, tiny, Radial::Singular).map(drop),
824 &case,
825 );
826 }
827 }
828 }
829}