1#![allow(clippy::indexing_slicing)] use nalgebra::DMatrix;
8
9use super::{InteractionGradient, InteractionResidual, interaction_blocks};
10use crate::sw;
11use crate::{
12 Complex, Error, Result,
13 basis::validate_wavenumbers,
14 saved::{Reader, SavedState, Writer},
15};
16
17#[derive(Debug)]
18enum ParticleExpansion {
19 Spherical(sw::ExpansionResidual),
20 Cylindrical(crate::cw::ExpansionResidual),
21}
22
23#[derive(Debug)]
27pub struct ParticleClusterResidual {
28 expansion: ParticleExpansion,
29 interaction: InteractionResidual,
30}
31
32fn invalid_state() -> Error {
33 Error::InvalidInput("invalid native particle-cluster state".into())
34}
35
36fn framed_state_size(expansion: usize, interaction: usize) -> Result<usize> {
39 17_usize
40 .checked_add(expansion)
41 .and_then(|size| size.checked_add(interaction))
42 .ok_or_else(invalid_state)
43}
44
45#[derive(Debug)]
49pub struct ParticleClusterGradient {
50 pub local: Vec<DMatrix<Complex>>,
52 pub positions: Vec<[f64; 3]>,
54 pub ks: [Complex; 2],
56}
57
58pub fn particle_cluster(
66 local: Vec<DMatrix<Complex>>,
67 basis: sw::Basis,
68 ks: [Complex; 2],
69 helicity: bool,
70) -> Result<ParticleClusterResidual> {
71 basis.validate()?;
72 validate_particle_blocks(
73 &local,
74 &basis.positions,
75 basis.modes.iter().map(|&(p, _)| p),
76 )?;
77 let (coupling, expansion) = sw::expansion(
78 basis.clone(),
79 basis,
80 ks,
81 helicity,
82 crate::special::Radial::Singular,
83 )?;
84 couple(local, coupling, ParticleExpansion::Spherical(expansion))
85}
86
87fn couple(
91 local: Vec<DMatrix<Complex>>,
92 coupling: DMatrix<Complex>,
93 expansion: ParticleExpansion,
94) -> Result<ParticleClusterResidual> {
95 Ok(ParticleClusterResidual {
96 expansion,
97 interaction: interaction_blocks(local, coupling)?,
98 })
99}
100
101fn validate_particle_blocks(
102 local: &[DMatrix<Complex>],
103 positions: &[[f64; 3]],
104 mut pidxs: impl ExactSizeIterator<Item = usize>,
105) -> Result<()> {
106 if local.len() != positions.len()
107 || local.iter().map(DMatrix::nrows).sum::<usize>() != pidxs.len()
108 {
109 return Err(Error::InvalidInput(
110 "one local matrix and mode block required per position".into(),
111 ));
112 }
113 for (i, block) in local.iter().enumerate() {
114 if pidxs.by_ref().take(block.nrows()).any(|p| p != i)
115 || positions[..i].contains(&positions[i])
116 {
117 return Err(Error::InvalidInput(
118 "particle modes must be grouped at distinct positions".into(),
119 ));
120 }
121 }
122 Ok(())
123}
124
125#[allow(clippy::float_cmp)] pub fn cylindrical_particle_cluster(
133 local: Vec<DMatrix<Complex>>,
134 basis: crate::cw::Basis,
135 ks: [Complex; 2],
136 helicity: bool,
137) -> Result<ParticleClusterResidual> {
138 basis.validate()?;
139 validate_particle_blocks(
140 &local,
141 &basis.positions,
142 basis.modes.iter().map(|&(p, _)| p),
143 )?;
144 validate_wavenumbers(ks, helicity, false)?;
146 for (i, position) in basis.positions.iter().enumerate() {
147 if basis.positions[..i].iter().any(|p| p[..2] == position[..2]) {
148 return Err(Error::InvalidInput(
149 "cylinders require distinct transverse positions".into(),
150 ));
151 }
152 }
153 let (coupling, expansion) =
154 crate::cw::expansion(basis.clone(), basis, ks, crate::special::Radial::Singular)?;
155 couple(local, coupling, ParticleExpansion::Cylindrical(expansion))
156}
157
158impl ParticleClusterResidual {
159 pub fn spherical_state_size(local_sizes: &[usize]) -> Result<usize> {
161 let (expansion, interaction) = Self::section_sizes(local_sizes, false)?;
162 framed_state_size(expansion, interaction)
163 }
164
165 pub fn cylindrical_state_size(local_sizes: &[usize]) -> Result<usize> {
167 let (expansion, interaction) = Self::section_sizes(local_sizes, true)?;
168 framed_state_size(expansion, interaction)
169 }
170
171 fn section_sizes(local_sizes: &[usize], cylindrical: bool) -> Result<(usize, usize)> {
172 let interaction = InteractionResidual::state_size(local_sizes)?;
173 let modes = local_sizes.iter().try_fold(0_usize, |sum, &size| {
174 sum.checked_add(size).ok_or_else(invalid_state)
175 })?;
176 let positions = local_sizes.len();
177 let expansion = if cylindrical {
178 crate::cw::ExpansionResidual::state_size(modes, positions, modes, positions)?
179 } else {
180 sw::ExpansionResidual::state_size(modes, positions, modes, positions)?
181 };
182 Ok((expansion, interaction))
183 }
184
185 #[must_use = "iterate over the recorded particle shapes"]
187 pub fn local_shapes(&self) -> impl ExactSizeIterator<Item = (usize, usize)> {
188 self.interaction.local_shapes()
189 }
190
191 #[must_use]
193 pub const fn value(&self) -> &DMatrix<Complex> {
194 self.interaction.value()
195 }
196
197 #[must_use]
199 pub fn shape(&self) -> (usize, usize) {
200 self.interaction.shape()
201 }
202
203 pub fn pushforward(
207 &self,
208 local: &[DMatrix<Complex>],
209 positions: &[[f64; 3]],
210 ks: [Complex; 2],
211 ) -> Result<DMatrix<Complex>> {
212 let coupling = match &self.expansion {
213 ParticleExpansion::Spherical(expansion) => {
214 expansion.pushforward(positions, positions, ks)?
215 }
216 ParticleExpansion::Cylindrical(expansion) => {
217 expansion.pushforward(positions, positions, ks)?
218 }
219 };
220 self.interaction.pushforward_blocks(local, &coupling)
221 }
222
223 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<ParticleClusterGradient> {
232 let InteractionGradient { local, coupling } =
233 self.interaction.pullback_blocks(cotangent)?;
234 let geometry = match &self.expansion {
235 ParticleExpansion::Spherical(expansion) => expansion.pullback(&coupling)?,
236 ParticleExpansion::Cylindrical(expansion) => expansion.pullback(&coupling)?,
237 };
238 let positions = geometry
239 .destination
240 .into_iter()
241 .zip(geometry.source)
242 .map(|(a, b)| std::array::from_fn(|i| a[i] + b[i]))
243 .collect();
244 Ok(ParticleClusterGradient {
245 local,
246 positions,
247 ks: geometry.ks,
248 })
249 }
250}
251
252impl SavedState for ParticleClusterResidual {
253 fn save_state(&self) -> Result<Vec<u8>> {
254 let cylindrical = matches!(&self.expansion, ParticleExpansion::Cylindrical(_));
255 let local_sizes = self
256 .local_shapes()
257 .map(|(size, _)| size)
258 .collect::<Vec<_>>();
259 let (expansion, interaction) = Self::section_sizes(&local_sizes, cylindrical)?;
260 let mut writer = Writer::new(framed_state_size(expansion, interaction)?);
261 writer.byte(u8::from(cylindrical));
262 writer.usize(expansion);
263 writer.usize(interaction);
264 match &self.expansion {
265 ParticleExpansion::Spherical(expansion) => expansion.write_state(&mut writer),
266 ParticleExpansion::Cylindrical(expansion) => expansion.write_state(&mut writer),
267 }
268 self.interaction.write_state(&mut writer);
269 Ok(writer.finish())
270 }
271
272 fn from_state(bytes: &[u8]) -> Result<Self> {
273 let mut reader = Reader::new(bytes);
274 let kind = reader.byte()?;
275 if kind > 1 {
276 return Err(invalid_state());
277 }
278 let expansion_size = reader.usize()?;
279 let interaction_size = reader.usize()?;
280 if bytes.len() != framed_state_size(expansion_size, interaction_size)? {
281 return Err(invalid_state());
282 }
283 let expansion_bytes = reader.raw(expansion_size)?;
284 let interaction_bytes = reader.raw(interaction_size)?;
285 reader.finish()?;
286 let (expansion, shape) = if kind == 0 {
287 let residual = sw::ExpansionResidual::from_state(expansion_bytes)?;
288 let shape = residual.shape();
289 (ParticleExpansion::Spherical(residual), shape)
290 } else {
291 let residual = crate::cw::ExpansionResidual::from_state(expansion_bytes)?;
292 let shape = residual.shape();
293 (ParticleExpansion::Cylindrical(residual), shape)
294 };
295 let interaction = InteractionResidual::from_state(interaction_bytes)?;
296 if shape != interaction.shape() {
297 return Err(invalid_state());
298 }
299 Ok(Self {
300 expansion,
301 interaction,
302 })
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::test_support::patterned;
310
311 fn fixture(cylindrical: bool) -> ParticleClusterResidual {
312 let positions = vec![[0.1, -0.2, 0.3], [1.7, 0.4, -0.5]];
313 let ks = [Complex::new(1.1, 0.05), Complex::new(1.4, 0.08)];
314 let local = vec![
315 patterned(1, 1, 0.2) * Complex::from(0.02),
316 patterned(2, 2, 0.3) * Complex::from(0.03),
317 ];
318 let labels = [(0, 0), (1, 0), (1, 1)];
319 if cylindrical {
320 let modes = labels
321 .map(|(p, pol)| {
322 (
323 p,
324 crate::cw::Mode {
325 kz: 0.2,
326 m: i32::from(pol),
327 pol,
328 },
329 )
330 })
331 .to_vec();
332 cylindrical_particle_cluster(local, crate::cw::Basis { modes, positions }, ks, true)
333 .unwrap()
334 } else {
335 let modes = labels
336 .map(|(p, pol)| {
337 (
338 p,
339 sw::Mode {
340 l: 1,
341 m: i32::from(pol),
342 pol,
343 },
344 )
345 })
346 .to_vec();
347 particle_cluster(local, sw::Basis { modes, positions }, ks, true).unwrap()
348 }
349 }
350
351 #[test]
352 fn saved_state_preserves_particle_blocks_values_and_derivatives() {
353 let local = [patterned(1, 1, 0.4), patterned(2, 2, 0.5)];
354 let positions = [[0.1, 0.2, -0.3], [-0.2, 0.1, 0.4]];
355 let ks = [Complex::new(0.2, -0.1), Complex::new(-0.15, 0.05)];
356 let weight = patterned(3, 3, 0.6);
357 for cylindrical in [false, true] {
358 let original = fixture(cylindrical);
359 let bytes = original.save_state().unwrap();
360 let expected_size = if cylindrical {
361 ParticleClusterResidual::cylindrical_state_size(&[1, 2])
362 } else {
363 ParticleClusterResidual::spherical_state_size(&[1, 2])
364 }
365 .unwrap();
366 assert_eq!(bytes.len(), expected_size);
367 let restored = ParticleClusterResidual::from_state(&bytes).unwrap();
368 assert_eq!(
369 restored.local_shapes().collect::<Vec<_>>(),
370 [(1, 1), (2, 2)]
371 );
372 assert_eq!(restored.value(), original.value());
373 let tangent = original.pushforward(&local, &positions, ks).unwrap();
374 assert_eq!(
375 restored.pushforward(&local, &positions, ks).unwrap(),
376 tangent
377 );
378 let actual = restored.pullback(&weight).unwrap();
379 let expected = original.pullback(&weight).unwrap();
380 assert_eq!(actual.local, expected.local);
381 assert_eq!(actual.positions, expected.positions);
382 assert_eq!(actual.ks, expected.ks);
383 assert_eq!(
384 restored.pushforward(&local, &positions, ks).unwrap(),
385 tangent
386 );
387 assert_eq!(restored.save_state().unwrap(), bytes);
388 }
389 }
390
391 #[test]
392 fn saved_state_rejects_inconsistent_section_sizes() {
393 let bytes = fixture(false).save_state().unwrap();
394 for length in [0, 1, 16, bytes.len() - 1] {
395 assert!(ParticleClusterResidual::from_state(&bytes[..length]).is_err());
396 }
397 let mut extra = bytes.clone();
398 extra.push(0);
399 assert!(ParticleClusterResidual::from_state(&extra).is_err());
400 let mut overflow = bytes;
401 overflow[1..9].copy_from_slice(&u64::MAX.to_le_bytes());
402 assert!(ParticleClusterResidual::from_state(&overflow).is_err());
403 }
404}