1#![allow(clippy::indexing_slicing)] use super::{Basis, plan::TranslationPlan};
10use crate::{
11 Complex, Error, Result,
12 basis::{ExpansionGradient, LatticeExpansionGradient, validate_wavenumbers},
13 numerics::{self, finite},
14 special::Radial,
15};
16use nalgebra::DMatrix;
17use rayon::prelude::*;
18use std::{collections::HashMap, sync::Arc};
19
20#[path = "expansion_lattice_state.rs"]
21mod lattice_state;
22
23#[derive(Clone, Debug)]
25struct Block {
26 destination: usize,
27 source: usize,
28 displacement: [f64; 3],
30 rows: Vec<usize>,
31 cols: Vec<usize>,
32 polarizations: Vec<u8>,
33 plan: Arc<TranslationPlan>,
34}
35
36impl Block {
37 fn len(&self) -> usize {
38 self.rows.len() * self.cols.len()
39 }
40
41 fn values(
43 shared: bool,
44 evaluate: impl Fn(usize) -> Result<Option<Vec<Complex>>>,
45 ) -> Result<[Option<Vec<Complex>>; 2]> {
46 Ok([evaluate(0)?, if shared { None } else { evaluate(1)? }])
47 }
48
49 fn gather(&self, matrix: &DMatrix<Complex>, pol: Option<usize>) -> Vec<Complex> {
52 let mut values = vec![Complex::default(); self.len()];
53 for (column, (&col, &p)) in values
54 .chunks_exact_mut(self.rows.len())
55 .zip(self.cols.iter().zip(&self.polarizations))
56 {
57 if pol.is_none_or(|pol| usize::from(p) == pol) {
58 for (value, &row) in column.iter_mut().zip(&self.rows) {
59 *value = matrix[(row, col)];
60 }
61 }
62 }
63 values
64 }
65
66 fn scatter(&self, values: &[Complex], pol: Option<usize>, matrix: &mut DMatrix<Complex>) {
69 for (column, (&col, &p)) in values
70 .chunks_exact(self.rows.len())
71 .zip(self.cols.iter().zip(&self.polarizations))
72 {
73 if pol.is_none_or(|pol| usize::from(p) == pol) {
74 for (&value, &row) in column.iter().zip(&self.rows) {
75 matrix[(row, col)] = value;
76 }
77 }
78 }
79 }
80}
81
82fn validate_expansion(
84 destination: &Basis,
85 source: &Basis,
86 ks: [Complex; 2],
87 helicity: bool,
88) -> Result<()> {
89 destination.validate()?;
90 source.validate()?;
91 validate_wavenumbers(ks, helicity, true)
93}
94
95#[derive(Clone, Debug)]
102pub struct ExpansionResidual {
103 destination: Basis,
104 source: Basis,
105 ks: [Complex; 2],
106 radial: Radial,
107 helicity: bool,
108 blocks: Vec<Block>,
109}
110
111pub fn expansion(
117 destination: Basis,
118 source: Basis,
119 ks: [Complex; 2],
120 helicity: bool,
121 radial: Radial,
122) -> Result<(DMatrix<Complex>, ExpansionResidual)> {
123 let residual = ExpansionResidual::prepare(destination, source, ks, helicity, radial)?;
124 let value = assemble(residual.shape(), &residual.blocks, BATCH, |block, _| {
125 Block::values(ks[0] == ks[1], |pol| {
126 expansion_block(&block.plan, ks[pol], block.displacement, radial)
127 })
128 })?;
129 Ok((value, residual))
130}
131
132impl ExpansionResidual {
133 fn prepare(
134 destination: Basis,
135 source: Basis,
136 ks: [Complex; 2],
137 helicity: bool,
138 radial: Radial,
139 ) -> Result<Self> {
140 validate_expansion(&destination, &source, ks, helicity)?;
141 let blocks = blocks(&destination, &source, helicity, false)?;
142 Ok(Self {
143 destination,
144 source,
145 ks,
146 radial,
147 helicity,
148 blocks,
149 })
150 }
151
152 pub fn state_size(
154 destination_modes: usize,
155 destination_positions: usize,
156 source_modes: usize,
157 source_positions: usize,
158 ) -> Result<usize> {
159 let destination = crate::saved::sw_basis_size(destination_modes, destination_positions)?;
160 let source = crate::saved::sw_basis_size(source_modes, source_positions)?;
161 destination
162 .checked_add(source)
163 .and_then(|n| n.checked_add(34))
164 .ok_or_else(crate::saved::invalid)
165 }
166
167 pub(crate) fn write_state(&self, writer: &mut crate::saved::Writer) {
169 crate::saved::write_sw_basis(writer, &self.destination);
170 crate::saved::write_sw_basis(writer, &self.source);
171 for k in self.ks {
172 writer.complex(k);
173 }
174 crate::saved::write_radial(writer, self.radial);
175 writer.byte(u8::from(self.helicity));
176 }
177 #[must_use]
179 pub fn shape(&self) -> (usize, usize) {
180 (self.destination.modes.len(), self.source.modes.len())
181 }
182
183 pub fn pushforward(
185 &self,
186 destination: &[[f64; 3]],
187 source: &[[f64; 3]],
188 ks: [Complex; 2],
189 ) -> Result<DMatrix<Complex>> {
190 crate::basis::validate_expansion_tangent(
191 destination,
192 source,
193 ks,
194 (
195 self.destination.positions.len(),
196 self.source.positions.len(),
197 ),
198 )?;
199 assemble(self.shape(), &self.blocks, BATCH, |block, _| {
200 if self.radial == Radial::Singular && block.displacement.iter().all(|&x| x == 0.0) {
201 return Ok([None, None]);
202 }
203 let direction = std::array::from_fn(|axis| {
204 destination[block.destination][axis] - source[block.source][axis]
205 });
206 Block::values(self.ks[0] == self.ks[1] && ks[0] == ks[1], |pol| {
207 block
208 .plan
209 .pushforward(
210 self.ks[pol],
211 block.displacement,
212 self.radial,
213 direction,
214 ks[pol],
215 )
216 .map(Some)
217 })
218 })
219 }
220
221 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<ExpansionGradient> {
224 if cotangent.shape() != self.shape() || cotangent.iter().any(|&z| !finite(z)) {
225 return Err(Error::InvalidInput("invalid expansion cotangent".into()));
226 }
227 let gradients = crate::threads::install(|| {
229 self.blocks
230 .par_iter()
231 .map(|block| {
232 let mut gradients = [([0.0; 3], Complex::default()); 2];
233 if self.radial == Radial::Singular
234 && block.displacement.iter().all(|&x| x == 0.0)
235 {
236 return Ok(gradients);
237 }
238 for (pol, gradient) in gradients.iter_mut().enumerate() {
239 let g = block.gather(cotangent, Some(pol));
240 if g.iter().any(|&z| z != Complex::default()) {
241 *gradient = block.plan.pullback(
242 self.ks[pol],
243 block.displacement,
244 self.radial,
245 &g,
246 )?;
247 }
248 }
249 Ok(gradients)
250 })
251 .collect::<Result<Vec<_>>>()
252 })?;
253 let mut result = ExpansionGradient::zeros(
254 self.destination.positions.len(),
255 self.source.positions.len(),
256 );
257 for (block, gradients) in self.blocks.iter().zip(gradients) {
258 for (pol, (position, k)) in gradients.into_iter().enumerate() {
259 result.ks[pol] += k;
260 for (axis, grad) in position.into_iter().enumerate() {
261 result.destination[block.destination][axis] += grad;
262 result.source[block.source][axis] -= grad;
263 }
264 }
265 }
266 Ok(result)
267 }
268}
269
270impl crate::saved::SavedState for ExpansionResidual {
271 fn save_state(&self) -> Result<Vec<u8>> {
272 let mut writer = crate::saved::Writer::new(Self::state_size(
273 self.destination.modes.len(),
274 self.destination.positions.len(),
275 self.source.modes.len(),
276 self.source.positions.len(),
277 )?);
278 self.write_state(&mut writer);
279 Ok(writer.finish())
280 }
281
282 fn from_state(bytes: &[u8]) -> Result<Self> {
283 let mut shape = crate::saved::Reader::new(bytes);
284 let (dm, dp) = crate::saved::read_sw_basis_dimensions(&mut shape)?;
285 let (sm, sp) = crate::saved::read_sw_basis_dimensions(&mut shape)?;
286 if bytes.len() != Self::state_size(dm, dp, sm, sp)? {
287 return Err(crate::saved::invalid());
288 }
289 let mut reader = crate::saved::Reader::new(bytes);
290 let destination = crate::saved::read_sw_basis(&mut reader)?;
291 let source = crate::saved::read_sw_basis(&mut reader)?;
292 let ks = [reader.complex()?, reader.complex()?];
293 let radial = crate::saved::read_radial(&mut reader)?;
294 let helicity = match reader.byte()? {
295 0 => false,
296 1 => true,
297 _ => return Err(crate::saved::invalid()),
298 };
299 reader.finish()?;
300 Self::prepare(destination, source, ks, helicity, radial)
302 }
303}
304
305fn blocks(
306 destination: &Basis,
307 source: &Basis,
308 helicity: bool,
309 normalized_harmonics: bool,
310) -> Result<Vec<Block>> {
311 let mut plans = HashMap::new();
312 let (destination_groups, source_groups) = (destination.groups(), source.groups());
313 let mut blocks = Vec::new();
314 numerics::reserve(
316 &mut blocks,
317 destination_groups.len().saturating_mul(source_groups.len()),
318 )?;
319 for (p, (rows, to)) in destination_groups {
320 let position = destination.positions[p];
321 for (&q, (cols, from)) in &source_groups {
322 let key = (to.clone(), from.clone());
323 let plan = if let Some(plan) = plans.get(&key) {
324 Arc::clone(plan)
325 } else {
326 let mut plan = TranslationPlan::between(&to, from, helicity)?;
327 if normalized_harmonics {
328 plan.weights_for_normalized_harmonics();
329 }
330 let plan = Arc::new(plan);
331 plans.insert(key, Arc::clone(&plan));
332 plan
333 };
334 blocks.push(Block {
335 destination: p,
336 source: q,
337 displacement: std::array::from_fn(|a| position[a] - source.positions[q][a]),
338 rows: rows.clone(),
339 cols: cols.clone(),
340 polarizations: from.iter().map(|m| m.pol).collect(),
341 plan,
342 });
343 }
344 }
345 Ok(blocks)
346}
347
348fn expansion_block(
351 plan: &TranslationPlan,
352 k: Complex,
353 displacement: [f64; 3],
354 radial: Radial,
355) -> Result<Option<Vec<Complex>>> {
356 if radial == Radial::Singular && displacement.iter().all(|&x| x == 0.0) {
357 return Ok(None);
358 }
359 plan.evaluate(k, displacement, radial).map(Some)
360}
361
362const BATCH: usize = 1 << 20;
365
366fn batch_count(blocks: &[Block], budget: usize) -> usize {
368 let mut size = 0;
369 blocks
370 .iter()
371 .take_while(|block| {
372 size += block.len();
373 size <= budget
374 })
375 .count()
376 .max(1)
377}
378
379fn assemble(
384 shape: (usize, usize),
385 blocks: &[Block],
386 batch: usize,
387 evaluate: impl Fn(&Block, usize) -> Result<[Option<Vec<Complex>>; 2]> + Sync,
388) -> Result<DMatrix<Complex>> {
389 let mut value = numerics::zeros(shape.0, shape.1)?;
390 let mut rest = blocks;
391 while !rest.is_empty() {
392 let count = batch_count(rest, batch);
393 let (current, next) = rest.split_at(count);
394 rest = next;
395 let workers = crate::threads::current_num_threads().div_ceil(current.len());
396 let values = crate::threads::install(|| {
397 current
398 .par_iter()
399 .map(|block| evaluate(block, workers))
400 .collect::<Result<Vec<_>>>()
401 })?;
402 for (block, [first, second]) in current.iter().zip(values) {
403 if let Some(first) = first {
404 block.scatter(&first, second.as_ref().map(|_| 0), &mut value);
405 }
406 if let Some(second) = second {
407 block.scatter(&second, Some(1), &mut value);
408 }
409 }
410 }
411 Ok(value)
412}
413
414pub fn lattice_expansion(
419 destination: Basis,
420 source: Basis,
421 ks: [Complex; 2],
422 helicity: bool,
423 lattice: crate::lattice::BlochLattice,
424 eta: Complex,
425) -> Result<(DMatrix<Complex>, LatticeExpansionResidual)> {
426 validate_expansion(&destination, &source, ks, helicity)?;
427 let blocks = blocks(&destination, &source, helicity, false)?;
428 let workers = crate::threads::current_num_threads().div_ceil(blocks.len().max(1));
429 let value = assemble(
430 (destination.modes.len(), source.modes.len()),
431 &blocks,
432 BATCH,
433 |block, _| {
434 Block::values(ks[0] == ks[1], |pol| {
435 block
436 .plan
437 .evaluate_periodic(ks[pol], block.displacement, &lattice, eta, workers)
438 .map(Some)
439 })
440 },
441 )?;
442 Ok((
443 value,
444 LatticeExpansionResidual {
445 destination,
446 source,
447 ks,
448 lattice,
449 eta,
450 helicity,
451 blocks,
452 },
453 ))
454}
455
456#[derive(Debug)]
462pub struct LatticeExpansionResidual {
463 destination: Basis,
464 source: Basis,
465 ks: [Complex; 2],
466 lattice: crate::lattice::BlochLattice,
467 eta: Complex,
468 helicity: bool,
469 blocks: Vec<Block>,
470}
471
472impl LatticeExpansionResidual {
473 pub fn pushforward(
476 &self,
477 destination: &[[f64; 3]],
478 source: &[[f64; 3]],
479 ks: [Complex; 2],
480 kpar: &[f64],
481 vectors: &DMatrix<f64>,
482 ) -> Result<DMatrix<Complex>> {
483 crate::basis::validate_expansion_tangent(
484 destination,
485 source,
486 ks,
487 (
488 self.destination.positions.len(),
489 self.source.positions.len(),
490 ),
491 )?;
492 let geometry =
493 crate::lattice::SumTangent::with_lattice(kpar, vectors, self.lattice.dimension())?;
494 assemble(self.shape(), &self.blocks, BATCH, |block, workers| {
495 let shift = std::array::from_fn(|a| {
496 source[block.source][a] - destination[block.destination][a]
497 });
498 let tangents = ks.map(|k| crate::lattice::SumTangent {
499 k,
500 shift,
501 ..geometry
502 });
503 block
504 .plan
505 .pushforward_periodic(
506 self.ks,
507 block.displacement,
508 &self.lattice,
509 self.eta,
510 &tangents,
511 workers,
512 )
513 .map(|values| values.map(Some))
514 })
515 }
516
517 #[must_use]
519 pub fn shape(&self) -> (usize, usize) {
520 (self.destination.modes.len(), self.source.modes.len())
521 }
522
523 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<LatticeExpansionGradient> {
526 if cotangent.shape() != self.shape() || cotangent.iter().any(|&g| !finite(g)) {
527 return Err(Error::InvalidInput(
528 "invalid periodic expansion cotangent".into(),
529 ));
530 }
531 let mut result = LatticeExpansionGradient::zeros(
532 self.destination.positions.len(),
533 self.source.positions.len(),
534 self.lattice.dimension(),
535 );
536 let mut rest = self.blocks.as_slice();
537 while !rest.is_empty() {
538 let (current, next) = rest.split_at(batch_count(rest, BATCH));
539 rest = next;
540 let workers = crate::threads::current_num_threads().div_ceil(current.len());
541 let gradients = crate::threads::install(|| {
542 current
543 .par_iter()
544 .map(|block| {
545 let g = [0, 1].map(|pol| block.gather(cotangent, Some(pol)));
546 block.plan.pullback_periodic(
547 self.ks,
548 block.displacement,
549 &self.lattice,
550 self.eta,
551 &g,
552 workers,
553 )
554 })
555 .collect::<Result<Vec<_>>>()
556 })?;
557 for (block, gradients) in current.iter().zip(gradients) {
558 for (pol, gradient) in gradients.into_iter().enumerate() {
559 result.add_lattice(&gradient);
560 result.expansion.ks[pol] += gradient.k;
561 for (axis, value) in gradient.shift.into_iter().enumerate() {
562 result.expansion.destination[block.destination][axis] += value;
563 result.expansion.source[block.source][axis] -= value;
564 }
565 }
566 }
567 }
568 Ok(result)
569 }
570}
571
572#[derive(Debug)]
576pub struct LatticeExpansionFromTableResidual {
577 destination: Basis,
578 source: Basis,
579 helicity: bool,
580 blocks: Vec<Block>,
581 table_shape: [usize; 4],
582 shape: (usize, usize),
583}
584
585pub fn lattice_expansion_from_table(
597 destination: &Basis,
598 source: &Basis,
599 helicity: bool,
600 channels: usize,
601 table: &[Complex],
602) -> Result<(DMatrix<Complex>, LatticeExpansionFromTableResidual)> {
603 destination.validate()?;
604 source.validate()?;
605 if !(1..=2).contains(&channels) || (!helicity && channels != 1) {
606 return Err(Error::InvalidInput(
607 "lattice table needs one or two channels; parity needs one".into(),
608 ));
609 }
610 let lmax = destination
611 .modes
612 .iter()
613 .map(|(_, m)| m.l)
614 .max()
615 .unwrap_or(0)
616 + source.modes.iter().map(|(_, m)| m.l).max().unwrap_or(0);
617 let harmonics = usize::try_from((lmax + 1).pow(2))
618 .map_err(|_| Error::InvalidInput("invalid harmonic count".into()))?;
619 let table_shape = [
620 destination.positions.len(),
621 source.positions.len(),
622 channels,
623 harmonics,
624 ];
625 let size = table_shape
626 .iter()
627 .try_fold(1_usize, |size, &length| size.checked_mul(length));
628 if size != Some(table.len()) || table.iter().any(|&value| !finite(value)) {
629 return Err(Error::InvalidInput(
630 "lattice table requires matching finite harmonic values".into(),
631 ));
632 }
633 let blocks = blocks(destination, source, helicity, true)?;
634 let shape = (destination.modes.len(), source.modes.len());
635 let residual = LatticeExpansionFromTableResidual {
636 destination: destination.clone(),
637 source: source.clone(),
638 helicity,
639 blocks,
640 table_shape,
641 shape,
642 };
643 Ok((residual.apply_table(table)?, residual))
644}
645
646impl LatticeExpansionFromTableResidual {
647 pub fn pushforward(&self, tangent: &[Complex]) -> Result<DMatrix<Complex>> {
649 if tangent.len() != self.table_shape.iter().product::<usize>()
650 || tangent.iter().any(|&value| !finite(value))
651 {
652 return Err(Error::InvalidInput(
653 "lattice table tangent must be finite and match the table".into(),
654 ));
655 }
656 self.apply_table(tangent)
657 }
658
659 fn apply_table(&self, tangent: &[Complex]) -> Result<DMatrix<Complex>> {
661 let [_, sources, channels, harmonics] = self.table_shape;
662 let mut result = numerics::zeros(self.shape.0, self.shape.1)?;
663 for block in &self.blocks {
664 let offset = (block.destination * sources + block.source) * channels * harmonics;
665 for channel in 0..channels {
666 let table =
667 &tangent[offset + channel * harmonics..offset + (channel + 1) * harmonics];
668 let evaluated = block.plan.evaluate_table(table);
669 block.scatter(&evaluated, (channels == 2).then_some(channel), &mut result);
670 }
671 }
672 if result.iter().any(|&value| !finite(value)) {
673 return Err(Error::NonFinite("lattice table sum overflows".into()));
674 }
675 Ok(result)
676 }
677
678 #[must_use]
680 pub const fn shape(&self) -> (usize, usize) {
681 self.shape
682 }
683
684 #[must_use]
687 pub const fn table_shape(&self) -> [usize; 4] {
688 self.table_shape
689 }
690
691 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<Vec<Complex>> {
694 if cotangent.shape() != self.shape || cotangent.iter().any(|&v| !finite(v)) {
695 return Err(Error::InvalidInput(
696 "invalid lattice table cotangent".into(),
697 ));
698 }
699 let [_, sources, channels, harmonics] = self.table_shape;
700 let mut gradient = vec![Complex::default(); self.table_shape.iter().product()];
701 for block in &self.blocks {
702 let offset = (block.destination * sources + block.source) * channels * harmonics;
703 for channel in 0..channels {
704 let local = block.gather(cotangent, (channels == 2).then_some(channel));
705 block.plan.pullback_table(
706 &local,
707 &mut gradient[offset + channel * harmonics..offset + (channel + 1) * harmonics],
708 );
709 }
710 }
711 Ok(gradient)
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::{Basis, Block, assemble, blocks, expansion, expansion_block};
721 use crate::{Complex, special::Radial, sw};
722
723 #[test]
726 fn batches_assemble_the_same_matrix() {
727 let basis = Basis {
731 modes: [1, 2, 1]
732 .into_iter()
733 .enumerate()
734 .flat_map(|(p, lmax)| {
735 let modes = sw::modes(lmax).unwrap();
736 modes.into_iter().map(move |mode| (p, mode))
737 })
738 .collect(),
739 positions: vec![[0.0; 3], [0.9, 0.2, 0.3], [-0.4, 0.8, -0.5]],
740 };
741 let blocks = blocks(&basis, &basis, true, false).unwrap();
742 let k = Complex::new(1.3, 0.05);
743 for ks in [[k, k], [k, k * 1.4]] {
744 for radial in [Radial::Regular, Radial::Singular] {
745 let (expected, _) =
746 expansion(basis.clone(), basis.clone(), ks, true, radial).unwrap();
747 for batch in [1, 150, 300] {
748 let value = assemble(
749 (basis.modes.len(), basis.modes.len()),
750 &blocks,
751 batch,
752 |block, _| {
753 Block::values(ks[0] == ks[1], |pol| {
754 expansion_block(&block.plan, ks[pol], block.displacement, radial)
755 })
756 },
757 )
758 .unwrap();
759 assert_eq!(value, expected, "batch {batch}, {radial:?}");
760 }
761 }
762 }
763 }
764}