1use crate::kernel;
15use crate::selling;
16use crate::Error;
17
18pub(crate) use crate::kernel::mul;
19
20#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct Cell {
39 h: [[f64; 3]; 3],
41 hinv: [[f64; 3]; 3],
43 origin: [f64; 3],
44 widths: [f64; 3],
46 ortho: bool,
48 restricted: bool,
50 tri: kernel::Tri,
52}
53
54#[cfg(target_arch = "x86_64")]
63#[inline(always)]
64unsafe fn load_lo8_hi16(p: *const f64) -> (f64, f64, f64) {
65 let x: f64;
66 let y: f64;
67 let z: f64;
68 std::arch::asm!(
72 "movsd {x}, qword ptr [{p}]",
73 "movupd {y}, xmmword ptr [{p} + 8]",
74 "movapd {z}, {y}",
75 "unpckhpd {z}, {y}",
76 p = in(reg) p,
77 x = out(xmm_reg) x,
78 y = out(xmm_reg) y,
79 z = out(xmm_reg) z,
80 options(nostack, preserves_flags, readonly),
81 );
82 (x, y, z)
83}
84
85#[cfg(target_arch = "x86_64")]
94#[inline(always)]
95unsafe fn load_lo16_hi8(p: *const f64) -> (f64, f64, f64) {
96 let x: f64;
97 let y: f64;
98 let z: f64;
99 std::arch::asm!(
100 "movupd {x}, xmmword ptr [{p}]",
101 "movapd {y}, {x}",
102 "unpckhpd {y}, {x}",
103 "movsd {z}, qword ptr [{p} + 16]",
104 p = in(reg) p,
105 x = out(xmm_reg) x,
106 y = out(xmm_reg) y,
107 z = out(xmm_reg) z,
108 options(nostack, preserves_flags, readonly),
109 );
110 (x, y, z)
111}
112
113#[cfg(not(target_arch = "x86_64"))]
119#[inline(always)]
120unsafe fn load_lo8_hi16(p: *const f64) -> (f64, f64, f64) {
121 (
122 std::ptr::read_volatile(p),
123 std::ptr::read_volatile(p.add(1)),
124 std::ptr::read_volatile(p.add(2)),
125 )
126}
127
128#[cfg(not(target_arch = "x86_64"))]
134#[inline(always)]
135unsafe fn load_lo16_hi8(p: *const f64) -> (f64, f64, f64) {
136 load_lo8_hi16(p)
137}
138
139impl Cell {
140 pub fn ortho(lx: f64, ly: f64, lz: f64) -> Result<Self, Error> {
145 Self::from_vectors(
146 [lx, 0.0, 0.0],
147 [0.0, ly, 0.0],
148 [0.0, 0.0, lz],
149 [0.0, 0.0, 0.0],
150 )
151 }
152
153 pub fn ortho_origin(lx: f64, ly: f64, lz: f64, origin: [f64; 3]) -> Result<Self, Error> {
155 Self::from_vectors([lx, 0.0, 0.0], [0.0, ly, 0.0], [0.0, 0.0, lz], origin)
156 }
157
158 #[inline(always)]
166 pub fn from_vectors(
167 a: [f64; 3],
168 b: [f64; 3],
169 c: [f64; 3],
170 origin: [f64; 3],
171 ) -> Result<Self, Error> {
172 let (ax, ay, az) = unsafe { load_lo8_hi16(std::ptr::addr_of!(a).cast()) };
177 let (bx, by, bz) = unsafe { load_lo16_hi8(std::ptr::addr_of!(b).cast()) };
178 let (cx, cy, cz) = unsafe { load_lo16_hi8(std::ptr::addr_of!(c).cast()) };
179 let (ox, oy, oz) = unsafe { load_lo16_hi8(std::ptr::addr_of!(origin).cast()) };
180 let bcx = by * cz - bz * cy;
182 let bcy = bz * cx - bx * cz;
183 let bcz = bx * cy - by * cx;
184 let cax = cy * az - cz * ay;
185 let cay = cz * ax - cx * az;
186 let caz = cx * ay - cy * ax;
187 let abx = ay * bz - az * by;
188 let aby = az * bx - ax * bz;
189 let abz = ax * by - ay * bx;
190 let det = ax * bcx + ay * bcy + az * bcz;
191 if !det.is_finite() || det.abs() < 1e-18 {
192 return Err(Error::BadBox);
193 }
194 let invdet = 1.0 / det;
195 let ad = det.abs();
196 let wa = ad / (bcx * bcx + bcy * bcy + bcz * bcz).sqrt();
197 let wb = ad / (cax * cax + cay * cay + caz * caz).sqrt();
198 let wc = ad / (abx * abx + aby * aby + abz * abz).sqrt();
199 if !(wa > 0.0 && wb > 0.0 && wc > 0.0) {
200 return Err(Error::BadBox);
201 }
202 let scale = (wa + wb + wc).max(1.0);
206 let tol = 1e-12 * scale;
207 let ortho = ay.abs() <= tol
208 && az.abs() <= tol
209 && bx.abs() <= tol
210 && bz.abs() <= tol
211 && cx.abs() <= tol
212 && cy.abs() <= tol;
213 let rtol = 1e-10 * scale;
214 let restricted = !(ay.abs() > rtol || az.abs() > rtol || bz.abs() > rtol)
215 && ax.abs() >= 1e-18
216 && by.abs() >= 1e-18
217 && cz.abs() >= 1e-18;
218 let tri = if restricted {
219 kernel::Tri {
220 lx: ax,
221 ly: by,
222 lz: cz,
223 xy: bx,
224 xz: cx,
225 yz: cy,
226 inv_lx: 1.0 / ax,
227 inv_ly: 1.0 / by,
228 inv_lz: 1.0 / cz,
229 }
230 } else {
231 kernel::Tri {
232 lx: 0.0,
233 ly: 0.0,
234 lz: 0.0,
235 xy: 0.0,
236 xz: 0.0,
237 yz: 0.0,
238 inv_lx: 0.0,
239 inv_ly: 0.0,
240 inv_lz: 0.0,
241 }
242 };
243 Ok(Self {
244 h: [[ax, ay, az], [bx, by, bz], [cx, cy, cz]],
245 hinv: [
246 [bcx * invdet, cax * invdet, abx * invdet],
247 [bcy * invdet, cay * invdet, aby * invdet],
248 [bcz * invdet, caz * invdet, abz * invdet],
249 ],
250 origin: [ox, oy, oz],
251 widths: [wa, wb, wc],
252 ortho,
253 restricted,
254 tri,
255 })
256 }
257
258 pub fn from_ase(rows: [[f64; 3]; 3]) -> Result<Self, Error> {
262 Self::from_vectors(rows[0], rows[1], rows[2], [0.0, 0.0, 0.0])
263 }
264
265 pub fn from_con(rows: [[f64; 3]; 3]) -> Result<Self, Error> {
270 Self::from_ase(rows)
271 }
272
273 pub fn from_con_box(boxl: [f64; 3], angles_deg: [f64; 3]) -> Result<Self, Error> {
279 let [lx, ly, lz] = boxl;
280 if !(lx > 0.0 && ly > 0.0 && lz > 0.0) {
281 return Err(Error::BadBox);
282 }
283 let alpha = angles_deg[0].to_radians();
284 let beta = angles_deg[1].to_radians();
285 let gamma = angles_deg[2].to_radians();
286 let cos_a = alpha.cos();
287 let cos_b = beta.cos();
288 let cos_g = gamma.cos();
289 let sin_g = gamma.sin();
290 if sin_g.abs() < 1e-18 {
291 return Err(Error::BadBox);
292 }
293 let a = [lx, 0.0, 0.0];
294 let b = [ly * cos_g, ly * sin_g, 0.0];
295 let cx = lz * cos_b;
296 let cy = lz * (cos_a - cos_b * cos_g) / sin_g;
297 let cz2 = lz * lz - cx * cx - cy * cy;
298 if cz2 <= 0.0 {
299 return Err(Error::BadBox);
300 }
301 Self::from_vectors(a, b, [cx, cy, cz2.sqrt()], [0.0, 0.0, 0.0])
302 }
303
304 pub fn from_vesin(rows: [[f64; 3]; 3]) -> Result<Self, Error> {
306 Self::from_ase(rows)
307 }
308
309 #[allow(clippy::too_many_arguments)]
315 pub fn from_lammps(
316 xlo: f64,
317 xhi: f64,
318 ylo: f64,
319 yhi: f64,
320 zlo: f64,
321 zhi: f64,
322 xy: f64,
323 xz: f64,
324 yz: f64,
325 ) -> Result<Self, Error> {
326 Self::from_lammps_bounds(xhi - xlo, yhi - ylo, zhi - zlo, xy, xz, yz, xlo, ylo, zlo)
327 }
328
329 #[allow(clippy::too_many_arguments)]
336 pub fn from_lammps_bounds(
337 xspan: f64,
338 yspan: f64,
339 zspan: f64,
340 xy: f64,
341 xz: f64,
342 yz: f64,
343 xlo_b: f64,
344 ylo_b: f64,
345 zlo_b: f64,
346 ) -> Result<Self, Error> {
347 let (h, origin) = dump_bounds_to_h(xspan, yspan, zspan, xy, xz, yz, xlo_b, ylo_b, zlo_b);
348 Self::from_vectors(h[0], h[1], h[2], origin)
349 }
350
351 pub fn is_ortho(&self) -> bool {
354 self.ortho
355 }
356
357 pub fn a(&self) -> [f64; 3] {
359 self.h[0]
360 }
361
362 pub fn b(&self) -> [f64; 3] {
364 self.h[1]
365 }
366
367 pub fn c(&self) -> [f64; 3] {
369 self.h[2]
370 }
371
372 pub fn origin(&self) -> [f64; 3] {
374 self.origin
375 }
376
377 pub fn widths(&self) -> [f64; 3] {
379 self.widths
380 }
381
382 pub fn h(&self) -> [[f64; 3]; 3] {
384 self.h
385 }
386
387 pub fn hinv(&self) -> [[f64; 3]; 3] {
389 self.hinv
390 }
391
392 #[inline]
397 pub fn fractional(&self, r: [f64; 3]) -> [f64; 3] {
398 if self.ortho {
399 return [
400 wrap01((r[0] - self.origin[0]) / self.widths[0]),
401 wrap01((r[1] - self.origin[1]) / self.widths[1]),
402 wrap01((r[2] - self.origin[2]) / self.widths[2]),
403 ];
404 }
405 let d = [
406 r[0] - self.origin[0],
407 r[1] - self.origin[1],
408 r[2] - self.origin[2],
409 ];
410 let mut s = mul(self.hinv, d);
411 for e in &mut s {
412 *e = wrap01(*e);
413 }
414 s
415 }
416
417 #[inline]
419 pub fn cartesian(&self, s: [f64; 3]) -> [f64; 3] {
420 let r = mul(self.h, s);
421 [
422 r[0] + self.origin[0],
423 r[1] + self.origin[1],
424 r[2] + self.origin[2],
425 ]
426 }
427
428 #[inline]
430 pub fn lattice_shift(&self, na: i32, nb: i32, nc: i32) -> [f64; 3] {
431 if self.ortho {
432 [
433 f64::from(na) * self.widths[0],
434 f64::from(nb) * self.widths[1],
435 f64::from(nc) * self.widths[2],
436 ]
437 } else {
438 let a = self.h[0];
439 let b = self.h[1];
440 let c = self.h[2];
441 let fa = f64::from(na);
442 let fb = f64::from(nb);
443 let fc = f64::from(nc);
444 [
445 fa * a[0] + fb * b[0] + fc * c[0],
446 fa * a[1] + fb * b[1] + fc * c[1],
447 fa * a[2] + fb * b[2] + fc * c[2],
448 ]
449 }
450 }
451
452 #[inline]
457 pub fn dist2_shifted(&self, p: [f64; 3], q: [f64; 3], shift: [f64; 3]) -> f64 {
458 let dx = q[0] + shift[0] - p[0];
459 let dy = q[1] + shift[1] - p[1];
460 let dz = q[2] + shift[2] - p[2];
461 dx * dx + dy * dy + dz * dz
462 }
463
464 #[inline]
469 pub fn displacement(&self, p: [f64; 3], q: [f64; 3]) -> [f64; 3] {
470 let dp = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
471 self.wrap_diff(dp)
472 }
473
474 #[inline]
476 pub(crate) fn wrap_diff(&self, dp: [f64; 3]) -> [f64; 3] {
477 if self.ortho {
478 return [
479 kernel::wrap_half(dp[0], self.widths[0]),
480 kernel::wrap_half(dp[1], self.widths[1]),
481 kernel::wrap_half(dp[2], self.widths[2]),
482 ];
483 }
484 if self.restricted {
485 return self.tri.wrap(dp);
486 }
487 kernel::general_wrap(self.h, self.hinv, dp)
488 }
489
490 #[inline]
492 pub fn dist2(&self, p: [f64; 3], q: [f64; 3]) -> f64 {
493 let dr = self.displacement(p, q);
494 dr[0] * dr[0] + dr[1] * dr[1] + dr[2] * dr[2]
495 }
496
497 pub fn displacement_cartesian(&self, p: [f64; 3], q: [f64; 3]) -> [f64; 3] {
505 let dp = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
506 let mut best = dp;
507 let mut best2 = dp[0] * dp[0] + dp[1] * dp[1] + dp[2] * dp[2];
508 for na in -1_i32..=1 {
509 for nb in -1_i32..=1 {
510 for nc in -1_i32..=1 {
511 if na == 0 && nb == 0 && nc == 0 {
512 continue;
513 }
514 let s = self.lattice_shift(na, nb, nc);
515 let t = [dp[0] + s[0], dp[1] + s[1], dp[2] + s[2]];
516 let t2 = t[0] * t[0] + t[1] * t[1] + t[2] * t[2];
517 if t2 < best2 {
518 best2 = t2;
519 best = t;
520 }
521 }
522 }
523 }
524 best
525 }
526
527 pub fn is_restricted(&self) -> bool {
532 self.restricted
533 }
534
535 pub fn tilts_reduced(&self) -> bool {
543 if !self.is_restricted() {
544 return false;
545 }
546 let lx = self.h[0][0].abs();
547 let ly = self.h[1][1].abs();
548 let xy = self.h[1][0];
549 let xz = self.h[2][0];
550 let yz = self.h[2][1];
551 let tol = 1e-12 * (lx + ly).max(1.0);
552 xy.abs() <= 0.5 * lx + tol && xz.abs() <= 0.5 * lx + tol && yz.abs() <= 0.5 * ly + tol
553 }
554
555 pub fn reduce_tilts(&self) -> Result<Self, Error> {
563 let (cell, _rot) = self.restricted_frame()?;
564 if cell.tilts_reduced() {
565 return Ok(cell);
566 }
567 let a = cell.h[0];
568 let mut b = cell.h[1];
569 let mut c = cell.h[2];
570 correct_box_elem(&mut c, b, 1);
571 correct_box_elem(&mut c, a, 0);
572 correct_box_elem(&mut b, a, 0);
573 Self::from_vectors(a, b, c, cell.origin)
574 }
575
576 pub fn to_restricted(&self) -> Result<Self, Error> {
580 Ok(self.restricted_frame()?.0)
581 }
582
583 #[inline]
600 pub fn displacement_euclidean(&self, p: [f64; 3], q: [f64; 3]) -> [f64; 3] {
601 if self.ortho {
602 return self.displacement(p, q);
603 }
604 let dp = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
605 let frac = self.wrap_diff(dp);
606 let f2 = kernel::n2(frac);
607 let w = self.widths;
608 let half_min = 0.5 * w[0].min(w[1]).min(w[2]);
609 let room = half_min - 1e-12;
611 if room > 0.0 && f2 < room * room {
612 return frac;
613 }
614 let euc = selling::closest_for(self.h, dp);
615 let e2 = kernel::n2(euc);
616 if e2 + 1e-12 * (1.0 + f2) < f2 {
617 euc
618 } else {
619 frac
620 }
621 }
622
623 pub fn dist2_shifted_many(
629 &self,
630 p: [f64; 3],
631 qs: &[[f64; 3]],
632 shift: [f64; 3],
633 out: &mut [f64],
634 ) -> Result<(), Error> {
635 if out.len() != qs.len() {
636 return Err(Error::BufferSize);
637 }
638 let _ = self.ortho;
639 crate::simd::dist2_shifted_many(p, qs, shift, out);
640 Ok(())
641 }
642
643 pub fn dist2_shifted_indexed(
655 &self,
656 p: [f64; 3],
657 positions: &[[f64; 3]],
658 indices: &[usize],
659 shift: [f64; 3],
660 out: &mut [f64],
661 ) -> Result<(), Error> {
662 if out.len() != indices.len() {
663 return Err(Error::BufferSize);
664 }
665 const CHUNK: usize = 64;
666 let mut gathered = [[0.0; 3]; CHUNK];
667 let mut start = 0;
668 while start < indices.len() {
669 let n = (indices.len() - start).min(CHUNK);
670 for k in 0..n {
671 gathered[k] = positions[indices[start + k]];
672 }
673 crate::simd::dist2_shifted_many(p, &gathered[..n], shift, &mut out[start..start + n]);
674 start += n;
675 }
676 let _ = self.ortho;
677 Ok(())
678 }
679
680 #[inline]
682 pub fn dist2_euclidean(&self, p: [f64; 3], q: [f64; 3]) -> f64 {
683 let dr = self.displacement_euclidean(p, q);
684 dr[0] * dr[0] + dr[1] * dr[1] + dr[2] * dr[2]
685 }
686
687 fn restricted_frame(&self) -> Result<(Self, [[f64; 3]; 3]), Error> {
688 if self.is_restricted() {
689 return Ok((*self, IDENTITY));
690 }
691 let a = self.h[0];
692 let b = self.h[1];
693 let c = self.h[2];
694 let al = kernel::norm(a);
695 if al < 1e-18 {
696 return Err(Error::BadBox);
697 }
698 let a_hat = [a[0] / al, a[1] / al, a[2] / al];
699 let b_par = kernel::dot(b, a_hat);
700 let b_perp = [
701 b[0] - b_par * a_hat[0],
702 b[1] - b_par * a_hat[1],
703 b[2] - b_par * a_hat[2],
704 ];
705 let bl = kernel::norm(b_perp);
706 if bl < 1e-18 {
707 return Err(Error::BadBox);
708 }
709 let n = [b_perp[0] / bl, b_perp[1] / bl, b_perp[2] / bl];
710 let k = kernel::cross(a_hat, n);
711 let rot = [a_hat, n, k];
712 let a_new = [al, 0.0, 0.0];
713 let b_new = [b_par, bl, 0.0];
714 let c_new = [kernel::dot(c, a_hat), kernel::dot(c, n), kernel::dot(c, k)];
715 let cell = Self::from_vectors(a_new, b_new, c_new, mul_rows(rot, self.origin))?;
716 Ok((cell, rot))
717 }
718
719 pub fn fractional_matches_cartesian(&self) -> bool {
728 let samples = [
729 [0.2, 0.0, 0.0],
730 [0.49, 0.49, 0.49],
731 [0.9, 0.1, 0.1],
732 [0.1, 0.9, 0.1],
733 [0.1, 0.1, 0.9],
734 [0.8, 0.8, 0.2],
735 ];
736 for p in samples {
737 let q = [0.0, 0.0, 0.0];
738 let a = self.displacement(p, q);
739 let b = self.displacement_cartesian(p, q);
740 let d2 = (a[0] - b[0]) * (a[0] - b[0])
741 + (a[1] - b[1]) * (a[1] - b[1])
742 + (a[2] - b[2]) * (a[2] - b[2]);
743 if d2 > 1e-20 {
744 return false;
745 }
746 }
747 true
748 }
749}
750
751#[cfg(test)]
752mod cartesian_tests {
753 use super::Cell;
754
755 #[test]
756 fn ortho_fractional_is_cartesian() {
757 let c = Cell::ortho(10.0, 11.0, 12.0).unwrap();
758 assert!(c.fractional_matches_cartesian());
759 }
760
761 #[test]
762 fn restricted_shear_fractional_is_cartesian() {
763 let c = Cell::from_vectors(
764 [10.0, 0.0, 0.0],
765 [5.0, 8.660254037844386, 0.0],
766 [0.0, 0.0, 10.0],
767 [0.0, 0.0, 0.0],
768 )
769 .unwrap();
770 assert!(c.fractional_matches_cartesian());
771 }
772
773 #[test]
774 fn extreme_skew_cartesian_can_be_shorter() {
775 let c = Cell::from_vectors(
776 [1.0, 0.0, 0.0],
777 [0.9, 0.1, 0.0],
778 [0.0, 0.0, 1.0],
779 [0.0, 0.0, 0.0],
780 )
781 .unwrap();
782 let p = [0.0, 0.0, 0.0];
783 let q = [0.95, 0.05, 0.0];
784 let frac = c.displacement(p, q);
785 let cart = c.displacement_cartesian(p, q);
786 let f2 = frac[0] * frac[0] + frac[1] * frac[1] + frac[2] * frac[2];
787 let c2 = cart[0] * cart[0] + cart[1] * cart[1] + cart[2] * cart[2];
788 assert!(c2 <= f2 + 1e-15);
789 assert!(c2 < 0.01);
790 }
791
792 #[test]
793 fn extreme_skew_lattice_point_needs_tilt_reduce() {
794 let c = Cell::from_vectors(
795 [1.0, 0.0, 0.0],
796 [0.99, 0.01, 0.0],
797 [0.0, 0.0, 1.0],
798 [0.0, 0.0, 0.0],
799 )
800 .unwrap();
801 assert!(c.is_restricted());
802 assert!(!c.tilts_reduced());
803 let p = [0.0, 0.0, 0.0];
804 let q = [0.02, -0.02, 0.0];
805 let cart = c.displacement_cartesian(p, q);
806 let euc = c.displacement_euclidean(p, q);
807 let c2 = cart[0] * cart[0] + cart[1] * cart[1] + cart[2] * cart[2];
808 let e2 = euc[0] * euc[0] + euc[1] * euc[1] + euc[2] * euc[2];
809 assert!(c2 > 1e-8, "27-image from the unreduced basis is not 0");
810 assert!(e2 < 1e-24, "tilt-reduced wrap sends the lattice point to 0");
811 let red = c.reduce_tilts().unwrap();
812 assert!(red.tilts_reduced());
813 assert!(red.h()[1][0].abs() <= 0.5 * red.h()[0][0].abs() + 1e-12);
814 }
815
816 #[test]
817 fn hex_body_diagonal_fractional_is_not_nearest() {
818 let c = Cell::from_vectors(
819 [10.0, 0.0, 0.0],
820 [5.0, 8.660254037844386, 0.0],
821 [0.0, 0.0, 10.0],
822 [0.0, 0.0, 0.0],
823 )
824 .unwrap();
825 assert!(c.tilts_reduced());
826 let p = c.cartesian([0.49, 0.49, 0.49]);
827 let q = c.cartesian([0.0, 0.0, 0.0]);
828 let frac = c.displacement(p, q);
829 let euc = c.displacement_euclidean(p, q);
830 let f2 = frac[0] * frac[0] + frac[1] * frac[1] + frac[2] * frac[2];
831 let e2 = euc[0] * euc[0] + euc[1] * euc[1] + euc[2] * euc[2];
832 assert!(
833 e2 + 1e-8 < f2,
834 "Nguyen-Stehle 27-image in the Minkowski basis is shorter than the fractional wrap"
835 );
836 }
837
838 #[test]
839 fn restricted_hex_is_already_reduced() {
840 let c = Cell::from_vectors(
841 [10.0, 0.0, 0.0],
842 [5.0, 8.660254037844386, 0.0],
843 [0.0, 0.0, 10.0],
844 [0.0, 0.0, 0.0],
845 )
846 .unwrap();
847 assert!(c.is_restricted());
848 assert!(c.tilts_reduced());
849 let p = [0.2, 0.1, 1.0];
850 let q = [9.7, 0.1, 1.0];
851 let a = c.displacement(p, q);
852 let b = c.displacement_euclidean(p, q);
853 assert!((a[0] - b[0]).abs() < 1e-15);
854 assert!((a[1] - b[1]).abs() < 1e-15);
855 assert!((a[2] - b[2]).abs() < 1e-15);
856 }
857
858 #[test]
859 fn euclidean_cache_alternates_two_lattices() {
860 let hex = Cell::from_vectors(
861 [10.0, 0.0, 0.0],
862 [5.0, 8.660254037844386, 0.0],
863 [0.0, 0.0, 10.0],
864 [0.0, 0.0, 0.0],
865 )
866 .unwrap();
867 let skew = Cell::from_vectors(
868 [1.0, 0.0, 0.0],
869 [0.99, 0.01, 0.0],
870 [0.0, 0.0, 1.0],
871 [0.0, 0.0, 0.0],
872 )
873 .unwrap();
874 let p = [0.0, 0.0, 0.0];
875 let q_hex = hex.cartesian([0.49, 0.49, 0.49]);
876 let q_skew = [0.02, -0.02, 0.0];
877 for _ in 0..6 {
878 let frac = hex.displacement(q_hex, p);
879 let euc = hex.displacement_euclidean(q_hex, p);
880 let f2 = frac[0] * frac[0] + frac[1] * frac[1] + frac[2] * frac[2];
881 let e2 = euc[0] * euc[0] + euc[1] * euc[1] + euc[2] * euc[2];
882 assert!(e2 + 1e-8 < f2);
883 let skew_e = skew.displacement_euclidean(p, q_skew);
884 let se2 = skew_e[0] * skew_e[0] + skew_e[1] * skew_e[1] + skew_e[2] * skew_e[2];
885 assert!(se2 < 1e-24);
886 }
887 }
888}
889
890#[allow(clippy::too_many_arguments)]
893pub fn dump_bounds_to_h(
894 xspan: f64,
895 yspan: f64,
896 zspan: f64,
897 xy: f64,
898 xz: f64,
899 yz: f64,
900 xlo_b: f64,
901 ylo_b: f64,
902 zlo_b: f64,
903) -> ([[f64; 3]; 3], [f64; 3]) {
904 let xmin = 0.0_f64.min(xy).min(xz).min(xy + xz);
905 let xmax = 0.0_f64.max(xy).max(xz).max(xy + xz);
906 let ymin = 0.0_f64.min(yz);
907 let ymax = 0.0_f64.max(yz);
908 let lx = xspan - xmax + xmin;
909 let ly = yspan - ymax + ymin;
910 let lz = zspan;
911 let a = [lx, 0.0, 0.0];
912 let b = [xy, ly, 0.0];
913 let c = [xz, yz, lz];
914 let origin = [xlo_b - xmin, ylo_b - ymin, zlo_b];
915 ([a, b, c], origin)
916}
917
918#[inline]
919fn wrap01(mut s: f64) -> f64 {
920 s -= s.floor();
921 if s >= 1.0 {
922 0.0
923 } else {
924 s
925 }
926}
927
928const IDENTITY: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
929
930fn correct_box_elem(vec: &mut [f64; 3], edge: [f64; 3], d: usize) {
933 const MARGIN: f64 = 1.001;
934 const MAX_SHIFT: i32 = 20;
935 let half = 0.5 * edge[d];
936 if half.abs() < 1e-18 {
937 return;
938 }
939 let mut n = 0;
940 while vec[d] > MARGIN * half && n < MAX_SHIFT {
941 vec[0] -= edge[0];
942 vec[1] -= edge[1];
943 vec[2] -= edge[2];
944 n += 1;
945 }
946 while vec[d] < -MARGIN * half && n < MAX_SHIFT {
947 vec[0] += edge[0];
948 vec[1] += edge[1];
949 vec[2] += edge[2];
950 n += 1;
951 }
952}
953
954#[inline]
955fn mul_rows(r: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
956 [
957 kernel::dot(r[0], v),
958 kernel::dot(r[1], v),
959 kernel::dot(r[2], v),
960 ]
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn ortho_flag_and_dist2_match_general() {
969 let b = Cell::ortho(10.0, 11.0, 12.0).unwrap();
970 assert!(b.is_ortho());
971 let p = [0.2, 1.0, 11.5];
972 let q = [9.7, 10.8, 0.4];
973 let fast = b.dist2(p, q);
974 let slow = {
975 let dr = kernel::general_wrap(b.h, b.hinv, [q[0] - p[0], q[1] - p[1], q[2] - p[2]]);
976 dr[0] * dr[0] + dr[1] * dr[1] + dr[2] * dr[2]
977 };
978 assert!((fast - slow).abs() <= 1e-12 * (1.0 + fast.abs()));
979 let left = [0.2, 0.0, 0.0];
980 let right = [9.4, 0.0, 0.0];
981 let mic = b.dist2(left, right);
982 let via = b.dist2_shifted(left, right, b.lattice_shift(-1, 0, 0));
983 assert!((via - mic).abs() <= 1e-12 * (1.0 + mic.abs()));
984 assert!((mic - 0.64).abs() <= 1e-12);
985 }
986
987 #[test]
988 fn ortho_wrap_covers_later_images_and_keeps_negative_half() {
989 let c = Cell::ortho(10.0, 11.0, 12.0).unwrap();
990 let d = c.displacement([0.0, 0.0, 0.0], [25.0, -18.0, 12.0]);
991 assert!((d[0] + 5.0).abs() < 1e-12);
992 assert!((d[1] - 4.0).abs() < 1e-12);
993 assert!(d[2].abs() < 1e-12);
994 let half = c.displacement([0.0, 0.0, 0.0], [5.0, -5.5, 6.0]);
995 assert!((half[0] + 5.0).abs() < 1e-12);
996 assert!((half[1] + 5.5).abs() < 1e-12);
997 assert!((half[2] + 6.0).abs() < 1e-12);
998 let euc = c.displacement_euclidean([0.0, 0.0, 0.0], [25.0, -18.0, 12.0]);
999 assert!((euc[0] - d[0]).abs() < 1e-12);
1000 assert!((euc[1] - d[1]).abs() < 1e-12);
1001 assert!((euc[2] - d[2]).abs() < 1e-12);
1002 }
1003
1004 #[test]
1005 fn indexed_shift_matches_the_contiguous_bin() {
1006 let cell = Cell::ortho(10.0, 11.0, 12.0).unwrap();
1007 let positions = [
1008 [0.2, 0.0, 0.0],
1009 [9.4, 1.0, 2.0],
1010 [3.0, 4.0, 5.0],
1011 [8.0, 0.5, 0.5],
1012 ];
1013 let indices = [2usize, 0, 3, 1, 2];
1014 let p = [1.0, 1.0, 1.0];
1015 let shift = cell.lattice_shift(1, 0, -1);
1016 let mut indexed = [0.0; 5];
1017 cell.dist2_shifted_indexed(p, &positions, &indices, shift, &mut indexed)
1018 .unwrap();
1019 for (slot, idx) in indexed.iter().zip(indices) {
1020 let one = cell.dist2_shifted(p, positions[idx], shift);
1021 assert!((slot - one).abs() < 1e-12);
1022 }
1023 let mut wrong = [0.0; 4];
1024 assert_eq!(
1025 cell.dist2_shifted_indexed(p, &positions, &indices, shift, &mut wrong),
1026 Err(Error::BufferSize)
1027 );
1028 }
1029
1030 #[test]
1031 fn lammps_sheared_a_image_is_quarter() {
1032 let cell =
1033 Cell::from_lammps_bounds(15.0, 8.660254037844386, 10.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0)
1034 .unwrap();
1035 assert!(!cell.is_ortho());
1036 assert!((cell.a()[0] - 10.0).abs() < 1e-12);
1037 assert!((cell.b()[0] - 5.0).abs() < 1e-12);
1038 let p = [0.2, 0.1, 1.0];
1039 let q = [9.7, 0.1, 1.0];
1040 assert!((cell.dist2(p, q) - 0.25).abs() <= 1e-12);
1041 }
1042
1043 #[test]
1044 fn ase_rows_match_from_vectors() {
1045 let rows = [[10.0, 0.0, 0.0], [5.0, 8.66, 0.0], [0.0, 0.0, 10.0]];
1046 let a = Cell::from_ase(rows).unwrap();
1047 let b = Cell::from_vectors(rows[0], rows[1], rows[2], [0.0, 0.0, 0.0]).unwrap();
1048 assert_eq!(a, b);
1049 assert_eq!(Cell::from_vesin(rows).unwrap(), a);
1050 assert_eq!(Cell::from_con(rows).unwrap(), a);
1051 }
1052
1053 #[test]
1054 fn con_box_ortho_is_diagonal() {
1055 let cell = Cell::from_con_box([10.0, 11.0, 12.0], [90.0, 90.0, 90.0]).unwrap();
1056 assert!(cell.is_ortho());
1057 assert!((cell.widths()[0] - 10.0).abs() < 1e-12);
1058 assert!((cell.widths()[1] - 11.0).abs() < 1e-12);
1059 assert!((cell.widths()[2] - 12.0).abs() < 1e-12);
1060 }
1061
1062 #[test]
1063 fn rel_from_j_matches_seams_mixed_image() {
1064 let cell =
1065 Cell::from_lammps_bounds(15.0, 8.660254037844386, 10.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0)
1066 .unwrap();
1067 let i = [0.5, 0.5, 1.0];
1068 let j = [1.0, 8.0, 1.0];
1069 let dr = cell.displacement(j, i);
1070 assert!((dr[0] - 4.5).abs() < 1e-10);
1071 assert!((dr[1] - 1.160254037844386).abs() < 1e-10);
1072 assert!(dr[2].abs() < 1e-12);
1073 }
1074}