1#![allow(clippy::indexing_slicing)] #![allow(clippy::float_cmp)] use crate::{
14 Complex, Error, MAX_DEGREE, Result,
15 basis::ModeLabel,
16 cw,
17 numerics::{self, finite, label_bits},
18 saved::{Reader, SavedState, Writer, invalid},
19 special::{index, ladder, pol_index, wigner_d, wigner_small_d_matrix},
20 sw,
21};
22use nalgebra::DMatrix;
23use std::collections::{BTreeMap, BTreeSet, HashMap};
24
25fn coupled<K: Eq + std::hash::Hash>(rows: &[K], columns: &[K]) -> Vec<(usize, usize)> {
28 let mut groups: HashMap<&K, Vec<usize>> = HashMap::new();
29 for (i, key) in rows.iter().enumerate() {
30 groups.entry(key).or_default().push(i);
31 }
32 columns
33 .iter()
34 .enumerate()
35 .flat_map(|(j, key)| groups.get(key).into_iter().flatten().map(move |&i| (i, j)))
36 .collect()
37}
38
39fn sw_keys(modes: &[(usize, sw::Mode)]) -> Vec<(usize, i32, u8)> {
40 modes
41 .iter()
42 .map(|&(pidx, mode)| (pidx, mode.l, mode.pol))
43 .collect()
44}
45
46fn cw_keys(modes: &[(usize, cw::Mode)]) -> Vec<(usize, u64, i32, u8)> {
47 modes
48 .iter()
49 .map(|&(pidx, mode)| (pidx, label_bits(mode.kz), mode.m, mode.pol))
50 .collect()
51}
52
53#[derive(Debug)]
55struct Spherical {
56 degrees: Vec<i32>,
58 tables: BTreeMap<i32, DMatrix<f64>>,
59 phases: [Vec<Complex>; 2],
61}
62
63#[derive(Debug)]
69pub struct RotationResidual {
70 rows: Vec<i32>,
71 columns: Vec<i32>,
72 entries: Vec<(usize, usize)>,
74 spherical: Option<Spherical>,
75 value: DMatrix<Complex>,
76}
77
78pub fn sw_rotation(
83 destination: &sw::Basis,
84 source: &sw::Basis,
85 angles: [f64; 3],
86) -> Result<RotationResidual> {
87 destination.validate()?;
88 source.validate()?;
89 if angles.iter().any(|a| !a.is_finite()) {
90 return Err(Error::InvalidInput("rotation angles must be finite".into()));
91 }
92 let mut tables = BTreeMap::new();
93 for &(_, mode) in &destination.modes {
94 if let std::collections::btree_map::Entry::Vacant(e) = tables.entry(mode.l) {
95 e.insert(wigner_small_d_matrix(mode.l, angles[1])?);
96 }
97 }
98 let entries = coupled(&sw_keys(&destination.modes), &sw_keys(&source.modes));
99 let orders = |basis: &sw::Basis| -> Vec<_> { basis.modes.iter().map(|(_, m)| m.m).collect() };
100 let (rows, columns) = (orders(destination), orders(source));
101 let phase = |m: i32, angle: f64| (-Complex::i() * f64::from(m) * angle).exp();
102 let spherical = Spherical {
103 degrees: destination.modes.iter().map(|(_, m)| m.l).collect(),
104 tables,
105 phases: [
106 rows.iter().map(|&mu| phase(mu, angles[0])).collect(),
107 columns.iter().map(|&m| phase(m, angles[2])).collect(),
108 ],
109 };
110 let mut value = numerics::zeros(rows.len(), columns.len())?;
111 for &(i, j) in &entries {
112 let l = spherical.degrees[i];
113 let d = spherical.tables[&l][(index(l, rows[i]), index(l, columns[j]))];
114 value[(i, j)] = spherical.phases[0][i] * spherical.phases[1][j] * d;
115 }
116 Ok(RotationResidual {
117 rows,
118 columns,
119 entries,
120 spherical: Some(spherical),
121 value,
122 })
123}
124
125pub fn cw_rotation(
130 destination: &cw::Basis,
131 source: &cw::Basis,
132 angles: [f64; 3],
133) -> Result<RotationResidual> {
134 destination.validate()?;
135 source.validate()?;
136 if angles.iter().any(|a| !a.is_finite()) || angles[1] != 0.0 {
137 return Err(Error::InvalidInput(
138 "cylindrical rotation requires finite angles and theta=0".into(),
139 ));
140 }
141 let entries = coupled(&cw_keys(&destination.modes), &cw_keys(&source.modes));
142 let orders = |basis: &cw::Basis| -> Vec<_> { basis.modes.iter().map(|(_, m)| m.m).collect() };
143 let (rows, columns) = (orders(destination), orders(source));
144 let mut value = numerics::zeros(rows.len(), columns.len())?;
145 for &(i, j) in &entries {
146 value[(i, j)] = (-Complex::i() * f64::from(rows[i]) * (angles[0] + angles[2])).exp();
147 }
148 Ok(RotationResidual {
149 rows,
150 columns,
151 entries,
152 spherical: None,
153 value,
154 })
155}
156
157impl RotationResidual {
158 pub fn sw_state_size(
160 destination: &[(usize, sw::Mode)],
161 source: &[(usize, sw::Mode)],
162 ) -> Result<usize> {
163 for &(_, mode) in destination.iter().chain(source) {
164 mode.validate()?;
165 }
166 let degrees = destination.iter().map(|(_, mode)| mode.l).collect();
167 state_size(
168 destination.len(),
169 source.len(),
170 coupled_count(&sw_keys(destination), &sw_keys(source))?,
171 Some(°rees),
172 )
173 }
174
175 pub fn cw_state_size(
177 destination: &[(usize, cw::Mode)],
178 source: &[(usize, cw::Mode)],
179 ) -> Result<usize> {
180 for &(_, mode) in destination.iter().chain(source) {
181 mode.validate()?;
182 }
183 state_size(
184 destination.len(),
185 source.len(),
186 coupled_count(&cw_keys(destination), &cw_keys(source))?,
187 None,
188 )
189 }
190
191 #[must_use]
194 pub const fn value(&self) -> &DMatrix<Complex> {
195 &self.value
196 }
197
198 #[must_use]
200 pub fn shape(&self) -> (usize, usize) {
201 self.value.shape()
202 }
203
204 pub fn pushforward(&self, angles: [f64; 3]) -> Result<DMatrix<Complex>> {
207 if angles.iter().any(|a| !a.is_finite()) {
208 return Err(Error::InvalidInput(
209 "rotation tangents must be finite".into(),
210 ));
211 }
212 let mut result = numerics::zeros(self.shape().0, self.shape().1)?;
213 for &(i, j) in &self.entries {
214 let (mu, m) = (self.rows[i], self.columns[j]);
215 result[(i, j)] = -Complex::i()
216 * self.value[(i, j)]
217 * (f64::from(mu) * angles[0] + f64::from(m) * angles[2])
218 + self.theta_derivative(i, j) * angles[1];
219 }
220 Ok(result)
221 }
222
223 fn theta_derivative(&self, i: usize, j: usize) -> Complex {
225 let Some(s) = &self.spherical else {
226 return Complex::default();
227 };
228 let (mu, m) = (self.rows[i], self.columns[j]);
229 let l = s.degrees[i];
230 let d = &s.tables[&l];
231 let column = index(l, m);
232 let raised = if mu < l {
233 ladder(l, mu) * d[(index(l, mu + 1), column)]
234 } else {
235 0.0
236 };
237 let lowered = if mu > -l {
238 ladder(l, mu - 1) * d[(index(l, mu - 1), column)]
239 } else {
240 0.0
241 };
242 s.phases[0][i] * s.phases[1][j] * (raised - lowered)
243 }
244
245 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<[f64; 3]> {
247 if cotangent.shape() != self.shape() || cotangent.iter().any(|&z| !finite(z)) {
248 return Err(Error::InvalidInput("invalid rotation cotangent".into()));
249 }
250 let mut result = [0.0; 3];
251 for &(i, j) in &self.entries {
252 let (mu, m) = (self.rows[i], self.columns[j]);
253 let g = cotangent[(i, j)].conj();
254 let paired = (g * (-Complex::i()) * self.value[(i, j)]).re;
256 result[0] += f64::from(mu) * paired;
257 result[2] += f64::from(m) * paired;
258 result[1] += (g * self.theta_derivative(i, j)).re;
259 }
260 Ok(result)
261 }
262}
263
264fn coupled_count<K: Eq + std::hash::Hash>(rows: &[K], columns: &[K]) -> Result<usize> {
265 let mut counts = HashMap::new();
266 for key in rows {
267 *counts.entry(key).or_insert(0_usize) += 1;
268 }
269 columns.iter().try_fold(0_usize, |total, key| {
270 total
271 .checked_add(*counts.get(key).unwrap_or(&0))
272 .ok_or_else(invalid)
273 })
274}
275
276fn bytes(count: usize, stride: usize) -> Result<usize> {
277 count.checked_mul(stride).ok_or_else(invalid)
278}
279
280fn table_dimension(degree: i32) -> Result<usize> {
281 if !(1..=MAX_DEGREE).contains(°ree) {
282 return Err(invalid());
283 }
284 usize::try_from(2 * degree + 1).map_err(|_| invalid())
285}
286
287fn state_size(
288 rows: usize,
289 columns: usize,
290 entries: usize,
291 degrees: Option<&BTreeSet<i32>>,
292) -> Result<usize> {
293 let labels = rows.checked_add(columns).ok_or_else(invalid)?;
294 let matrix = bytes(bytes(rows, columns)?, 16)?;
295 let mut parts = vec![25, bytes(labels, 4)?, bytes(entries, 16)?, matrix];
296 if let Some(degrees) = degrees {
297 parts.extend([bytes(rows, 4)?, bytes(labels, 16)?, 8]);
298 for °ree in degrees {
299 let dimension = table_dimension(degree)?;
300 parts.extend([4, bytes(bytes(dimension, dimension)?, 8)?]);
301 }
302 }
303 parts.into_iter().try_fold(0_usize, |total, part| {
304 total.checked_add(part).ok_or_else(invalid)
305 })
306}
307
308fn check_state_layout(state: &[u8]) -> Result<()> {
311 let mut reader = Reader::new(state);
312 let spherical = match reader.byte()? {
313 0 => false,
314 1 => true,
315 _ => return Err(invalid()),
316 };
317 let (rows, columns, entries) = (reader.usize()?, reader.usize()?, reader.usize()?);
318 let labels = rows.checked_add(columns).ok_or_else(invalid)?;
319 reader.raw(bytes(labels, 4)?)?;
320 reader.raw(bytes(entries, 16)?)?;
321 reader.raw(bytes(bytes(rows, columns)?, 16)?)?;
322 if spherical {
323 reader.raw(bytes(rows, 4)?)?;
324 reader.raw(bytes(labels, 16)?)?;
325 for _ in 0..reader.count(4)? {
326 let dimension = table_dimension(reader.i32()?)?;
327 reader.raw(bytes(bytes(dimension, dimension)?, 8)?)?;
328 }
329 }
330 reader.finish()
331}
332
333impl SavedState for RotationResidual {
334 fn save_state(&self) -> Result<Vec<u8>> {
335 let degrees = self
336 .spherical
337 .as_ref()
338 .map(|s| s.tables.keys().copied().collect());
339 let mut writer = Writer::new(state_size(
340 self.rows.len(),
341 self.columns.len(),
342 self.entries.len(),
343 degrees.as_ref(),
344 )?);
345 writer.byte(u8::from(self.spherical.is_some()));
346 writer.usize(self.rows.len());
347 writer.usize(self.columns.len());
348 writer.usize(self.entries.len());
349 for &order in self.rows.iter().chain(&self.columns) {
350 writer.i32(order);
351 }
352 for &(row, column) in &self.entries {
353 writer.usize(row);
354 writer.usize(column);
355 }
356 for &value in &self.value {
357 writer.complex(value);
358 }
359 if let Some(s) = &self.spherical {
360 for °ree in &s.degrees {
361 writer.i32(degree);
362 }
363 for &phase in s.phases.iter().flatten() {
364 writer.complex(phase);
365 }
366 writer.usize(s.tables.len());
367 for (°ree, table) in &s.tables {
368 writer.i32(degree);
369 for &value in table {
370 writer.f64(value);
371 }
372 }
373 }
374 Ok(writer.finish())
375 }
376
377 fn from_state(state: &[u8]) -> Result<Self> {
378 check_state_layout(state)?;
379 let mut reader = Reader::new(state);
380 let spherical = reader.byte()? == 1;
381 let (nrows, ncolumns, nentries) = (reader.usize()?, reader.usize()?, reader.usize()?);
382 let rows = read_orders(&mut reader, nrows)?;
383 let columns = read_orders(&mut reader, ncolumns)?;
384 let entries = (0..nentries)
385 .map(|_| {
386 let (i, j) = (reader.usize()?, reader.usize()?);
387 if i >= nrows || j >= ncolumns {
388 return Err(invalid());
389 }
390 Ok((i, j))
391 })
392 .collect::<Result<Vec<_>>>()?;
393 let mut value = numerics::zeros(nrows, ncolumns)?;
394 for z in &mut value {
395 *z = reader.complex()?;
396 }
397 let spherical = if spherical {
398 let degrees = (0..nrows)
399 .map(|_| reader.i32())
400 .collect::<Result<Vec<_>>>()?;
401 let mut phases = [Vec::with_capacity(nrows), Vec::with_capacity(ncolumns)];
402 for (phase, length) in phases.iter_mut().zip([nrows, ncolumns]) {
403 for _ in 0..length {
404 phase.push(reader.complex()?);
405 }
406 }
407 let mut tables = BTreeMap::new();
408 for _ in 0..reader.usize()? {
409 let degree = reader.i32()?;
410 let dimension = table_dimension(degree)?;
411 let values = (0..dimension * dimension)
412 .map(|_| reader.f64())
413 .collect::<Result<Vec<_>>>()?;
414 let table = DMatrix::from_vec(dimension, dimension, values);
415 if tables.insert(degree, table).is_some() {
416 return Err(invalid());
417 }
418 }
419 if degrees
420 .iter()
421 .zip(&rows)
422 .any(|(l, m)| !tables.contains_key(l) || m.abs() > *l)
423 || entries.iter().any(|&(i, j)| columns[j].abs() > degrees[i])
424 {
425 return Err(invalid());
426 }
427 Some(Spherical {
428 degrees,
429 tables,
430 phases,
431 })
432 } else {
433 None
434 };
435 reader.finish()?;
436 Ok(Self {
437 rows,
438 columns,
439 entries,
440 spherical,
441 value,
442 })
443 }
444}
445
446fn read_orders(reader: &mut Reader<'_>, count: usize) -> Result<Vec<i32>> {
447 (0..count)
448 .map(|_| {
449 let order = reader.i32()?;
450 if order.unsigned_abs() > MAX_DEGREE.unsigned_abs() {
451 return Err(invalid());
452 }
453 Ok(order)
454 })
455 .collect()
456}
457
458#[inline]
470pub fn sw_rotate(
471 destination: sw::Mode,
472 source: sw::Mode,
473 [phi, theta, psi]: [f64; 3],
474) -> Result<Complex> {
475 destination.validate()?;
476 source.validate()?;
477 if destination.l != source.l || destination.pol != source.pol {
478 Ok(Complex::default())
479 } else {
480 wigner_d(
481 destination.l,
482 destination.m,
483 source.m,
484 [phi.into(), theta.into(), psi.into()],
485 )
486 }
487}
488
489#[inline]
501pub fn cw_rotate(kz: f64, mu: i64, p: i64, qz: f64, m: i64, q: i64, phi: f64) -> Result<Complex> {
502 let p = pol_index(p)?;
503 let q = pol_index(q)?;
504 let bound = u64::from(MAX_DEGREE.unsigned_abs());
505 if !kz.is_finite()
506 || !qz.is_finite()
507 || !phi.is_finite()
508 || mu.unsigned_abs() > bound
509 || m.unsigned_abs() > bound
510 {
511 return Err(Error::InvalidInput(
512 "finite axial/angle arguments and |orders| <= 128 required".into(),
514 ));
515 }
516 if kz != qz || mu != m || p != q {
517 Ok(Complex::default())
518 } else {
519 let order =
520 f64::from(i32::try_from(m).map_err(|_| Error::InvalidInput("invalid order".into()))?);
521 let (sin, cos) = (-order * phi).sin_cos();
522 Ok(Complex::new(cos, sin))
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 fn check_saved_rotation(residual: &RotationResidual, size: usize) {
531 let state = residual.save_state().unwrap();
532 assert_eq!(state.len(), size);
533 let restored = RotationResidual::from_state(&state).unwrap();
534 assert_eq!(restored.save_state().unwrap(), state);
535 assert_eq!(restored.value(), residual.value());
536 let direction = [0.3, -0.4, 0.2];
537 assert_eq!(
538 restored.pushforward(direction).unwrap(),
539 residual.pushforward(direction).unwrap()
540 );
541 let cotangent = crate::test_support::patterned(residual.shape().0, residual.shape().1, 0.3);
542 assert_eq!(
543 restored.pullback(&cotangent).unwrap(),
544 residual.pullback(&cotangent).unwrap()
545 );
546 for length in 0..state.len() {
547 assert!(RotationResidual::from_state(&state[..length]).is_err());
548 }
549 let mut malformed = state.clone();
550 malformed[1..9].copy_from_slice(&u64::MAX.to_le_bytes());
551 assert!(RotationResidual::from_state(&malformed).is_err());
552 malformed = state;
553 malformed.push(0);
554 assert!(RotationResidual::from_state(&malformed).is_err());
555 }
556
557 #[test]
558 fn saved_spherical_rotation_preserves_sparse_blocks_and_derivatives() {
559 let basis = |labels: &[(usize, i32, i32, u8)]| sw::Basis {
560 modes: labels
561 .iter()
562 .map(|&(position, l, m, pol)| (position, sw::Mode { l, m, pol }))
563 .collect(),
564 positions: vec![[0.0; 3], [1.0, 0.0, 0.0]],
565 };
566 let destination = basis(&[(0, 1, 0, 0), (1, 2, 2, 1), (0, 3, -2, 0)]);
567 let source = basis(&[(0, 1, 1, 0), (1, 2, -1, 1), (0, 3, 1, 1), (0, 1, -1, 0)]);
568 let residual = sw_rotation(&destination, &source, [0.3, 0.8, -0.4]).unwrap();
569 let size = RotationResidual::sw_state_size(&destination.modes, &source.modes).unwrap();
570 check_saved_rotation(&residual, size);
571 let state = residual.save_state().unwrap();
572 let restored = RotationResidual::from_state(&state).unwrap();
573 assert_eq!(restored.entries, residual.entries);
574 let spherical = restored.spherical.unwrap();
575 assert_eq!(
576 spherical.tables.keys().copied().collect::<Vec<_>>(),
577 [1, 2, 3]
578 );
579 assert_eq!(spherical.tables[&3].shape(), (7, 7));
580 }
581
582 #[test]
583 fn saved_cylindrical_rotation_preserves_sparse_couplings_and_derivatives() {
584 let basis = |labels: &[(usize, f64, i32, u8)]| cw::Basis {
585 modes: labels
586 .iter()
587 .map(|&(position, kz, m, pol)| (position, cw::Mode { kz, m, pol }))
588 .collect(),
589 positions: vec![[0.0; 3]],
590 };
591 let destination = basis(&[(0, 0.0, -1, 0), (0, 0.2, 2, 1)]);
592 let source = basis(&[(0, -0.0, -1, 0), (0, 0.2, 1, 1), (0, 0.2, 2, 1)]);
593 let residual = cw_rotation(&destination, &source, [0.3, 0.0, -0.4]).unwrap();
594 let size = RotationResidual::cw_state_size(&destination.modes, &source.modes).unwrap();
595 check_saved_rotation(&residual, size);
596 let restored = RotationResidual::from_state(&residual.save_state().unwrap()).unwrap();
597 assert_eq!(restored.entries, [(0, 0), (1, 2)]);
598 assert!(restored.spherical.is_none());
599 }
600}