1#![allow(clippy::indexing_slicing)] use crate::{Error, Result, numerics};
10use nalgebra::{Matrix3, Vector3};
11use std::{
12 f64::consts::TAU,
13 ops::{Add, Mul, Sub},
14};
15
16#[inline]
20pub fn volume<T: Copy + Add<Output = T> + Sub<Output = T> + Mul<Output = T>>(
21 a: [[T; 3]; 3],
22 dim: usize,
23) -> Result<T> {
24 match dim {
25 1 => Ok(a[0][0]),
26 2 => Ok(a[0][0] * a[1][1] - a[0][1] * a[1][0]),
27 3 => Ok(a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
28 + a[0][1] * (a[1][2] * a[2][0] - a[1][0] * a[2][2])
29 + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])),
30 _ => Err(Error::InvalidInput(
31 "cell dimension must be 1, 2 or 3".into(),
32 )),
33 }
34}
35
36pub fn reciprocal(a: [[f64; 3]; 3], dim: usize) -> Result<[[f64; 3]; 3]> {
40 if !(1..=3).contains(&dim)
41 || a[..dim]
42 .iter()
43 .any(|r| r[..dim].iter().any(|v| !v.is_finite()))
44 {
45 return Err(Error::InvalidInput(
46 "require finite lattice vectors in dimension 1, 2 or 3".into(),
47 ));
48 }
49 let matrix = Matrix3::from_fn(|i, j| {
50 if i < dim && j < dim {
51 a[i][j]
52 } else {
53 f64::from(i == j)
54 }
55 });
56 let inverse = matrix
57 .try_inverse()
58 .ok_or_else(|| Error::InvalidInput("lattice vectors must be independent".into()))?;
59 let result = inverse.transpose() * TAU;
60 if result.iter().any(|v| !v.is_finite()) {
61 return Err(Error::InvalidInput(
62 "lattice is numerically singular".into(),
63 ));
64 }
65 Ok(std::array::from_fn(|i| {
66 std::array::from_fn(|j| result[(i, j)])
67 }))
68}
69
70#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub(crate) fn reduce_basis(rows: [[f64; 3]; 3], dim: usize) -> Option<[[[i64; 3]; 3]; 2]> {
82 const LIMIT: i64 = 1 << 20;
83 struct Basis {
85 v: [[f64; 3]; 3],
86 u: [[i64; 3]; 3],
87 w: [[i64; 3]; 3],
88 }
89 fn norm(v: [f64; 3]) -> f64 {
90 v.iter().map(|x| x * x).sum()
91 }
92 impl Basis {
93 fn shorten(&mut self, k: usize, terms: &[(usize, i64)]) -> Option<bool> {
95 let mut candidate = self.v[k];
96 for &(i, c) in terms {
97 if c.abs() > LIMIT {
98 return None;
99 }
100 for (x, y) in candidate.iter_mut().zip(self.v[i]) {
101 *x += c as f64 * y;
102 }
103 }
104 if norm(candidate) >= (1.0 - 1e-8) * norm(self.v[k]) {
105 return Some(false);
106 }
107 for &(i, c) in terms {
109 for j in 0..3 {
110 self.u[k][j] += c * self.u[i][j];
111 self.w[i][j] -= c * self.w[k][j];
112 }
113 }
114 if self
115 .u
116 .iter()
117 .chain(&self.w)
118 .flatten()
119 .any(|x| x.abs() > LIMIT)
120 {
121 return None;
122 }
123 self.v[k] = candidate;
124 Some(true)
125 }
126 }
127 let identity: [[i64; 3]; 3] =
128 std::array::from_fn(|i| std::array::from_fn(|j| i64::from(i == j)));
129 let mut basis = Basis {
130 v: std::array::from_fn(|i| {
131 std::array::from_fn(|j| if i < dim && j < dim { rows[i][j] } else { 0.0 })
132 }),
133 u: identity,
134 w: identity,
135 };
136 for _ in 0..64 {
137 let mut changed = false;
138 for k in 0..dim {
139 for i in (0..dim).filter(|&i| i != k) {
140 let (a, b) = (basis.v[k], basis.v[i]);
141 let mu = (a.iter().zip(b).map(|(x, y)| x * y).sum::<f64>() / norm(b)).round();
142 if !mu.is_finite() || mu.abs() > LIMIT as f64 {
143 return None;
144 }
145 if mu != 0.0 {
146 changed |= basis.shorten(k, &[(i, -mu as i64)])?;
147 }
148 }
149 if dim == 3 {
150 let (i, j) = ((k + 1) % 3, (k + 2) % 3);
151 for (s, t) in [(1, 1), (1, -1), (-1, 1), (-1, -1)] {
152 changed |= basis.shorten(k, &[(i, s), (j, t)])?;
153 }
154 }
155 }
156 if !changed {
157 break;
158 }
159 }
160 (basis.u != identity).then_some([basis.u, basis.w])
161}
162
163pub fn cube(dim: usize, n: i64, edge: bool) -> Result<Vec<i64>> {
168 if !(1..=3).contains(&dim) || n < 0 {
169 return Err(Error::InvalidInput(
170 "cube requires dimension 1, 2 or 3 and nonnegative size".into(),
171 ));
172 }
173 let count = usize::try_from(n)
174 .ok()
175 .and_then(|n| n.checked_mul(2))
176 .and_then(|n| n.checked_add(1))
177 .and_then(|side| {
178 side.checked_pow(u32::try_from(dim).ok()?).map(|outer| {
179 if edge && side > 1 {
180 outer - (side - 2).pow(u32::try_from(dim).unwrap_or_default())
181 } else {
182 outer
183 }
184 })
185 })
186 .and_then(|rows| rows.checked_mul(dim))
187 .ok_or_else(|| Error::InvalidInput("cube output size overflows".into()))?;
188 let mut values = Vec::new();
189 numerics::reserve(&mut values, count)?;
190 visit_cube(dim, n, edge, |point| {
191 values.extend_from_slice(&point[..dim]);
192 Ok(())
193 })?;
194 Ok(values)
195}
196
197pub(crate) fn visit_cube(
199 dim: usize,
200 n: i64,
201 edge: bool,
202 mut visitor: impl FnMut([i64; 3]) -> Result<()>,
203) -> Result<()> {
204 if !(1..=3).contains(&dim) || n < 0 {
205 return Err(Error::InvalidInput(
206 "cube requires dimension 1, 2 or 3 and nonnegative size".into(),
207 ));
208 }
209 fn append(
210 point: &mut [i64; 3],
211 dim: usize,
212 axis: usize,
213 n: i64,
214 edge: bool,
215 boundary: bool,
216 visitor: &mut impl FnMut([i64; 3]) -> Result<()>,
217 ) -> Result<()> {
218 if axis == dim {
219 return visitor(*point);
220 }
221 if edge && !boundary && axis + 1 == dim && n > 0 {
222 for value in [-n, n] {
223 point[axis] = value;
224 append(point, dim, axis + 1, n, edge, true, visitor)?;
225 }
226 } else {
227 for value in -n..=n {
228 point[axis] = value;
229 append(
230 point,
231 dim,
232 axis + 1,
233 n,
234 edge,
235 boundary || value.abs() == n,
236 visitor,
237 )?;
238 }
239 }
240 Ok(())
241 }
242 append(&mut [0; 3], dim, 0, n, edge, false, &mut visitor)
243}
244
245#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn diffraction_orders(b: [[f64; 2]; 2], radius: f64) -> Result<Vec<i64>> {
250 if !radius.is_finite() || b.iter().flatten().any(|v| !v.is_finite()) {
251 return Err(Error::InvalidInput(
252 "diffraction geometry must be finite".into(),
253 ));
254 }
255 let det = b[0][0] * b[1][1] - b[0][1] * b[1][0];
256 let length = b[1][0].hypot(b[1][1]);
257 if det == 0.0 || !det.is_finite() || !length.is_finite() {
258 return Err(Error::InvalidInput(
259 "lattice vectors must be independent".into(),
260 ));
261 }
262 if radius < 0.0 {
263 return Ok(Vec::new());
264 }
265 let bound = (radius * length / det.abs()).ceil();
266 let nbound = radius * b[0][0].hypot(b[0][1]) / det.abs();
267 if !bound.is_finite()
268 || !nbound.is_finite()
269 || bound >= i64::MAX as f64 - 4.0
270 || nbound >= i64::MAX as f64 - 4.0
271 {
272 return Err(Error::InvalidInput(
273 "diffraction order bounds overflow".into(),
274 ));
275 }
276 let mut orders = vec![0, 0];
277 for m in 0..=bound as i64 {
278 let distance = (m as f64 * det / length).abs();
281 let center =
282 -(m as f64) * (b[0][0] * (b[1][0] / length) + b[0][1] * (b[1][1] / length)) / length;
283 let half = ((radius - distance) * (radius + distance)).max(0.0).sqrt() / length;
284 let lower = (center - half).floor() as i64 - 1;
285 let upper = (center + half).ceil() as i64 + 1;
286 let mut emit = |n: i64| -> Result<()> {
287 if m == 0 && n <= 0 {
288 return Ok(());
289 }
290 let x = m as f64 * b[0][0] + n as f64 * b[1][0];
291 let y = m as f64 * b[0][1] + n as f64 * b[1][1];
292 if x.hypot(y) <= radius {
293 orders
295 .try_reserve(4)
296 .map_err(|_| Error::out_of_memory(orders.len() + 4, size_of::<i64>()))?;
297 orders.extend([m, n, -m, -n]);
298 }
299 Ok(())
300 };
301 for n in lower.max(0)..=upper {
302 emit(n)?;
303 }
304 for n in (lower..=upper.min(-1)).rev() {
305 emit(n)?;
306 }
307 }
308 Ok(orders)
309}
310
311pub fn first_brillouin_1d(k: f64, b: f64) -> Result<f64> {
315 if !k.is_finite() || !b.is_finite() || b <= 0.0 {
316 return Err(Error::InvalidInput(
317 "require finite wavevector and positive reciprocal pitch".into(),
318 ));
319 }
320 let mut result = k - b * (k / b).round_ties_even();
321 if result > 0.5 * b {
322 result -= b;
323 }
324 if result <= -0.5 * b {
325 result += b;
326 }
327 if !result.is_finite() {
328 return Err(Error::InvalidInput("wavevector reduction overflow".into()));
329 }
330 Ok(result)
331}
332
333#[allow(clippy::float_cmp)] pub fn first_brillouin(
348 mut k: [f64; 3],
349 b: [[f64; 3]; 3],
350 dim: usize,
351 iterations: usize,
352) -> Result<[f64; 3]> {
353 reciprocal(b, dim)?;
354 if !(2..=3).contains(&dim) || k[..dim].iter().any(|v| !v.is_finite()) {
355 return Err(Error::InvalidInput(
356 "require finite two- or three-dimensional wavevector".into(),
357 ));
358 }
359 k[dim..].fill(0.0);
360 let rows = std::array::from_fn::<_, 3, _>(|i| {
361 Vector3::from_fn(|j, _| if i < dim && j < dim { b[i][j] } else { 0.0 })
362 });
363 let norms = rows.map(|v| v.norm_squared());
364 let tolerance = (1e-13 * norms[0].max(norms[1])).max(1e-14);
368 if dim == 2
369 && [
370 (rows[0] + rows[1]).norm_squared(),
371 (rows[0] - rows[1]).norm_squared(),
372 ]
373 .iter()
374 .any(|&v| v < norms[0] - tolerance || v < norms[1] - tolerance)
375 {
376 return Err(Error::InvalidInput(
377 "lattice vectors are not of minimal length".into(),
378 ));
379 }
380 let neighbours = cube(dim, 1, false)?;
381 for _ in 0..=iterations {
382 let previous = k;
383 let mut point = Vector3::from(k);
384 for j in 0..dim {
385 point -= rows[j] * (point.dot(&rows[j]) / norms[j]).round_ties_even();
386 }
387 let mut best = point;
388 for index in neighbours.chunks_exact(dim) {
389 let mut candidate = point;
390 for j in 0..dim {
391 candidate += rows[j] * f64::from(i32::try_from(index[j]).unwrap_or_default());
392 }
393 if candidate.norm_squared() < best.norm_squared() {
394 best = candidate;
395 }
396 }
397 k = best.into();
398 if k == previous {
399 break;
400 }
401 }
402 Ok(k)
403}
404
405#[cfg(test)]
406mod tests {
407 use std::{f64::consts::TAU, result::Result};
408
409 use proptest::{prelude::*, test_runner::TestCaseError};
410
411 use super::{
412 cube, diffraction_orders, first_brillouin, first_brillouin_1d, reciprocal, reduce_basis,
413 volume,
414 };
415 use crate::test_support::{ALGEBRA_CASES, log_uniform, prop_assert_close, table};
416
417 proptest! {
418 #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
419
420 #[test]
421 fn reciprocal_duality(
422 a in 0.3f64..3.0,
423 b in 0.3f64..3.0,
424 c in 0.3f64..3.0,
425 s in -1.0f64..1.0,
426 dim in 1usize..4,
427 ) {
428 check_reciprocal_duality([[a, s, 0.0], [0.0, b, s], [0.0, 0.0, c]], dim)?;
429 }
430
431 #[test]
432 fn boundary_is_cube_difference(dim in 1usize..4, n in 1i64..8) {
433 check_cube_boundary(dim, n)?;
434 }
435
436 #[test]
437 fn diffraction_orders_are_complete_paired_and_unique(
438 basis in reciprocal_basis(-3.0..3.0, 0.5..2.0),
439 radius in 0.0f64..4.0,
440 ) {
441 check_diffraction_orders(basis, radius)?;
442 }
443
444 #[test]
445 fn brillouin_reduction_is_a_covariant_voronoi_projection(
446 basis in reciprocal_basis(-0.5..0.5, 1.0..2.0),
447 hexagonal in any::<bool>(),
448 k in prop::array::uniform2(-5.0f64..5.0),
449 cells in prop::array::uniform2(-6i32..=6),
450 ) {
451 check_brillouin_2d(basis, hexagonal, k, cells)?;
452 }
453
454 #[test]
455 fn brillouin_reduction_in_three_dimensions_is_periodic(
456 skew in prop::array::uniform3(-0.2f64..0.2),
457 k in prop::array::uniform3(-5.0f64..5.0),
458 cells in prop::array::uniform3(-6i32..=6),
459 ) {
460 check_brillouin_3d(skew, k, cells)?;
461 }
462
463 #[test]
464 fn brillouin_reduction_in_one_dimension_is_periodic(
465 k in -100.0f64..100.0,
466 b in log_uniform(-3.0..8.0),
467 cells in -20i32..=20,
468 ) {
469 check_brillouin_1d(k, b, cells)?;
470 }
471
472 #[test]
473 fn basis_reduction_finds_the_successive_minima(
474 dim in 2usize..4,
475 pitch in prop::array::uniform3(1.0f64..2.0),
476 skew in prop::array::uniform3(-1.0f64..1.0),
477 shear in prop::array::uniform3(-9i64..=9),
478 ) {
479 check_basis_reduction(dim, pitch, skew, shear)?;
480 }
481 }
482
483 fn check_cube_boundary(dim: usize, n: i64) -> Result<(), TestCaseError> {
486 let full = cube(dim, n, false)?;
487 let boundary = cube(dim, n, true)?;
488 let expected: Vec<_> = full
489 .chunks_exact(dim)
490 .filter(|point| point.iter().any(|x| x.abs() == n))
491 .flatten()
492 .copied()
493 .collect();
494 prop_assert_eq!(boundary, expected);
495 Ok(())
496 }
497
498 fn reciprocal_basis(
501 shear: std::ops::Range<f64>,
502 aspect: std::ops::Range<f64>,
503 ) -> impl Strategy<Value = ([[f64; 2]; 2], f64)> {
504 (
505 shear,
506 aspect,
507 0.0..TAU,
508 any::<bool>(),
509 log_uniform(-3.0..8.0),
510 )
511 .prop_map(|(shear, aspect, theta, reflect, scale)| {
512 let (sin, cos) = theta.sin_cos();
513 let flip = if reflect { -1.0 } else { 1.0 };
514 let row = |x: f64, y: f64| {
515 [
516 scale * (cos * x - sin * y),
517 scale * flip * (sin * x + cos * y),
518 ]
519 };
520 ([row(1.0, 0.0), row(shear, aspect)], scale)
521 })
522 }
523
524 fn check_reciprocal_duality(direct: [[f64; 3]; 3], dim: usize) -> Result<(), TestCaseError> {
527 let reciprocal = reciprocal(direct, dim)?;
528 for (i, row) in direct.iter().enumerate().take(dim) {
529 for (j, dual) in reciprocal.iter().enumerate().take(dim) {
530 let dot: f64 = row.iter().zip(dual).take(dim).map(|(a, b)| a * b).sum();
531 let expected = if i == j { TAU } else { 0.0 };
532 prop_assert_close!(dot, expected, 3e-14);
533 }
534 }
535 let involution = super::reciprocal(reciprocal, dim)?;
536 for (row, expected) in involution.iter().zip(direct).take(dim) {
537 for (x, e) in row.iter().zip(expected).take(dim) {
538 prop_assert_close!(*x, e, 1e-14, "{involution:?} {direct:?}");
539 }
540 }
541 let product = volume(direct, dim)? * volume(reciprocal, dim)?;
542 let expected = TAU.powi(i32::try_from(dim).unwrap());
543 prop_assert_close!(product, expected, 1e-14 * expected);
544 Ok(())
545 }
546
547 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] fn check_diffraction_orders(
552 (b, scale): ([[f64; 2]; 2], f64),
553 radius: f64,
554 ) -> Result<(), TestCaseError> {
555 let radius = radius * scale;
556 let orders = diffraction_orders(b, radius)?;
557 prop_assert_eq!(&orders[..2], &[0, 0]);
558 let pairs: Vec<(i64, i64)> = orders
559 .as_chunks::<2>()
560 .0
561 .iter()
562 .map(|&p| p.into())
563 .collect();
564 for pair in pairs[1..].as_chunks::<2>().0 {
565 prop_assert_eq!(pair[1], (-pair[0].0, -pair[0].1));
566 }
567 let set: std::collections::BTreeSet<_> = pairs.iter().copied().collect();
568 prop_assert_eq!(set.len(), pairs.len(), "duplicate orders");
569 let det = (b[0][0] * b[1][1] - b[0][1] * b[1][0]).abs();
570 let bound = |row: [f64; 2]| (radius * row[0].hypot(row[1]) / det).ceil() as i64 + 1;
571 let magnitude = |m: i64, n: i64| {
572 let (m, n) = (m as f64, n as f64);
573 (m * b[0][0] + n * b[1][0]).hypot(m * b[0][1] + n * b[1][1])
574 };
575 let (mmax, nmax) = (bound(b[1]), bound(b[0]));
576 let expected: std::collections::BTreeSet<_> = (-mmax..=mmax)
577 .flat_map(|m| (-nmax..=nmax).map(move |n| (m, n)))
578 .filter(|&(m, n)| magnitude(m, n) <= radius)
579 .collect();
580 prop_assert_eq!(set, expected);
581 Ok(())
582 }
583
584 fn same_reduction(
587 a: [f64; 3],
588 b: [f64; 3],
589 dual: &[[f64; 3]; 3],
590 scale: f64,
591 ) -> Result<(), TestCaseError> {
592 let d: [f64; 3] = std::array::from_fn(|j| a[j] - b[j]);
593 let norm = |v: [f64; 3]| v.iter().map(|x| x * x).sum::<f64>();
594 if norm(d).sqrt() <= 1e-12 * scale {
595 return Ok(());
596 }
597 for row in dual {
598 let cells = row.iter().zip(d).map(|(x, y)| x * y).sum::<f64>() / TAU;
599 prop_assert!(
600 (cells - cells.round()).abs() < 1e-9,
601 "{a:?} and {b:?} differ"
602 );
603 }
604 prop_assert!(
605 (norm(a) - norm(b)).abs() <= 1e-10 * scale * scale,
606 "{a:?} {b:?}"
607 );
608 Ok(())
609 }
610
611 fn check_brillouin_2d(
616 (b, scale): ([[f64; 2]; 2], f64),
617 hexagonal: bool,
618 k: [f64; 2],
619 cells: [i32; 2],
620 ) -> Result<(), TestCaseError> {
621 let b = if hexagonal {
622 let [x, y] = [b[0][0], b[0][1]];
624 let s3 = 3.0_f64.sqrt() / 2.0;
625 let flip = (b[0][0] * b[1][1] - b[0][1] * b[1][0]).signum();
626 [[x, y], [-0.5 * x - flip * s3 * y, -0.5 * y + flip * s3 * x]]
627 } else {
628 b
629 };
630 let rows = [[b[0][0], b[0][1], 0.0], [b[1][0], b[1][1], 0.0], [0.0; 3]];
631 let dual = reciprocal(rows, 2)?;
632 let k = [k[0] * scale, k[1] * scale, 0.0];
633 let result = first_brillouin(k, rows, 2, 10)?;
634 let norm = |v: [f64; 3]| v.iter().map(|x| x * x).sum::<f64>();
635 for m in -1..=1_i32 {
636 for n in -1..=1_i32 {
637 let (m, n) = (f64::from(m), f64::from(n));
638 let neighbour: [f64; 3] =
639 std::array::from_fn(|j| result[j] + m * rows[0][j] + n * rows[1][j]);
640 prop_assert!(
641 norm(result) <= norm(neighbour) * (1.0 + 1e-12),
642 "{result:?} is longer than {neighbour:?}"
643 );
644 }
645 }
646 let shifted: [f64; 3] = std::array::from_fn(|j| {
647 k[j] + f64::from(cells[0]) * rows[0][j] + f64::from(cells[1]) * rows[1][j]
648 });
649 let size = scale * (1.0 + norm(k).sqrt() / scale);
650 same_reduction(first_brillouin(shifted, rows, 2, 10)?, result, &dual, size)?;
651 same_reduction(first_brillouin(result, rows, 2, 10)?, result, &dual, size)?;
652 let unit = first_brillouin(
653 k.map(|x| x / scale),
654 rows.map(|r| r.map(|x| x / scale)),
655 2,
656 10,
657 )?;
658 same_reduction(unit.map(|x| x * scale), result, &dual, size)?;
659 Ok(())
660 }
661
662 fn check_brillouin_3d(
665 skew: [f64; 3],
666 k: [f64; 3],
667 cells: [i32; 3],
668 ) -> Result<(), TestCaseError> {
669 let rows = [
670 [1.0, skew[0], skew[1]],
671 [skew[2], 1.3, skew[0]],
672 [skew[1], skew[2], 1.6],
673 ];
674 let dual = reciprocal(rows, 3)?;
675 let result = first_brillouin(k, rows, 3, 10)?;
676 let shifted: [f64; 3] = std::array::from_fn(|j| {
677 k[j] + (0..3)
678 .map(|i| f64::from(cells[i]) * rows[i][j])
679 .sum::<f64>()
680 });
681 same_reduction(first_brillouin(shifted, rows, 3, 10)?, result, &dual, 10.0)?;
682 same_reduction(first_brillouin(result, rows, 3, 10)?, result, &dual, 10.0)?;
683 Ok(())
684 }
685
686 fn check_brillouin_1d(k: f64, b: f64, cells: i32) -> Result<(), TestCaseError> {
688 let k = k * b;
689 let result = first_brillouin_1d(k, b)?;
690 prop_assert!(
691 -0.5 * b < result && result <= 0.5 * b,
692 "{result} for b = {b}"
693 );
694 let shifted = first_brillouin_1d(k + f64::from(cells) * b, b)?;
695 let difference = (result - shifted).abs();
697 prop_assert!(
698 difference < 1e-12 * b || (difference - b).abs() < 1e-12 * b,
699 "{result} {shifted}"
700 );
701 Ok(())
702 }
703
704 #[test]
705 fn hexagonal_brillouin_zones_are_accepted_at_every_scale() {
706 let s = 3.0_f64.sqrt();
709 for pitch in [0.3, 0.5, 0.8, 1.0, 20.0, 1e-7, 1e9] {
710 let rows = [
711 [pitch, 0.0, 0.0],
712 [0.5 * pitch, 0.5 * s * pitch, 0.0],
713 [0.0; 3],
714 ];
715 let b = reciprocal(rows, 2).unwrap();
716 let k = [0.1 / pitch, 0.2 / pitch, 0.0];
717 assert!(first_brillouin(k, b, 2, 2).is_ok(), "pitch {pitch}");
718 }
719 let skewed = [[1.0, 0.0, 0.0], [0.9, 0.3, 0.0], [0.0; 3]];
720 assert!(first_brillouin([0.1, 0.2, 0.0], skewed, 2, 2).is_err());
721 }
722
723 fn check_basis_reduction(
728 dim: usize,
729 pitch: [f64; 3],
730 skew: [f64; 3],
731 shear: [i64; 3],
732 ) -> Result<(), TestCaseError> {
733 let a = [
734 [pitch[0], 0.0, 0.0],
735 [skew[0], pitch[1], 0.0],
736 [skew[1], skew[2], pitch[2]],
737 ];
738 let identity: [[i64; 3]; 3] =
739 std::array::from_fn(|i| std::array::from_fn(|j| i64::from(i == j)));
740 let shear = [[1, 0, 0], [shear[0], 1, 0], [shear[1], shear[2], 1]];
741 let combine = |u: &[[i64; 3]; 3], rows: &[[f64; 3]; 3]| -> [[f64; 3]; 3] {
742 std::array::from_fn(|i| {
743 std::array::from_fn(|j| {
744 if i < dim && j < dim {
745 (0..dim)
746 .map(|k| f64::from(i32::try_from(u[i][k]).unwrap()) * rows[k][j])
747 .sum()
748 } else {
749 0.0
750 }
751 })
752 })
753 };
754 let norm = |v: &[f64; 3]| v.iter().map(|x| x * x).sum::<f64>();
755 let mut minima = Vec::new();
756 for rows in [combine(&identity, &a), combine(&shear, &a)] {
757 let [u, w] = reduce_basis(rows, dim).unwrap_or([identity; 2]);
758 for (i, row) in u.iter().enumerate() {
759 for (j, dual) in w.iter().enumerate() {
760 let product: i64 = row.iter().zip(dual).map(|(a, b)| a * b).sum();
761 prop_assert_eq!(product, i64::from(i == j), "U W^T = I");
762 }
763 }
764 let v = combine(&u, &rows);
765 for k in 0..dim {
766 for c in [
767 [1, 0],
768 [-1, 0],
769 [0, 1],
770 [0, -1],
771 [1, 1],
772 [1, -1],
773 [-1, 1],
774 [-1, -1],
775 ] {
776 let (i, j) = ((k + 1) % dim, (k + 2) % dim);
777 if dim == 2 && c[1] != 0 {
778 continue;
779 }
780 let other: [f64; 3] = std::array::from_fn(|x| {
781 v[k][x] + f64::from(c[0]) * v[i][x] + f64::from(c[1]) * v[j][x]
782 });
783 prop_assert!(
784 norm(&v[k]) <= (1.0 + 1e-7) * norm(&other),
785 "row {k} of {v:?} shortened by {c:?}"
786 );
787 }
788 }
789 prop_assert!(reduce_basis(v, dim).is_none(), "{v:?} is reduced");
790 let mut lengths: Vec<f64> = v[..dim].iter().map(norm).collect();
791 lengths.sort_by(f64::total_cmp);
792 minima.push(lengths);
793 }
794 for (a, b) in minima[0].iter().zip(&minima[1]) {
795 prop_assert!((a - b).abs() <= 1e-7 * a, "{minima:?}");
796 }
797 Ok(())
798 }
799
800 #[test]
801 fn reduced_bases_are_kept() {
802 let s = 3.0_f64.sqrt();
803 for (rows, dim) in [
804 ([[1.0, 0.0, 0.0], [0.5, 0.5 * s, 0.0], [0.0; 3]], 2),
805 ([[1.0, 0.0, 0.0], [-0.5, 0.5 * s, 0.0], [0.0; 3]], 2),
806 ([[1.6, 0.0, 0.0], [0.3, 1.4, 0.0], [0.0; 3]], 2),
808 (
809 [
810 [1.0, 0.0, 0.0],
811 [0.5, 0.5 * s, 0.0],
812 [0.5, 0.5 / s, (2.0_f64 / 3.0).sqrt()],
813 ],
814 3,
815 ),
816 ([[2.0, 0.0, 0.0], [0.0; 3], [0.0; 3]], 1),
817 ] {
818 assert!(reduce_basis(rows, dim).is_none(), "{rows:?}");
819 }
820 let sheared = [[1.6, 0.0, 0.0], [13.1, 1.4, 0.0], [0.0; 3]];
821 assert_eq!(
822 reduce_basis(sheared, 2),
823 Some([
824 [[1, 0, 0], [-8, 1, 0], [0, 0, 1]],
825 [[1, 8, 0], [0, 1, 0], [0, 0, 1]]
826 ])
827 );
828 }
829
830 #[test]
831 fn diffraction_cutoff_on_an_order_keeps_it() -> crate::Result<()> {
832 let mut failures = Vec::new();
835 for a in [
836 0.1, 0.2, 0.25, 0.3, 0.4, 0.45, 0.5, 0.6, 0.7, 0.8, 1.0, 1.3, 1.5, 2.0,
837 ] {
838 let s = 3.0_f64.sqrt();
839 for b in [
840 [[TAU / a, 0.0], [0.0, TAU / a]],
841 [[TAU / a, 0.0], [0.0, TAU / (1.7 * a)]],
842 [[TAU / a, -TAU / (a * s)], [0.0, 2.0 * TAU / (a * s)]],
843 ] {
844 let magnitude = |m: i32, n: i32| {
845 let (m, n) = (f64::from(m), f64::from(n));
846 (m * b[0][0] + n * b[1][0]).hypot(m * b[0][1] + n * b[1][1])
847 };
848 for (m, n) in [(1, 0), (0, 1), (1, 1), (2, 0), (1, -1), (2, 1)] {
849 let radius = magnitude(m, n);
850 let orders = diffraction_orders(b, radius)?;
851 let set: std::collections::BTreeSet<(i64, i64)> = orders
852 .as_chunks::<2>()
853 .0
854 .iter()
855 .map(|&p| p.into())
856 .collect();
857 let expected: std::collections::BTreeSet<_> = (-8..=8)
858 .flat_map(|m| (-8..=8).map(move |n| (m, n)))
859 .filter(|&(m, n)| magnitude(m, n) <= radius)
860 .map(|(m, n)| (i64::from(m), i64::from(n)))
861 .collect();
862 if set != expected {
863 failures.push((a, b, (m, n)));
864 }
865 }
866 }
867 }
868 assert!(
869 failures.is_empty(),
870 "orders dropped at the cutoff: {failures:?}"
871 );
872 Ok(())
873 }
874
875 #[test]
876 fn cube_matches_lean_model() {
877 let cases = table::<String, i64>(include_str!(concat!(
879 env!("CARGO_MANIFEST_DIR"),
880 "/../../formal/golden/cube.txt"
881 )));
882 let grid: Vec<_> = (1..=3)
885 .flat_map(|dim| (0..=4).flat_map(move |n| [true, false].map(|edge| (dim, n, edge))))
886 .collect();
887 assert_eq!(cases.len(), grid.len());
888 for ((key, expected), (dim, n, edge)) in cases.iter().zip(grid) {
889 assert_eq!(key, &[dim.to_string(), n.to_string(), edge.to_string()]);
890 assert_eq!(&cube(dim, n, edge).unwrap(), expected, "{key:?}");
891 }
892 }
893}