1#![allow(clippy::indexing_slicing)] use nalgebra::DMatrix;
10
11use rayon::prelude::*;
12
13use super::{InteractionGradient, InteractionResidual, interaction_blocks};
14use crate::coeffs::Material;
15use crate::special::Radial;
16use crate::sw::{self, TranslationPlan};
17use crate::tmatrix::{SphereResidual, sphere};
18use crate::{
19 Complex, Error, Result,
20 numerics::{self, finite},
21 saved::{Reader, SavedState, Writer},
22};
23
24#[derive(Clone, Debug)]
27pub struct SphereClusterResidual {
28 k0: f64,
29 positions: Vec<[f64; 3]>,
30 plan: TranslationPlan,
31 spheres: Vec<SphereResidual>,
32 interaction: InteractionResidual,
33}
34
35#[derive(Clone, Debug)]
37pub struct SphereClusterGradient {
38 pub k0: f64,
40 pub radii: Vec<f64>,
42 pub epsilon: Vec<Complex>,
44 pub positions: Vec<[f64; 3]>,
46}
47
48pub fn sphere_cluster(
57 lmax: u32,
58 k0: f64,
59 radii: &[f64],
60 epsilon: &[Complex],
61 positions: &[[f64; 3]],
62) -> Result<SphereClusterResidual> {
63 let ClusterParts {
64 blocks,
65 spheres,
66 plan,
67 coupling,
68 } = cluster_parts(lmax, k0, radii, epsilon, positions)?;
69 Ok(SphereClusterResidual {
70 k0,
71 positions: positions.to_vec(),
72 spheres,
73 plan,
74 interaction: interaction_blocks(blocks, coupling)?,
75 })
76}
77
78pub fn sphere_cluster_factor(
86 lmax: u32,
87 k0: f64,
88 radii: &[f64],
89 epsilon: &[Complex],
90 positions: &[[f64; 3]],
91) -> Result<super::InteractionFactor> {
92 let parts = cluster_parts(lmax, k0, radii, epsilon, positions)?;
93 super::InteractionFactor::from_blocks(parts.blocks, parts.coupling)
94}
95
96pub(crate) fn validate_spheres(
100 k0: f64,
101 radii: &[f64],
102 epsilon: &[Complex],
103 positions: &[[f64; 3]],
104) -> Result<()> {
105 if !k0.is_finite()
106 || k0 <= 0.0
107 || radii.is_empty()
108 || radii.len() != epsilon.len()
109 || radii.len() != positions.len()
110 || radii.iter().any(|r| !r.is_finite() || *r <= 0.0)
111 || epsilon.iter().any(|&z| !finite(z))
112 || positions.iter().flatten().any(|x| !x.is_finite())
113 {
114 return Err(Error::InvalidInput(
115 "require positive finite k0 and radii, and matching finite permittivities and positions".into(),
116 ));
117 }
118 for i in 0..radii.len() {
119 for j in 0..i {
120 let distance = positions[i]
121 .iter()
122 .zip(positions[j])
123 .fold(0.0_f64, |norm, (a, b)| norm.hypot(a - b));
124 if distance <= radii[i] + radii[j] || !distance.is_finite() {
125 return Err(Error::InvalidInput(
126 "spherical particles must have finite separation and must not touch or overlap"
127 .into(),
128 ));
129 }
130 }
131 }
132 Ok(())
133}
134
135pub(crate) fn vacuum_sphere(epsilon: Complex) -> [Material; 2] {
137 [
138 Material {
139 epsilon,
140 ..Material::default()
141 },
142 Material::default(),
143 ]
144}
145
146struct ClusterParts {
147 blocks: Vec<DMatrix<Complex>>,
149 spheres: Vec<SphereResidual>,
150 plan: TranslationPlan,
151 coupling: DMatrix<Complex>,
152}
153
154fn cluster_parts(
155 lmax: u32,
156 k0: f64,
157 radii: &[f64],
158 epsilon: &[Complex],
159 positions: &[[f64; 3]],
160) -> Result<ClusterParts> {
161 validate_spheres(k0, radii, epsilon, positions)?;
162 let modes = sw::modes(lmax)?;
163 let modes_per_particle = modes.len();
164 let (blocks, spheres): (Vec<_>, Vec<_>) = crate::threads::install(|| {
165 radii
166 .par_iter()
167 .zip(epsilon)
168 .map(|(&radius, &epsilon)| sphere(lmax, k0, &[radius], &vacuum_sphere(epsilon)))
169 .collect::<Result<Vec<_>>>()
170 })?
171 .into_iter()
172 .unzip();
173 let plan = TranslationPlan::between(&modes, &modes, true)?;
174 let coupling = coupling_matrix(spheres.len(), modes_per_particle, |i, j| {
175 let displacement = std::array::from_fn(|axis| positions[i][axis] - positions[j][axis]);
176 plan.evaluate(Complex::new(k0, 0.0), displacement, Radial::Singular)
177 })?;
178 Ok(ClusterParts {
179 blocks,
180 spheres,
181 plan,
182 coupling,
183 })
184}
185
186fn coupling_matrix(
189 particles: usize,
190 modes_per_particle: usize,
191 pair: impl Fn(usize, usize) -> Result<Vec<Complex>> + Sync,
192) -> Result<DMatrix<Complex>> {
193 let dimension = modes_per_particle
194 .checked_mul(particles)
195 .ok_or_else(|| Error::InvalidInput("cluster is too large".into()))?;
196 let mut coupling = numerics::zeros(dimension, dimension)?;
197 crate::threads::install(|| {
199 coupling
200 .as_mut_slice()
201 .par_chunks_mut(dimension * modes_per_particle)
202 .enumerate()
203 .try_for_each(|(j, columns)| -> Result<()> {
204 for i in 0..particles {
205 if i == j {
206 continue;
207 }
208 let values = pair(i, j)?;
209 for col in 0..modes_per_particle {
210 let start = col * dimension + i * modes_per_particle;
211 columns[start..start + modes_per_particle].copy_from_slice(
212 &values[col * modes_per_particle..(col + 1) * modes_per_particle],
213 );
214 }
215 }
216 Ok(())
217 })
218 })?;
219 Ok(coupling)
220}
221
222impl SphereClusterResidual {
223 pub fn state_size(lmax: usize, particles: usize) -> Result<usize> {
225 if particles == 0 {
226 return Err(invalid_state());
227 }
228 let sphere_size = SphereResidual::state_size(lmax, 1)?;
229 let modes = lmax
230 .checked_add(2)
231 .and_then(|value| value.checked_mul(lmax))
232 .and_then(|value| value.checked_mul(2))
233 .ok_or_else(invalid_state)?;
234 let particle_size = sphere_size.checked_add(24).ok_or_else(invalid_state)?;
235 let interaction_size = InteractionResidual::state_size_uniform(modes, particles)?;
236 particles
237 .checked_mul(particle_size)
238 .and_then(|size| size.checked_add(24))
239 .and_then(|size| size.checked_add(interaction_size))
240 .ok_or_else(invalid_state)
241 }
242
243 #[must_use]
245 pub fn particles(&self) -> usize {
246 self.spheres.len()
247 }
248
249 #[must_use]
251 pub const fn value(&self) -> &DMatrix<Complex> {
252 self.interaction.value()
253 }
254
255 #[must_use]
257 pub fn shape(&self) -> (usize, usize) {
258 self.interaction.shape()
259 }
260
261 pub fn pushforward(
264 &self,
265 k0: f64,
266 radii: &[f64],
267 epsilon: &[Complex],
268 positions: &[[f64; 3]],
269 ) -> Result<DMatrix<Complex>> {
270 let n = self.spheres.len();
271 if !k0.is_finite()
272 || radii.len() != n
273 || epsilon.len() != n
274 || positions.len() != n
275 || radii.iter().any(|r| !r.is_finite())
276 || epsilon.iter().any(|&z| !finite(z))
277 || positions.iter().flatten().any(|x| !x.is_finite())
278 {
279 return Err(Error::InvalidInput(
280 "sphere tangents must be finite and match the recorded cluster".into(),
281 ));
282 }
283 let blocks = crate::threads::install(|| {
284 self.spheres
285 .par_iter()
286 .zip(radii)
287 .zip(epsilon)
288 .map(|((sphere, &radius), &epsilon)| {
289 let zero = Material {
290 epsilon: Complex::default(),
291 mu: Complex::default(),
292 kappa: Complex::default(),
293 };
294 sphere.pushforward(k0, &[radius], &[Material { epsilon, ..zero }, zero])
295 })
296 .collect::<Result<Vec<_>>>()
297 })?;
298 let modes_per_particle = self.shape().0 / n;
299 let coupling = coupling_matrix(n, modes_per_particle, |i, j| {
300 let displacement =
301 std::array::from_fn(|axis| self.positions[i][axis] - self.positions[j][axis]);
302 let tangent = std::array::from_fn(|axis| positions[i][axis] - positions[j][axis]);
303 self.plan.pushforward(
304 Complex::new(self.k0, 0.0),
305 displacement,
306 Radial::Singular,
307 tangent,
308 Complex::new(k0, 0.0),
309 )
310 })?;
311 self.interaction.pushforward_blocks(&blocks, &coupling)
312 }
313
314 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<SphereClusterGradient> {
322 let InteractionGradient { local, coupling } =
323 self.interaction.pullback_blocks(cotangent)?;
324 let n = self.spheres.len();
325 let modes_per_particle = coupling.nrows() / n;
326 let (k0, plan, positions) = (self.k0, &self.plan, &self.positions);
327 let parts = crate::threads::install(|| {
330 self.spheres
331 .par_iter()
332 .zip(local)
333 .enumerate()
334 .map(|(i, (sphere, local))| {
335 let gradient = sphere.pullback(&local)?;
336 let mut block = DMatrix::zeros(modes_per_particle, modes_per_particle);
337 let pairs = (0..n)
338 .filter(|&j| j != i)
339 .map(|j| {
340 block.copy_from(&coupling.view(
341 (i * modes_per_particle, j * modes_per_particle),
342 (modes_per_particle, modes_per_particle),
343 ));
344 let displacement =
345 std::array::from_fn(|axis| positions[i][axis] - positions[j][axis]);
346 plan.pullback(
347 Complex::new(k0, 0.0),
348 displacement,
349 Radial::Singular,
350 block.as_slice(),
351 )
352 .map(|(position, k)| (position, k.re))
353 })
354 .collect::<Result<Vec<_>>>()?;
355 Ok((gradient, pairs))
356 })
357 .collect::<Result<Vec<_>>>()
358 })?;
359 let mut result = SphereClusterGradient {
360 k0: 0.0,
361 radii: vec![0.0; n],
362 epsilon: vec![Complex::default(); n],
363 positions: vec![[0.0; 3]; n],
364 };
365 for (i, (sphere, pairs)) in parts.into_iter().enumerate() {
366 result.radii[i] = sphere.layers.radii[0];
367 result.epsilon[i] = sphere.layers.epsilon[0];
368 result.k0 += sphere.k0;
369 for (j, (gradient, gk)) in (0..n).filter(|&j| j != i).zip(pairs) {
370 result.k0 += gk;
371 for (axis, g) in gradient.into_iter().enumerate() {
372 result.positions[i][axis] += g;
373 result.positions[j][axis] -= g;
374 }
375 }
376 }
377 Ok(result)
378 }
379}
380
381fn invalid_state() -> Error {
382 Error::InvalidInput("invalid native sphere-cluster state".into())
383}
384
385impl SavedState for SphereClusterResidual {
386 fn save_state(&self) -> Result<Vec<u8>> {
387 let lmax = self.spheres.first().ok_or_else(invalid_state)?.lmax();
388 let particles = self.particles();
389 let mut writer = Writer::new(Self::state_size(lmax, particles)?);
390 writer.usize(lmax);
391 writer.usize(particles);
392 writer.f64(self.k0);
393 for position in &self.positions {
394 for &coordinate in position {
395 writer.f64(coordinate);
396 }
397 }
398 for sphere in &self.spheres {
399 sphere.write_state(&mut writer);
400 }
401 self.interaction.write_state(&mut writer);
402 Ok(writer.finish())
403 }
404
405 fn from_state(bytes: &[u8]) -> Result<Self> {
406 let mut reader = Reader::new(bytes);
407 let lmax = reader.usize()?;
408 let particles = reader.usize()?;
409 if bytes.len() != Self::state_size(lmax, particles)? {
411 return Err(invalid_state());
412 }
413 let k0 = reader.f64()?;
414 if !k0.is_finite() || k0 <= 0.0 {
415 return Err(invalid_state());
416 }
417 let positions = (0..particles)
418 .map(|_| {
419 let position = [reader.f64()?, reader.f64()?, reader.f64()?];
420 if position.iter().any(|value| !value.is_finite()) {
421 return Err(invalid_state());
422 }
423 Ok(position)
424 })
425 .collect::<Result<Vec<_>>>()?;
426 let spheres = (0..particles)
427 .map(|_| {
428 let sphere = SphereResidual::read_state(&mut reader)?;
429 if sphere.lmax() != lmax || sphere.boundaries() != 1 {
430 return Err(invalid_state());
431 }
432 Ok(sphere)
433 })
434 .collect::<Result<Vec<_>>>()?;
435 let interaction = InteractionResidual::read_state(&mut reader)?;
436 let modes_per_particle = 2 * lmax * (lmax + 2);
437 if interaction.local_shapes().len() != particles
438 || interaction
439 .local_shapes()
440 .any(|shape| shape != (modes_per_particle, modes_per_particle))
441 {
442 return Err(invalid_state());
443 }
444 reader.finish()?;
445 let modes = sw::modes(u32::try_from(lmax).map_err(|_| invalid_state())?)?;
448 let plan = TranslationPlan::between(&modes, &modes, true)?;
449 Ok(Self {
450 k0,
451 positions,
452 plan,
453 spheres,
454 interaction,
455 })
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 #![allow(clippy::float_cmp)] use super::*;
464
465 #[test]
466 fn saved_state_preserves_value_and_both_derivatives() -> Result<()> {
467 let cluster = sphere_cluster(
468 1,
469 0.9,
470 &[0.25, 0.3],
471 &[Complex::new(2.1, 0.04), Complex::new(1.7, 0.02)],
472 &[[0.0, 0.0, 0.0], [1.3, 0.2, -0.1]],
473 )?;
474 let bytes = cluster.save_state()?;
475 assert_eq!(bytes.len(), SphereClusterResidual::state_size(1, 2)?);
476 let restored = SphereClusterResidual::from_state(&bytes)?;
477 assert_eq!(cluster.value(), restored.value());
478 assert_eq!(restored.save_state()?, bytes);
479
480 let tangent = |residual: &SphereClusterResidual| {
481 residual.pushforward(
482 0.12,
483 &[0.04, -0.02],
484 &[Complex::new(0.13, 0.01), Complex::new(-0.04, 0.02)],
485 &[[0.01, -0.03, 0.02], [-0.02, 0.04, 0.01]],
486 )
487 };
488 assert_eq!(tangent(&cluster)?, tangent(&restored)?);
489 let cotangent = cluster
490 .value()
491 .map(|value| value + Complex::new(0.03, 0.07));
492 let original = cluster.pullback(&cotangent)?;
493 let roundtrip = restored.pullback(&cotangent)?;
494 assert_eq!(original.k0, roundtrip.k0);
495 assert_eq!(original.radii, roundtrip.radii);
496 assert_eq!(original.epsilon, roundtrip.epsilon);
497 assert_eq!(original.positions, roundtrip.positions);
498
499 assert!(SphereClusterResidual::from_state(&bytes[..bytes.len() - 1]).is_err());
500 let mut extra = bytes;
501 extra.push(0);
502 assert!(SphereClusterResidual::from_state(&extra).is_err());
503 Ok(())
504 }
505}