treams_core/coeffs/
mie.rs

1//! Mie coefficients of multilayer chiral spheres and their analytic pullback.
2//!
3//! Upstream: `treams.coeffs.mie`. The pullback is a treams-rs extension.
4#![allow(clippy::indexing_slicing)] // Fixed-size matrices of the interface equations.
5
6use super::{Material, Matrix2, Matrix4, Matrix42, material::add_entries, validate_layers};
7use crate::special::{Radial, spherical_radial};
8use crate::{
9    Complex, Error, MAX_DEGREE, Result,
10    numerics::{finite, ratio},
11};
12
13mod saved;
14
15// Radial entries of one interface argument x = size (n -+ kappa) for the regular and
16// singular kinds: (f, f' + f/x) of the spherical function f, and their x-derivatives.
17#[derive(Clone, Copy, Debug)]
18struct Riccati {
19    waves: [(Complex, Complex); 2],
20    slopes: [(Complex, Complex); 2],
21}
22
23impl Riccati {
24    fn new(l: u32, x: Complex) -> Result<Self> {
25        let zero = (Complex::default(), Complex::default());
26        let mut result = Self {
27            waves: [zero; 2],
28            slopes: [zero; 2],
29        };
30        for (kind, radial) in [Radial::Regular, Radial::Singular].into_iter().enumerate() {
31            let f = spherical_radial(l, x, radial)?;
32            result.waves[kind] = (f.value, f.first + f.value / x);
33            result.slopes[kind] = (f.first, f.second + f.first / x - f.value / x.powu(2));
34        }
35        Ok(result)
36    }
37}
38
39// Radial entries of both helicities on both sides of an interface; achiral media share
40// one argument for both helicities.
41fn radials(l: u32, x: [[Complex; 2]; 2]) -> Result<[[Riccati; 2]; 2]> {
42    let side = |[negative, positive]: [Complex; 2]| -> Result<[Riccati; 2]> {
43        let first = Riccati::new(l, negative)?;
44        Ok([
45            first,
46            if positive == negative {
47                first
48            } else {
49                Riccati::new(l, positive)?
50            },
51        ])
52    };
53    Ok([side(x[0])?, side(x[1])?])
54}
55
56/// One boundary between two media, as the pullback reads it.
57#[derive(Clone, Debug)]
58struct Interface {
59    /// Arguments `size (n -+ kappa)` of the inner and outer medium, by helicity.
60    x: [[Complex; 2]; 2],
61    /// Impedances of the inner and outer medium.
62    z: [Complex; 2],
63    /// Radial functions at `x`.
64    radials: [[Riccati; 2]; 2],
65    /// The four-channel interface matrix `F` from inner to outer coefficients.
66    matrix: Matrix4,
67    /// The product of the interface matrices inside this boundary, applied to the
68    /// two regular core channels.
69    before: Matrix42,
70}
71
72/// What [`mie`] saves for its pullback: the interface matrices and radial functions of
73/// one degree `l`, so the pullback evaluates no Bessel functions.
74#[derive(Clone, Debug)]
75pub struct MieResidual {
76    sizes: Vec<f64>,
77    materials: Vec<Material>,
78    interfaces: Vec<Interface>,
79    inverse: Matrix2,
80    value: Matrix2,
81}
82
83/// Gradients of the inputs of [`mie`], in the order of its arguments.
84#[derive(Clone, Debug)]
85pub struct MieGradient {
86    /// Size-parameter gradients, from inner to outer boundary.
87    pub sizes: Vec<f64>,
88    /// Permittivity gradients, including the embedding medium.
89    pub epsilon: Vec<Complex>,
90    /// Permeability gradients, including the embedding medium.
91    pub mu: Vec<Complex>,
92    /// Chirality gradients, including the embedding medium.
93    pub kappa: Vec<Complex>,
94}
95
96impl MieGradient {
97    /// Zero gradients of `boundaries` size parameters and `boundaries + 1` media.
98    pub(crate) fn zeros(boundaries: usize) -> Self {
99        let media = vec![Complex::default(); boundaries + 1];
100        Self {
101            sizes: vec![0.0; boundaries],
102            epsilon: media.clone(),
103            mu: media.clone(),
104            kappa: media,
105        }
106    }
107
108    /// Add the gradient of the same layers along another cotangent.
109    pub(crate) fn accumulate(&mut self, other: &Self) {
110        add_entries(&mut self.sizes, &other.sizes);
111        add_entries(&mut self.epsilon, &other.epsilon);
112        add_entries(&mut self.mu, &other.mu);
113        add_entries(&mut self.kappa, &other.kappa);
114    }
115}
116
117/// A coefficient matrix in the mode order of a T-matrix.
118///
119/// [`mie`] and [`mie_cyl`](fn@super::mie_cyl) index their matrices by helicity (negative,
120/// positive), which is the polarization index `pol` (0, 1). A basis lists `pol = 1`
121/// before `pol = 0`, so both axes swap. The swap is its own inverse, so it also takes a
122/// cotangent in mode order back to helicity order.
123pub(crate) fn to_mode_order(matrix: &Matrix2) -> Matrix2 {
124    Matrix2::new(
125        matrix[(1, 1)],
126        matrix[(1, 0)],
127        matrix[(0, 1)],
128        matrix[(0, 0)],
129    )
130}
131
132// The four-channel interface matrix F and, if TANGENT, its directional derivative
133// along (dx, dz). The radial derivatives are the cached slopes times dx.
134fn interface<const TANGENT: bool>(
135    x: [[Complex; 2]; 2],
136    z: [Complex; 2],
137    radials: &[[Riccati; 2]; 2],
138    dx: [[Complex; 2]; 2],
139    dz: [Complex; 2],
140) -> (Matrix4, Matrix4) {
141    let derivative = |side: usize, pol: usize, kind: usize| {
142        let (value, first) = radials[side][pol].slopes[kind];
143        (value * dx[side][pol], first * dx[side][pol])
144    };
145    let impedance_ratio = z[1] / z[0];
146    let da = (dz[1] / z[0] - impedance_ratio * dz[0] / z[0]) / (2.0 * Complex::i());
147    let mut f = Matrix4::zeros();
148    let mut df = Matrix4::zeros();
149    for p in 0..2 {
150        let square = x[1][p].powu(2);
151        for q in 0..2 {
152            let sign = if p == q { 1.0 } else { -1.0 };
153            let a = (impedance_ratio + sign) / (2.0 * Complex::i());
154            let factor = a * square;
155            let dfactor = if TANGENT {
156                da * square + 2.0 * a * x[1][p] * dx[1][p]
157            } else {
158                Complex::default()
159            };
160            for kind in 0..2 {
161                let inner = radials[0][q].waves[kind];
162                for out_kind in 0..2 {
163                    let outer = radials[1][p].waves[1 - out_kind];
164                    let out_sign = if out_kind == 0 { 1.0 } else { -1.0 };
165                    let term = out_sign * (outer.1 * inner.0 - sign * outer.0 * inner.1);
166                    let entry = (p + out_kind * 2, q + kind * 2);
167                    f[entry] = term * factor;
168                    if TANGENT {
169                        let (dinner, douter) =
170                            (derivative(0, q, kind), derivative(1, p, 1 - out_kind));
171                        let dterm = out_sign
172                            * (douter.1 * inner.0 + outer.1 * dinner.0
173                                - sign * (douter.0 * inner.1 + outer.0 * dinner.1));
174                        df[entry] = dterm * factor + term * dfactor;
175                    }
176                }
177            }
178        }
179    }
180    (f, df)
181}
182
183// Absorbing media can make every entry enormous while the coefficient ratio is
184// well conditioned. Normalize before forming the determinant, then use the
185// shared scaled complex division instead of squaring its magnitude.
186fn inverse2(matrix: Matrix2) -> Result<Matrix2> {
187    let scale = matrix
188        .iter()
189        .map(|z| z.re.abs().max(z.im.abs()))
190        .fold(0.0, f64::max);
191    if scale == 0.0 || !scale.is_finite() {
192        return Err(Error::Singular);
193    }
194    let a = matrix.map(|z| z / scale);
195    let determinant = a.determinant();
196    if determinant == Complex::default() {
197        return Err(Error::Singular);
198    }
199    Ok(Matrix2::new(a[(1, 1)], -a[(0, 1)], -a[(1, 0)], a[(0, 0)])
200        .map(|z| ratio(z, determinant) / scale))
201}
202
203/// Mie coefficients of degree `l` of a concentric multilayer chiral sphere.
204///
205/// `sizes` are the size parameters `k0 r` of the boundaries, from inner to outer, and
206/// `materials` has one more entry, the embedding medium last.
207///
208/// Upstream: `treams.coeffs.mie`.
209pub fn mie(l: u32, sizes: &[f64], materials: &[Material]) -> Result<MieResidual> {
210    if !(1..=MAX_DEGREE.unsigned_abs()).contains(&l) {
211        return Err(Error::InvalidInput(
212            // 128 is MAX_DEGREE.
213            "require 1 <= l <= 128".into(),
214        ));
215    }
216    validate_layers(sizes, materials)?;
217    let mut q = Matrix42::zeros();
218    q[(0, 0)] = Complex::new(1.0, 0.0);
219    q[(1, 1)] = Complex::new(1.0, 0.0);
220    let mut interfaces = Vec::with_capacity(sizes.len());
221    for (i, size) in sizes.iter().enumerate() {
222        let pair = [materials[i], materials[i + 1]];
223        let x = pair.map(|m| [size * (m.index() - m.kappa), size * (m.index() + m.kappa)]);
224        let z = pair.map(Material::impedance);
225        let radials = radials(l, x)?;
226        let (matrix, _) = interface::<false>(
227            x,
228            z,
229            &radials,
230            [[Complex::default(); 2]; 2],
231            [Complex::default(); 2],
232        );
233        interfaces.push(Interface {
234            x,
235            z,
236            radials,
237            matrix,
238            before: q,
239        });
240        q = matrix * q;
241    }
242    let upper = q.fixed_rows::<2>(0).into_owned();
243    let inverse = inverse2(upper)?;
244    let value = q.fixed_rows::<2>(2) * inverse;
245    if value.iter().any(|z| !finite(*z)) {
246        return Err(Error::Singular);
247    }
248    Ok(MieResidual {
249        sizes: sizes.to_vec(),
250        materials: materials.to_vec(),
251        interfaces,
252        inverse,
253        value,
254    })
255}
256
257impl MieResidual {
258    /// The number of recorded layer boundaries.
259    #[must_use]
260    pub fn boundaries(&self) -> usize {
261        self.sizes.len()
262    }
263
264    /// The Mie coefficient matrix, which the pullback reads.
265    #[must_use]
266    pub const fn value(&self) -> &Matrix2 {
267        &self.value
268    }
269
270    /// The coefficient tangent along changes of the sizes and materials. Each
271    /// `materials` entry stores changes of epsilon, mu and kappa, including the
272    /// embedding medium. One combined direction travels through the cached
273    /// interfaces; no Bessel functions or parameter Jacobians are evaluated.
274    pub fn pushforward(&self, sizes: &[f64], materials: &[Material]) -> Result<Matrix2> {
275        super::validate_layer_tangents(self.sizes.len(), sizes, materials)?;
276        let optical: Vec<_> = self
277            .materials
278            .iter()
279            .zip(materials)
280            .map(|(&material, &direction)| material.pushforward(direction))
281            .collect();
282        let mut dq = Matrix42::zeros();
283        for (i, layer) in self.interfaces.iter().enumerate() {
284            let dx = std::array::from_fn(|side| {
285                let material = optical[i + side];
286                let dn = [
287                    material.index - material.kappa,
288                    material.index + material.kappa,
289                ];
290                std::array::from_fn(|pol| {
291                    layer.x[side][pol] * (sizes[i] / self.sizes[i]) + self.sizes[i] * dn[pol]
292                })
293            });
294            let dz = [optical[i].impedance, optical[i + 1].impedance];
295            let (_, df) = interface::<true>(layer.x, layer.z, &layer.radials, dx, dz);
296            dq = df * layer.before + layer.matrix * dq;
297        }
298        Ok((dq.fixed_rows::<2>(2) - self.value * dq.fixed_rows::<2>(0)) * self.inverse)
299    }
300
301    /// Gradients of the size parameters and the materials from `cotangent`, the
302    /// gradient of a real loss with respect to the coefficient matrix.
303    ///
304    /// The pullback runs on the calling thread and adds the boundaries in order, from
305    /// the outermost in.
306    pub fn pullback(&self, cotangent: &Matrix2) -> Result<MieGradient> {
307        if cotangent.iter().any(|z| !finite(*z)) {
308            return Err(Error::InvalidInput("cotangents must be finite".into()));
309        }
310        let mut result = MieGradient::zeros(self.sizes.len());
311        let lower = cotangent * self.inverse.adjoint();
312        let upper = -self.value.adjoint() * lower;
313        let mut gq = Matrix42::zeros();
314        gq.fixed_rows_mut::<2>(0).copy_from(&upper);
315        gq.fixed_rows_mut::<2>(2).copy_from(&lower);
316        for (i, layer) in self.interfaces.iter().enumerate().rev() {
317            let gf = gq * layer.before.adjoint();
318            let contract = |dx, dz| -> Complex {
319                let (_, df) = interface::<true>(layer.x, layer.z, &layer.radials, dx, dz);
320                gf.iter()
321                    .zip(df.iter())
322                    .map(|(g, d)| g.conj() * d)
323                    .sum::<Complex>()
324                    .conj()
325            };
326            let size = self.sizes[i];
327            result.sizes[i] = contract(
328                layer.x.map(|row| row.map(|x| x / size)),
329                [Complex::default(); 2],
330            )
331            .re;
332            for side in 0..2 {
333                let material = self.materials[i + side];
334                let targets = [&mut result.epsilon, &mut result.mu, &mut result.kappa];
335                for (tangent, target) in material.tangents().into_iter().zip(targets) {
336                    let mut dx = [[Complex::default(); 2]; 2];
337                    dx[side] = [
338                        size * (tangent.index - tangent.kappa),
339                        size * (tangent.index + tangent.kappa),
340                    ];
341                    let mut dz = [Complex::default(); 2];
342                    dz[side] = tangent.impedance;
343                    target[i + side] += contract(dx, dz);
344                }
345            }
346            gq = layer.matrix.adjoint() * gq;
347        }
348        Ok(result)
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use proptest::{prelude::*, test_runner::TestCaseError};
355
356    use super::{Material, Matrix2, inverse2, mie};
357    use crate::{
358        Complex,
359        test_support::{ALGEBRA_CASES, Chirality, complex, helicity_ks, prop_assert_close, re_dot},
360    };
361
362    /// A material with epsilon in [1, 6] + i [0, 0.5 loss], mu in [1, 2] + i [0, 0.2 loss]
363    /// and a real chirality of at most 0.3 Re n, so both helicity indices stay positive.
364    fn material(loss: f64) -> impl Strategy<Value = Material> {
365        crate::test_support::material(
366            1.0..6.0,
367            1.0..2.0,
368            [0.5 * loss, 0.2 * loss],
369            Chirality::Relative(0.3),
370        )
371    }
372
373    /// One to three concentric boundaries at least 0.05 apart from 0.2 up, the layer
374    /// materials, and a lossless chiral embedding.
375    fn sphere(loss: f64) -> impl Strategy<Value = (Vec<f64>, Vec<Material>)> {
376        (1_usize..=3)
377            .prop_flat_map(move |layers| {
378                (
379                    0.2_f64..1.0,
380                    prop::collection::vec(0.05_f64..1.0, layers - 1),
381                    prop::collection::vec(material(loss), layers),
382                    material(0.0),
383                )
384            })
385            .prop_map(|(first, gaps, mut materials, embedding)| {
386                let sizes = std::iter::once(first)
387                    .chain(gaps)
388                    .scan(0.0, |size, gap| {
389                        *size += gap;
390                        Some(*size)
391                    })
392                    .collect();
393                materials.push(embedding);
394                (sizes, materials)
395            })
396    }
397
398    /// A lossless dielectric sphere of size 0.2 to 3 and permittivity 1 to 8 in vacuum.
399    fn large() -> impl Strategy<Value = (Vec<f64>, Vec<Material>)> {
400        (0.2_f64..3.0, 1.0_f64..8.0).prop_map(|(size, epsilon)| {
401            let sphere = Material {
402                epsilon: Complex::from(epsilon),
403                ..Material::default()
404            };
405            (vec![size], vec![sphere, Material::default()])
406        })
407    }
408
409    fn cotangent() -> impl Strategy<Value = Matrix2> {
410        [complex(1.0), complex(1.0), complex(1.0), complex(1.0)]
411            .prop_map(|[a, b, c, d]| Matrix2::new(a, b, c, d))
412    }
413
414    proptest! {
415        // A Mie evaluation of up to 3 layers costs microseconds, as an algebraic check does.
416        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
417
418        #[test]
419        fn mie_reciprocity_mirror_and_duality((sizes, materials) in sphere(1.0), l in 1_u32..8) {
420            check_mie_symmetries(l, &sizes, &materials)?;
421        }
422
423        #[test]
424        fn lossless_mie_optical_theorem_and_its_derivative(
425            (sizes, materials) in prop_oneof![sphere(0.0), large()], l in 1_u32..9,
426        ) {
427            check_lossless_mie(l, &sizes, &materials)?;
428        }
429
430        #[test]
431        fn mie_pullback_scaling_identities(
432            (sizes, materials) in sphere(1.0), l in 1_u32..8, g in cotangent(),
433        ) {
434            check_mie_scaling(l, &sizes, &materials, &g)?;
435        }
436    }
437
438    fn largest(matrix: &Matrix2) -> f64 {
439        matrix.iter().map(|z| z.norm()).fold(0.0, f64::max)
440    }
441
442    /// Helicity wavenumbers `n -+ kappa` of the embedding, the last material, at
443    /// `k0 = 1`.
444    fn embedding(materials: &[Material]) -> [Complex; 2] {
445        helicity_ks(1.0, &materials.last().copied().unwrap_or_default())
446    }
447
448    /// Reciprocity `k_+^2 T_-+ = k_-^2 T_+-` in the helicity wavenumbers of the embedding;
449    /// mirroring every chirality swaps the helicities; and spheres whose media all have
450    /// `epsilon = mu` are dual symmetric and keep the helicity.
451    fn check_mie_symmetries(
452        l: u32,
453        sizes: &[f64],
454        materials: &[Material],
455    ) -> Result<(), TestCaseError> {
456        let t = mie(l, sizes, materials).unwrap().value;
457        let scale = largest(&t);
458        let [minus, plus] = embedding(materials).map(|k| k * k);
459        prop_assert_close!(
460            plus * t[(0, 1)],
461            minus * t[(1, 0)],
462            1e-12 * scale * plus.norm().max(minus.norm())
463        );
464        let mirrored: Vec<_> = materials
465            .iter()
466            .map(|m| Material {
467                kappa: -m.kappa,
468                ..*m
469            })
470            .collect();
471        let swapped = mie(l, sizes, &mirrored).unwrap().value;
472        let reversed = Matrix2::new(t[(1, 1)], t[(1, 0)], t[(0, 1)], t[(0, 0)]);
473        prop_assert_close!(swapped, reversed, 1e-13 * scale);
474        let dual: Vec<_> = materials
475            .iter()
476            .map(|m| Material {
477                mu: m.epsilon,
478                ..*m
479            })
480            .collect();
481        let t = mie(l, sizes, &dual).unwrap().value;
482        prop_assert_close!(
483            [t[(0, 1)], t[(1, 0)]],
484            [Complex::default(); 2],
485            1e-14 * largest(&t)
486        );
487        Ok(())
488    }
489
490    /// Lossless spheres conserve energy: `T^H W + W T + 2 T^H W T = 0` with the flux
491    /// weights `W = diag(k_-^-2, k_+^-2)` of the embedding, which makes `I + 2T` unitary
492    /// for an achiral one. Its derivative along every lossless direction that keeps W,
493    /// all but the embedding's parameters, is `Re tr(G^H dT) = 0` with `G = W (I + 2T)`:
494    /// the pullback of G has vanishing size gradients and real material parts.
495    ///
496    /// Near a sharp resonance the halves `W` and `2 W T` of G pull back to large, nearly
497    /// cancelling gradients, so each bound scales with the gradient of `<W, T>` in that
498    /// parameter, the pullbacks of `W` and `i W`. A fixed bound of `1e-12 max W` fails
499    /// for large spheres at resonance, which need about `2e-11 max W`.
500    fn check_lossless_mie(
501        l: u32,
502        sizes: &[f64],
503        materials: &[Material],
504    ) -> Result<(), TestCaseError> {
505        let t = mie(l, sizes, materials).unwrap().value;
506        let weights = Matrix2::from_diagonal(&embedding(materials).map(|k| (k * k).inv()).into());
507        let scale = largest(&weights);
508        let two = Complex::new(2.0, 0.0);
509        let residual = t.adjoint() * weights + weights * t + t.adjoint() * weights * t * two;
510        prop_assert_close!(residual, Matrix2::zeros(), 1e-12 * scale);
511        // The gradients of the sizes, then of the interior epsilon, mu and kappa.
512        let lossless = |g: Matrix2| {
513            let gradient = mie(l, sizes, materials).unwrap().pullback(&g).unwrap();
514            let interior = [gradient.epsilon, gradient.mu, gradient.kappa]
515                .into_iter()
516                .flat_map(|values| values.into_iter().take(sizes.len()));
517            let boundaries = gradient.sizes.into_iter().map(Complex::from);
518            boundaries.chain(interior).collect::<Vec<_>>()
519        };
520        let gradient = lossless(weights * (Matrix2::identity() + t * two));
521        let real = lossless(weights);
522        let imaginary = lossless(weights * Complex::i());
523        for (i, value) in gradient.iter().enumerate() {
524            let tolerance = 1e-12 * scale.max(real[i].norm() + imaginary[i].norm());
525            prop_assert_close!(value.re, 0.0, tolerance, "parameter {}", i);
526        }
527        Ok(())
528    }
529
530    /// T depends only on `size (n -+ kappa)` and impedance ratios, which scaling every
531    /// medium by `epsilon -> s^2 epsilon` (or `mu -> s^2 mu`), `kappa -> s kappa` and every
532    /// size by `1/s` leaves unchanged. At `s = 1` the pullback therefore satisfies
533    /// `sum size g_size = 2 Re<g_epsilon, epsilon> + Re<g_kappa, kappa>`, and the same with mu.
534    fn check_mie_scaling(
535        l: u32,
536        sizes: &[f64],
537        materials: &[Material],
538        g: &Matrix2,
539    ) -> Result<(), TestCaseError> {
540        let gradient = mie(l, sizes, materials).unwrap().pullback(g).unwrap();
541        let size = sizes
542            .iter()
543            .zip(&gradient.sizes)
544            .map(|(s, g)| s * g)
545            .sum::<f64>();
546        let epsilon = re_dot(&gradient.epsilon, materials.iter().map(|m| m.epsilon));
547        let mu = re_dot(&gradient.mu, materials.iter().map(|m| m.mu));
548        let kappa = re_dot(&gradient.kappa, materials.iter().map(|m| m.kappa));
549        let scale = size.abs() + epsilon.abs() + mu.abs() + kappa.abs();
550        prop_assert_close!(size, 2.0 * epsilon + kappa, 1e-12 * scale);
551        prop_assert_close!(size, 2.0 * mu + kappa, 1e-12 * scale);
552        Ok(())
553    }
554
555    proptest! {
556        // One closed-form 2 x 2 inverse per case.
557        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
558
559        #[test]
560        fn inverse_preserves_complex_scale(
561            re in -1.0_f64..1.0,
562            im in -1.0_f64..1.0,
563            exponent in -300_i32..301,
564        ) {
565            let z = Complex::new(re, im);
566            let matrix = Matrix2::new(3.0 + z, z, -z, 4.0 - z)
567                * Complex::new(10.0_f64.powi(exponent), 0.0);
568            let inverse = inverse2(matrix).unwrap();
569            prop_assert!((matrix * inverse - Matrix2::identity()).norm() < 4e-15);
570        }
571    }
572
573    #[test]
574    fn metallic_sphere_matches_independent_mie_ratios() {
575        // 120-digit direct Riccati-Bessel ratios from scripts/qualify_references.py,
576        // stable at 80 digits; x = 80, epsilon = -8 + 0.4i, vacuum.
577        let references = [
578            (
579                1,
580                (-0.499_957_965_242_302_9, -0.000_033_295_596_701_331_95),
581                (-0.315_374_612_638_891_9, -0.377_908_027_840_409_7),
582            ),
583            (
584                3,
585                (-0.499_774_523_335_837_05, -0.000_229_816_544_066_314_44),
586                (-0.360_037_027_186_839_1, -0.335_632_030_366_205_5),
587            ),
588            (
589                80,
590                (-0.421_594_673_791_517_6, -0.391_815_903_887_81),
591                (-0.280_927_898_598_179_07, -0.045_759_796_275_944_01),
592            ),
593            (
594                99,
595                (-6.117_266_839_130_22e-10, 1.109_939_769_728_892_8e-8),
596                (-5.594_374_358_681_406e-10, 1.597_214_940_003_522_4e-8),
597            ),
598        ];
599        let material = Material {
600            epsilon: Complex::new(-8.0, 0.4),
601            ..Material::default()
602        };
603        for (l, diagonal, off_diagonal) in references {
604            let diagonal = Complex::new(diagonal.0, diagonal.1);
605            let off_diagonal = Complex::new(off_diagonal.0, off_diagonal.1);
606            let reference = Matrix2::new(diagonal, off_diagonal, off_diagonal, diagonal);
607            let result = mie(l, &[80.0], &[material, Material::default()]).unwrap();
608            for (actual, expected) in result.value.iter().zip(reference.iter()) {
609                assert!((*actual - expected).norm() <= 2e-13 + 2e-11 * expected.norm());
610            }
611        }
612    }
613}