1use crate::SpatialError;
4use crate::numeric::{f64_f32, i64_f64, rounded_i64};
5use molframe_core::structure::UnitCell;
6
7const ORTHOGONAL_ANGLE_TOLERANCE: f64 = 1.0e-10;
8
9const IMAGE_OFFSETS: [[f64; 3]; 27] = [
10 [-1.0, -1.0, -1.0],
11 [-1.0, -1.0, 0.0],
12 [-1.0, -1.0, 1.0],
13 [-1.0, 0.0, -1.0],
14 [-1.0, 0.0, 0.0],
15 [-1.0, 0.0, 1.0],
16 [-1.0, 1.0, -1.0],
17 [-1.0, 1.0, 0.0],
18 [-1.0, 1.0, 1.0],
19 [0.0, -1.0, -1.0],
20 [0.0, -1.0, 0.0],
21 [0.0, -1.0, 1.0],
22 [0.0, 0.0, -1.0],
23 [0.0, 0.0, 0.0],
24 [0.0, 0.0, 1.0],
25 [0.0, 1.0, -1.0],
26 [0.0, 1.0, 0.0],
27 [0.0, 1.0, 1.0],
28 [1.0, -1.0, -1.0],
29 [1.0, -1.0, 0.0],
30 [1.0, -1.0, 1.0],
31 [1.0, 0.0, -1.0],
32 [1.0, 0.0, 0.0],
33 [1.0, 0.0, 1.0],
34 [1.0, 1.0, -1.0],
35 [1.0, 1.0, 0.0],
36 [1.0, 1.0, 1.0],
37];
38
39#[derive(Clone, Copy, PartialEq, Debug)]
41pub struct PeriodicBox {
42 basis: [[f64; 3]; 3],
43 inverse: [[f64; 3]; 3],
44 orthogonal: bool,
45}
46
47#[derive(Clone, Copy, PartialEq, Debug)]
49pub struct PeriodicImage {
50 pub displacement: [f32; 3],
52 pub lattice_shift: [i64; 3],
54}
55
56impl PeriodicBox {
57 pub fn from_cell(cell: UnitCell) -> Result<Self, SpatialError> {
64 validate_cell(&cell)?;
65
66 let orthogonal = cell
67 .angles
68 .iter()
69 .all(|angle| (*angle - 90.0).abs() < ORTHOGONAL_ANGLE_TOLERANCE);
70
71 let basis = basis_from_cell(&cell)?;
72 let Some(inverse) = inverse3(basis) else {
73 return Err(SpatialError::InvalidCell);
74 };
75
76 Ok(Self {
77 basis,
78 inverse,
79 orthogonal,
80 })
81 }
82
83 #[must_use]
90 pub fn displacement(&self, left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
91 self.minimum_image(left, right).displacement
92 }
93
94 #[must_use]
99 pub fn minimum_image(&self, left: [f32; 3], right: [f32; 3]) -> PeriodicImage {
100 let (best, lattice_shift) = self.image_f64(left.map(f64::from), right.map(f64::from));
101
102 PeriodicImage {
103 displacement: best.map(f64_f32),
104 lattice_shift,
105 }
106 }
107
108 #[must_use]
111 pub fn displacement_f64(&self, left: [f64; 3], right: [f64; 3]) -> [f64; 3] {
112 self.image_f64(left, right).0
113 }
114
115 fn image_f64(&self, left: [f64; 3], right: [f64; 3]) -> ([f64; 3], [i64; 3]) {
116 let cartesian = cartesian_delta(left, right);
117 let fractional = multiply(self.inverse, cartesian);
118 if self.orthogonal {
119 orthogonal_image(self.basis, cartesian, fractional)
120 } else {
121 triclinic_image(self.basis, cartesian, fractional)
122 }
123 }
124
125 #[must_use]
127 pub fn distance_squared(&self, left: [f32; 3], right: [f32; 3]) -> f32 {
128 let displacement = self.displacement(left, right);
129
130 displacement[0] * displacement[0]
131 + displacement[1] * displacement[1]
132 + displacement[2] * displacement[2]
133 }
134
135 #[must_use]
137 pub fn fractional(&self, position: [f32; 3]) -> [f64; 3] {
138 multiply(self.inverse, position.map(f64::from))
139 }
140
141 #[must_use]
143 pub fn cartesian(&self, fractional: [f64; 3]) -> [f32; 3] {
144 multiply(self.basis, fractional).map(f64_f32)
145 }
146
147 #[must_use]
149 pub fn wrap(&self, position: [f32; 3]) -> [f32; 3] {
150 let fractional = self.fractional(position).map(|value| value.rem_euclid(1.0));
151
152 self.cartesian(fractional)
153 }
154
155 #[must_use]
160 pub fn interpolate(&self, left: [f32; 3], right: [f32; 3], amount: f64) -> Option<[f32; 3]> {
161 if !amount.is_finite() {
162 return None;
163 }
164 let displacement = self.minimum_image(left, right).displacement;
165 let point = std::array::from_fn(|axis| {
166 f64_f32(f64::from(left[axis]) + amount * f64::from(displacement[axis]))
167 });
168 Some(self.wrap(point))
169 }
170
171 #[must_use]
176 pub(crate) fn fractional_cutoff_bounds(&self, cutoff: f32) -> [f64; 3] {
177 self.inverse
178 .map(|row| squared(row).sqrt() * f64::from(cutoff))
179 }
180
181 pub(crate) fn translated(
183 &self,
184 point: [f32; 3],
185 shift: [i64; 3],
186 ) -> Result<[f32; 3], SpatialError> {
187 let convert = |value| i64_f64(value).ok_or(SpatialError::NumericRangeExceeded);
188 let translation = multiply(
189 self.basis,
190 [convert(shift[0])?, convert(shift[1])?, convert(shift[2])?],
191 );
192 Ok(std::array::from_fn(|axis| {
193 f64_f32(f64::from(point[axis]) + translation[axis])
194 }))
195 }
196}
197
198fn validate_cell(cell: &UnitCell) -> Result<(), SpatialError> {
202 let finite = cell
203 .lengths
204 .iter()
205 .chain(&cell.angles)
206 .all(|value| value.is_finite());
207
208 let positive = cell.lengths.iter().all(|length| *length > 0.0);
209
210 if finite && positive {
211 Ok(())
212 } else {
213 Err(SpatialError::InvalidCell)
214 }
215}
216
217fn basis_from_cell(cell: &UnitCell) -> Result<[[f64; 3]; 3], SpatialError> {
224 let [a, b, c] = cell.lengths;
225 let [alpha, beta, gamma] = cell.angles.map(f64::to_radians);
226
227 let sin_gamma = gamma.sin();
228
229 if !sin_gamma.is_finite() || sin_gamma.abs() <= f64::EPSILON {
230 return Err(SpatialError::InvalidCell);
231 }
232
233 let cos_gamma = gamma.cos();
234 let cos_beta = beta.cos();
235 let cos_alpha = alpha.cos();
236
237 let cx = c * cos_beta;
238 let cy = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma;
239 let cz_squared = c.mul_add(c, -cx.mul_add(cx, cy * cy));
240
241 if !cz_squared.is_finite() || cz_squared <= f64::EPSILON {
242 return Err(SpatialError::InvalidCell);
243 }
244
245 Ok([
246 [a, b * cos_gamma, cx],
247 [0.0, b * sin_gamma, cy],
248 [0.0, 0.0, cz_squared.sqrt()],
249 ])
250}
251
252#[inline]
256fn cartesian_delta(left: [f64; 3], right: [f64; 3]) -> [f64; 3] {
257 [right[0] - left[0], right[1] - left[1], right[2] - left[2]]
258}
259
260fn orthogonal_image(
265 basis: [[f64; 3]; 3],
266 cartesian: [f64; 3],
267 fractional: [f64; 3],
268) -> ([f64; 3], [i64; 3]) {
269 let rounded = fractional.map(f64::round);
270 let wrapped = [
271 fractional[0] - rounded[0],
272 fractional[1] - rounded[1],
273 fractional[2] - rounded[2],
274 ];
275
276 let candidate = multiply(basis, wrapped);
277
278 if squared(candidate) < squared(cartesian) {
279 (candidate, integer_shift(rounded))
280 } else {
281 (cartesian, [0; 3])
282 }
283}
284
285fn triclinic_image(
290 basis: [[f64; 3]; 3],
291 cartesian: [f64; 3],
292 fractional: [f64; 3],
293) -> ([f64; 3], [i64; 3]) {
294 let centre = fractional.map(f64::round);
295 let base = [
296 fractional[0] - centre[0],
297 fractional[1] - centre[1],
298 fractional[2] - centre[2],
299 ];
300
301 let mut best = cartesian;
302 let mut best_squared = squared(best);
303 let mut best_shift = [0; 3];
304
305 for offset in IMAGE_OFFSETS {
306 let image = [
307 base[0] - offset[0],
308 base[1] - offset[1],
309 base[2] - offset[2],
310 ];
311
312 let candidate = multiply(basis, image);
313 let candidate_squared = squared(candidate);
314
315 if candidate_squared < best_squared {
316 best = candidate;
317 best_squared = candidate_squared;
318 best_shift = integer_shift([
319 centre[0] + offset[0],
320 centre[1] + offset[1],
321 centre[2] + offset[2],
322 ]);
323 }
324 }
325
326 (best, best_shift)
327}
328
329fn integer_shift(shift: [f64; 3]) -> [i64; 3] {
330 shift.map(rounded_i64)
331}
332
333#[inline]
337fn multiply(matrix: [[f64; 3]; 3], vector: [f64; 3]) -> [f64; 3] {
338 [
339 matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
340 matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
341 matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2],
342 ]
343}
344
345#[inline]
349fn squared(vector: [f64; 3]) -> f64 {
350 vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]
351}
352
353fn cofactors3(matrix: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
357 [
358 [
359 matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1],
360 matrix[1][2] * matrix[2][0] - matrix[1][0] * matrix[2][2],
361 matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0],
362 ],
363 [
364 matrix[0][2] * matrix[2][1] - matrix[0][1] * matrix[2][2],
365 matrix[0][0] * matrix[2][2] - matrix[0][2] * matrix[2][0],
366 matrix[0][1] * matrix[2][0] - matrix[0][0] * matrix[2][1],
367 ],
368 [
369 matrix[0][1] * matrix[1][2] - matrix[0][2] * matrix[1][1],
370 matrix[0][2] * matrix[1][0] - matrix[0][0] * matrix[1][2],
371 matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0],
372 ],
373 ]
374}
375
376fn inverse3(matrix: [[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
381 let cofactors = cofactors3(matrix);
382
383 let determinant = matrix[0][0] * cofactors[0][0]
384 + matrix[0][1] * cofactors[0][1]
385 + matrix[0][2] * cofactors[0][2];
386
387 if !determinant.is_finite() || determinant.abs() <= f64::EPSILON {
388 return None;
389 }
390
391 let reciprocal = determinant.recip();
392
393 Some([
394 [
395 cofactors[0][0] * reciprocal,
396 cofactors[1][0] * reciprocal,
397 cofactors[2][0] * reciprocal,
398 ],
399 [
400 cofactors[0][1] * reciprocal,
401 cofactors[1][1] * reciprocal,
402 cofactors[2][1] * reciprocal,
403 ],
404 [
405 cofactors[0][2] * reciprocal,
406 cofactors[1][2] * reciprocal,
407 cofactors[2][2] * reciprocal,
408 ],
409 ])
410}
411
412#[cfg(test)]
413#[path = "periodic_tests.rs"]
414mod tests;