1#![allow(clippy::indexing_slicing)] use 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#[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
39fn 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#[derive(Clone, Debug)]
58struct Interface {
59 x: [[Complex; 2]; 2],
61 z: [Complex; 2],
63 radials: [[Riccati; 2]; 2],
65 matrix: Matrix4,
67 before: Matrix42,
70}
71
72#[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#[derive(Clone, Debug)]
85pub struct MieGradient {
86 pub sizes: Vec<f64>,
88 pub epsilon: Vec<Complex>,
90 pub mu: Vec<Complex>,
92 pub kappa: Vec<Complex>,
94}
95
96impl MieGradient {
97 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 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
117pub(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
132fn 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
183fn 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
203pub 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 "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 #[must_use]
260 pub fn boundaries(&self) -> usize {
261 self.sizes.len()
262 }
263
264 #[must_use]
266 pub const fn value(&self) -> &Matrix2 {
267 &self.value
268 }
269
270 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 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 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 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 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 #![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 fn embedding(materials: &[Material]) -> [Complex; 2] {
445 helicity_ks(1.0, &materials.last().copied().unwrap_or_default())
446 }
447
448 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 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 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 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 #![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 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}