1use super::reduction::{self, Matrix};
4use crate::SpatialError;
5use crate::numeric::{f64_f32, i64_f64, rounded_i64};
6use molframe_core::structure::UnitCell;
7
8const ORTHOGONAL_ANGLE_TOLERANCE: f64 = 1.0e-10;
9
10const MAX_SEARCH_REACH: f64 = 16.0;
12
13#[derive(Clone, Copy, PartialEq, Debug)]
15pub struct PeriodicBox {
16 basis: [[f64; 3]; 3],
17 inverse: [[f64; 3]; 3],
18 orthogonal: bool,
19 reduction: Reduction,
20}
21
22#[derive(Clone, Copy, PartialEq, Debug)]
27struct Reduction {
28 transform: [[i32; 3]; 3],
29 inverse_transform: [[i32; 3]; 3],
30}
31
32struct ReducedFrame {
34 basis: Matrix,
35 inverse: Matrix,
36 row_norms: [f64; 3],
37}
38
39impl Reduction {
40 const IDENTITY: Self = Self {
41 transform: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
42 inverse_transform: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
43 };
44
45 fn new(basis: Matrix) -> Self {
46 let reduced = reduction::reduce(basis);
47 reduction::narrow(reduced.transform).map_or(
48 Self::IDENTITY,
49 |(transform, inverse_transform)| Self {
50 transform,
51 inverse_transform,
52 },
53 )
54 }
55
56 fn frame(&self, basis: Matrix, inverse: Matrix) -> ReducedFrame {
57 let to_f64 = |matrix: [[i32; 3]; 3]| matrix.map(|row| row.map(f64::from));
58 let reduced = multiply_matrices(basis, to_f64(self.transform));
59 let reduced_inverse = multiply_matrices(to_f64(self.inverse_transform), inverse);
60 ReducedFrame {
61 basis: reduced,
62 inverse: reduced_inverse,
63 row_norms: reduced_inverse.map(|row| squared(row).sqrt()),
64 }
65 }
66
67 fn original_shift(&self, shift: [i64; 3]) -> [i64; 3] {
69 std::array::from_fn(|row| {
70 (0..3).fold(0_i64, |total, column| {
71 total.saturating_add(
72 i64::from(self.transform[row][column]).saturating_mul(shift[column]),
73 )
74 })
75 })
76 }
77}
78
79fn small_integer(value: f64) -> i32 {
80 match i32::try_from(rounded_i64(value)) {
81 Ok(converted) => converted,
82 Err(_) => 0,
83 }
84}
85
86fn multiply_matrices(left: Matrix, right: Matrix) -> Matrix {
87 std::array::from_fn(|row| {
88 std::array::from_fn(|column| (0..3).map(|k| left[row][k] * right[k][column]).sum())
89 })
90}
91
92#[derive(Clone, Copy, PartialEq, Debug)]
94pub struct PeriodicImage {
95 pub displacement: [f32; 3],
97 pub lattice_shift: [i64; 3],
99}
100
101impl PeriodicBox {
102 pub fn from_cell(cell: UnitCell) -> Result<Self, SpatialError> {
109 validate_cell(&cell)?;
110
111 let orthogonal = cell
112 .angles
113 .iter()
114 .all(|angle| (*angle - 90.0).abs() < ORTHOGONAL_ANGLE_TOLERANCE);
115
116 let basis = basis_from_cell(&cell)?;
117 let Some(inverse) = inverse3(basis) else {
118 return Err(SpatialError::InvalidCell);
119 };
120
121 let reduction = Reduction::new(basis);
122
123 Ok(Self {
124 basis,
125 inverse,
126 orthogonal,
127 reduction,
128 })
129 }
130
131 #[must_use]
138 pub fn displacement(&self, left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
139 self.minimum_image(left, right).displacement
140 }
141
142 #[must_use]
147 pub fn minimum_image(&self, left: [f32; 3], right: [f32; 3]) -> PeriodicImage {
148 let (best, lattice_shift) = self.image_f64(left.map(f64::from), right.map(f64::from));
149
150 PeriodicImage {
151 displacement: best.map(f64_f32),
152 lattice_shift,
153 }
154 }
155
156 #[must_use]
159 pub fn displacement_f64(&self, left: [f64; 3], right: [f64; 3]) -> [f64; 3] {
160 self.image_f64(left, right).0
161 }
162
163 fn image_f64(&self, left: [f64; 3], right: [f64; 3]) -> ([f64; 3], [i64; 3]) {
164 let cartesian = cartesian_delta(left, right);
165 if self.orthogonal {
166 let fractional = multiply(self.inverse, cartesian);
167 orthogonal_image(self.basis, cartesian, fractional)
168 } else {
169 triclinic_image(&self.reduction, self.basis, self.inverse, cartesian)
170 }
171 }
172
173 #[must_use]
175 pub fn distance_squared(&self, left: [f32; 3], right: [f32; 3]) -> f32 {
176 let displacement = self.displacement(left, right);
177
178 displacement[0] * displacement[0]
179 + displacement[1] * displacement[1]
180 + displacement[2] * displacement[2]
181 }
182
183 #[must_use]
185 pub fn fractional(&self, position: [f32; 3]) -> [f64; 3] {
186 multiply(self.inverse, position.map(f64::from))
187 }
188
189 #[must_use]
191 pub fn cartesian(&self, fractional: [f64; 3]) -> [f32; 3] {
192 multiply(self.basis, fractional).map(f64_f32)
193 }
194
195 #[must_use]
197 pub fn wrap(&self, position: [f32; 3]) -> [f32; 3] {
198 let fractional = self.fractional(position).map(|value| value.rem_euclid(1.0));
199
200 self.cartesian(fractional)
201 }
202
203 #[must_use]
208 pub fn interpolate(&self, left: [f32; 3], right: [f32; 3], amount: f64) -> Option<[f32; 3]> {
209 if !amount.is_finite() {
210 return None;
211 }
212 let displacement = self.minimum_image(left, right).displacement;
213 let point = std::array::from_fn(|axis| {
214 f64_f32(f64::from(left[axis]) + amount * f64::from(displacement[axis]))
215 });
216 Some(self.wrap(point))
217 }
218
219 #[must_use]
224 pub(crate) fn fractional_cutoff_bounds(&self, cutoff: f32) -> [f64; 3] {
225 self.inverse
226 .map(|row| squared(row).sqrt() * f64::from(cutoff))
227 }
228
229 #[must_use]
237 pub fn contains_cutoff_sphere(&self, cutoff: f32) -> bool {
238 self.fractional_cutoff_bounds(cutoff)
239 .iter()
240 .all(|bound| *bound * 2.0 <= 1.0)
241 }
242
243 pub(crate) fn translated(
245 &self,
246 point: [f32; 3],
247 shift: [i64; 3],
248 ) -> Result<[f32; 3], SpatialError> {
249 let convert = |value| i64_f64(value).ok_or(SpatialError::NumericRangeExceeded);
250 let translation = multiply(
251 self.basis,
252 [convert(shift[0])?, convert(shift[1])?, convert(shift[2])?],
253 );
254 Ok(std::array::from_fn(|axis| {
255 f64_f32(f64::from(point[axis]) + translation[axis])
256 }))
257 }
258}
259
260fn validate_cell(cell: &UnitCell) -> Result<(), SpatialError> {
264 let finite = cell
265 .lengths
266 .iter()
267 .chain(&cell.angles)
268 .all(|value| value.is_finite());
269
270 let positive = cell.lengths.iter().all(|length| *length > 0.0);
271
272 if finite && positive {
273 Ok(())
274 } else {
275 Err(SpatialError::InvalidCell)
276 }
277}
278
279fn basis_from_cell(cell: &UnitCell) -> Result<[[f64; 3]; 3], SpatialError> {
286 let [a, b, c] = cell.lengths;
287 let [alpha, beta, gamma] = cell.angles.map(f64::to_radians);
288
289 let sin_gamma = gamma.sin();
290
291 if !sin_gamma.is_finite() || sin_gamma.abs() <= f64::EPSILON {
292 return Err(SpatialError::InvalidCell);
293 }
294
295 let cos_gamma = gamma.cos();
296 let cos_beta = beta.cos();
297 let cos_alpha = alpha.cos();
298
299 let cx = c * cos_beta;
300 let cy = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma;
301 let cz_squared = c.mul_add(c, -cx.mul_add(cx, cy * cy));
302
303 if !cz_squared.is_finite() || cz_squared <= f64::EPSILON {
304 return Err(SpatialError::InvalidCell);
305 }
306
307 Ok([
308 [a, b * cos_gamma, cx],
309 [0.0, b * sin_gamma, cy],
310 [0.0, 0.0, cz_squared.sqrt()],
311 ])
312}
313
314#[inline]
318fn cartesian_delta(left: [f64; 3], right: [f64; 3]) -> [f64; 3] {
319 [right[0] - left[0], right[1] - left[1], right[2] - left[2]]
320}
321
322fn orthogonal_image(
327 basis: [[f64; 3]; 3],
328 cartesian: [f64; 3],
329 fractional: [f64; 3],
330) -> ([f64; 3], [i64; 3]) {
331 let rounded = fractional.map(f64::round);
332 let wrapped = [
333 fractional[0] - rounded[0],
334 fractional[1] - rounded[1],
335 fractional[2] - rounded[2],
336 ];
337
338 let candidate = multiply(basis, wrapped);
339
340 if squared(candidate) < squared(cartesian) {
341 (candidate, integer_shift(rounded))
342 } else {
343 (cartesian, [0; 3])
344 }
345}
346
347fn triclinic_image(
357 reduction: &Reduction,
358 basis: Matrix,
359 inverse: Matrix,
360 cartesian: [f64; 3],
361) -> ([f64; 3], [i64; 3]) {
362 let frame = reduction.frame(basis, inverse);
363 let fractional = multiply(frame.inverse, cartesian);
364 let centre = fractional.map(f64::round);
365 let base = [
366 fractional[0] - centre[0],
367 fractional[1] - centre[1],
368 fractional[2] - centre[2],
369 ];
370
371 let rounded = multiply(frame.basis, base);
372 let radius = squared(rounded).sqrt();
373 if !radius.is_finite() {
374 return (cartesian, [0; 3]);
375 }
376
377 let reach = frame.row_norms.map(|norm| {
378 let bound = (2.0 * radius * norm).mul_add(1.0 + 1.0e-9, 1.0e-9).ceil();
379 small_integer(bound.clamp(0.0, MAX_SEARCH_REACH))
380 });
381
382 let mut best = cartesian;
383 let mut best_squared = squared(best);
384 let mut best_shift = [0; 3];
385
386 for first in -reach[0]..=reach[0] {
387 for second in -reach[1]..=reach[1] {
388 for third in -reach[2]..=reach[2] {
389 let offset = [first, second, third].map(f64::from);
390 let image = [
391 base[0] - offset[0],
392 base[1] - offset[1],
393 base[2] - offset[2],
394 ];
395 let candidate = multiply(frame.basis, image);
396 let candidate_squared = squared(candidate);
397
398 if candidate_squared < best_squared {
399 best = candidate;
400 best_squared = candidate_squared;
401 best_shift = reduction.original_shift([
402 rounded_i64(centre[0]).saturating_add(i64::from(first)),
403 rounded_i64(centre[1]).saturating_add(i64::from(second)),
404 rounded_i64(centre[2]).saturating_add(i64::from(third)),
405 ]);
406 }
407 }
408 }
409 }
410
411 (best, best_shift)
412}
413
414fn integer_shift(shift: [f64; 3]) -> [i64; 3] {
415 shift.map(rounded_i64)
416}
417
418#[inline]
422fn multiply(matrix: [[f64; 3]; 3], vector: [f64; 3]) -> [f64; 3] {
423 [
424 matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
425 matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
426 matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2],
427 ]
428}
429
430#[inline]
434fn squared(vector: [f64; 3]) -> f64 {
435 vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]
436}
437
438fn cofactors3(matrix: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
442 [
443 [
444 matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1],
445 matrix[1][2] * matrix[2][0] - matrix[1][0] * matrix[2][2],
446 matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0],
447 ],
448 [
449 matrix[0][2] * matrix[2][1] - matrix[0][1] * matrix[2][2],
450 matrix[0][0] * matrix[2][2] - matrix[0][2] * matrix[2][0],
451 matrix[0][1] * matrix[2][0] - matrix[0][0] * matrix[2][1],
452 ],
453 [
454 matrix[0][1] * matrix[1][2] - matrix[0][2] * matrix[1][1],
455 matrix[0][2] * matrix[1][0] - matrix[0][0] * matrix[1][2],
456 matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0],
457 ],
458 ]
459}
460
461fn inverse3(matrix: [[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
466 let cofactors = cofactors3(matrix);
467
468 let determinant = matrix[0][0] * cofactors[0][0]
469 + matrix[0][1] * cofactors[0][1]
470 + matrix[0][2] * cofactors[0][2];
471
472 if !determinant.is_finite() || determinant.abs() <= f64::EPSILON {
473 return None;
474 }
475
476 let reciprocal = determinant.recip();
477
478 Some([
479 [
480 cofactors[0][0] * reciprocal,
481 cofactors[1][0] * reciprocal,
482 cofactors[2][0] * reciprocal,
483 ],
484 [
485 cofactors[0][1] * reciprocal,
486 cofactors[1][1] * reciprocal,
487 cofactors[2][1] * reciprocal,
488 ],
489 [
490 cofactors[0][2] * reciprocal,
491 cofactors[1][2] * reciprocal,
492 cofactors[2][2] * reciprocal,
493 ],
494 ])
495}
496
497#[cfg(test)]
498#[path = "periodic_tests.rs"]
499mod tests;