1use super::region::Region;
9use crate::math;
10use crate::types::{F, F3, F3View, F3x3, FNx3, FNx3View, Pbc3};
11use ndarray::{Array1, Array2, ArrayView1, array};
12
13#[derive(Debug, Clone, PartialEq)]
15pub enum BoxKind {
16 Ortho { len: F3, inv_len: F3 },
18 Triclinic,
20}
21
22#[derive(Debug, Clone)]
24pub struct SimBox {
25 h: F3x3,
27 inv: F3x3,
29 origin: F3,
31 pbc: Pbc3,
33 kind: BoxKind,
35 cell_defined: bool,
42}
43
44#[derive(Debug)]
46pub enum BoxError {
47 SingularCell,
49 InvalidMatrixShape { rows: usize, cols: usize },
51 InvalidVectorLength { len: usize },
53 NonContiguous(&'static str),
55}
56
57impl SimBox {
58 pub fn new(h: F3x3, origin: F3, pbc: Pbc3) -> Result<Self, BoxError> {
60 Self::new_cell(h, origin, pbc, true)
61 }
62
63 pub fn new_cell(h: F3x3, origin: F3, pbc: Pbc3, cell_defined: bool) -> Result<Self, BoxError> {
68 if let Some(inv) = math::inv3(&h) {
69 let kind = detect_box_kind(&h);
70 Ok(Self {
71 h,
72 inv,
73 origin,
74 pbc,
75 kind,
76 cell_defined,
77 })
78 } else {
79 Err(BoxError::SingularCell)
80 }
81 }
82
83 pub fn is_cell_defined(&self) -> bool {
88 self.cell_defined
89 }
90
91 pub fn try_new(h: F3x3, origin: F3, pbc: Pbc3) -> Result<Self, BoxError> {
92 Self::new(h, origin, pbc)
93 }
94
95 pub fn cube(a: F, origin: F3, pbc: Pbc3) -> Result<Self, BoxError> {
97 if a <= 0.0 {
98 return Err(BoxError::InvalidVectorLength { len: 0 });
99 }
100 let h = array![[a, 0.0, 0.0], [0.0, a, 0.0], [0.0, 0.0, a]];
101 Self::new(h, origin, pbc)
102 }
103
104 pub fn ortho(lengths: F3, origin: F3, pbc: Pbc3) -> Result<Self, BoxError> {
106 if lengths.len() != 3 {
107 return Err(BoxError::InvalidVectorLength { len: lengths.len() });
108 }
109 if lengths.iter().any(|v| *v <= 0.0) {
110 return Err(BoxError::InvalidVectorLength { len: 0 });
111 }
112 let h = array![
113 [lengths[0], 0.0, 0.0],
114 [0.0, lengths[1], 0.0],
115 [0.0, 0.0, lengths[2]],
116 ];
117 Self::new(h, origin, pbc)
118 }
119
120 pub fn free(points: FNx3View<'_>, padding: F) -> Result<Self, BoxError> {
134 assert!(padding > 0.0, "padding must be positive");
135 let n = points.nrows();
136 if n == 0 {
137 return Self::cube(padding, array![0.0 as F, 0.0, 0.0], [false, false, false]);
139 }
140 let mut min = array![points[[0, 0]], points[[0, 1]], points[[0, 2]]];
141 let mut max = min.clone();
142 for i in 1..n {
143 for d in 0..3 {
144 if points[[i, d]] < min[d] {
145 min[d] = points[[i, d]];
146 }
147 if points[[i, d]] > max[d] {
148 max[d] = points[[i, d]];
149 }
150 }
151 }
152 let origin = array![min[0] - padding, min[1] - padding, min[2] - padding,];
153 let lengths = array![
154 (max[0] - min[0] + 2.0 * padding).max(padding),
155 (max[1] - min[1] + 2.0 * padding).max(padding),
156 (max[2] - min[2] + 2.0 * padding).max(padding),
157 ];
158 Self::ortho(lengths, origin, [false, false, false])
159 }
160
161 pub fn h_view(&self) -> FNx3View<'_> {
163 self.h.view()
164 }
165
166 pub fn inv_view(&self) -> FNx3View<'_> {
168 self.inv.view()
169 }
170
171 pub fn origin_view(&self) -> F3View<'_> {
173 self.origin.view()
174 }
175
176 pub fn pbc_view(&self) -> ArrayView1<'_, bool> {
178 ArrayView1::from_shape(3, &self.pbc).expect("pbc_view shape")
179 }
180
181 pub fn pbc(&self) -> Pbc3 {
183 self.pbc
184 }
185
186 pub fn volume(&self) -> F {
188 math::det3(&self.h).abs()
189 }
190
191 pub fn is_free(&self) -> bool {
193 self.pbc.iter().all(|&p| !p)
194 }
195
196 pub fn style(&self) -> &'static str {
199 if self.is_free() {
200 "free"
201 } else {
202 match self.kind {
203 BoxKind::Ortho { .. } => "orthogonal",
204 BoxKind::Triclinic => "triclinic",
205 }
206 }
207 }
208
209 pub fn tilts(&self) -> F3 {
211 array![self.h[[0, 1]], self.h[[0, 2]], self.h[[1, 2]]]
212 }
213
214 pub fn lengths(&self) -> F3 {
216 let a = self.lattice(0);
217 let b = self.lattice(1);
218 let c = self.lattice(2);
219 array![math::norm3(&a), math::norm3(&b), math::norm3(&c)]
220 }
221
222 pub fn nearest_plane_distance(&self) -> F3 {
225 let v = self.volume();
226 let a1 = self.lattice(0);
227 let a2 = self.lattice(1);
228 let a3 = self.lattice(2);
229
230 let c23 = math::cross3(&a2, &a3);
231 let c31 = math::cross3(&a3, &a1);
232 let c12 = math::cross3(&a1, &a2);
233
234 array![
235 v / math::norm3(&c23),
236 v / math::norm3(&c31),
237 v / math::norm3(&c12)
238 ]
239 }
240
241 pub fn kind(&self) -> &BoxKind {
242 &self.kind
243 }
244
245 pub fn lattice(&self, index: usize) -> F3 {
247 assert!(index < 3, "lattice index must be 0..2");
248 self.h.column(index).to_owned()
249 }
250
251 pub fn make_fractional(&self, r: F3View<'_>) -> F3 {
253 let dr = &r - &self.origin.view();
254 let mut frac = self.inv.dot(&dr);
255 for f in frac.iter_mut() {
256 *f -= f.floor();
257 }
258 frac
259 }
260
261 #[inline(always)]
263 pub fn make_fractional_fast(&self, r: F3View<'_>) -> F3 {
264 match &self.kind {
265 BoxKind::Ortho { inv_len, .. } => {
266 let mut frac = array![
267 (r[0] - self.origin[0]) * inv_len[0],
268 (r[1] - self.origin[1]) * inv_len[1],
269 (r[2] - self.origin[2]) * inv_len[2],
270 ];
271 for f in frac.iter_mut() {
272 *f -= f.floor();
273 }
274 frac
275 }
276 BoxKind::Triclinic => self.make_fractional(r),
277 }
278 }
279
280 #[inline(always)]
286 pub fn make_fractional_fast_arr(&self, r: F3View<'_>) -> [F; 3] {
287 match &self.kind {
288 BoxKind::Ortho { inv_len, .. } => {
289 let fx = (r[0] - self.origin[0]) * inv_len[0];
290 let fy = (r[1] - self.origin[1]) * inv_len[1];
291 let fz = (r[2] - self.origin[2]) * inv_len[2];
292 [fx - fx.floor(), fy - fy.floor(), fz - fz.floor()]
293 }
294 BoxKind::Triclinic => {
295 let f = self.make_fractional(r);
296 [f[0], f[1], f[2]]
297 }
298 }
299 }
300
301 pub fn make_cartesian(&self, frac: F3View<'_>) -> F3 {
303 &self.origin + &self.h.dot(&frac)
304 }
305
306 #[inline(always)]
316 fn mic_kernel(&self, a: [F; 3], b: [F; 3]) -> [F; 3] {
317 match &self.kind {
318 BoxKind::Ortho { len, inv_len } => {
319 let mut dr = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
320 if self.pbc[0] {
321 dr[0] -= (dr[0] * inv_len[0]).round() * len[0];
322 }
323 if self.pbc[1] {
324 dr[1] -= (dr[1] * inv_len[1]).round() * len[1];
325 }
326 if self.pbc[2] {
327 dr[2] -= (dr[2] * inv_len[2]).round() * len[2];
328 }
329 dr
330 }
331 BoxKind::Triclinic => {
332 let dr_cart = array![b[0] - a[0], b[1] - a[1], b[2] - a[2]];
336 let mut dr_frac = self.inv.dot(&dr_cart);
337 for d in 0..3 {
338 if self.pbc[d] {
339 dr_frac[d] -= dr_frac[d].round();
340 }
341 }
342 let v = self.h.dot(&dr_frac);
343 [v[0], v[1], v[2]]
344 }
345 }
346 }
347
348 #[inline]
355 pub fn shortest_vector(&self, r1: F3View<'_>, r2: F3View<'_>) -> F3 {
356 let dr = self.mic_kernel([r1[0], r1[1], r1[2]], [r2[0], r2[1], r2[2]]);
357 array![dr[0], dr[1], dr[2]]
358 }
359
360 #[inline(always)]
367 pub fn shortest_vector_impl(&self, a: [F; 3], b: [F; 3]) -> [F; 3] {
368 self.mic_kernel(a, b)
369 }
370
371 #[inline]
373 pub fn calc_distance2(&self, a: F3View<'_>, b: F3View<'_>) -> F {
374 let dr = self.shortest_vector(a, b);
375 dr.dot(&dr)
376 }
377
378 pub fn to_frac(&self, xyz: FNx3View<'_>) -> FNx3 {
380 let n = xyz.nrows();
381 let mut result = FNx3::zeros((n, 3));
382 for i in 0..n {
383 let dr = &xyz.row(i) - &self.origin.view();
384 result.row_mut(i).assign(&self.inv.dot(&dr));
385 }
386 result
387 }
388
389 pub fn to_cart(&self, frac: FNx3View<'_>) -> FNx3 {
391 let n = frac.nrows();
392 let mut result = FNx3::zeros((n, 3));
393 for i in 0..n {
394 let cart = &self.origin + &self.h.dot(&frac.row(i));
395 result.row_mut(i).assign(&cart);
396 }
397 result
398 }
399
400 pub fn isin(&self, xyz: FNx3View<'_>) -> Array1<bool> {
402 let n = xyz.nrows();
403 let mut mask = Vec::with_capacity(n);
404 for i in 0..n {
405 let dr = &xyz.row(i) - &self.origin.view();
406 let frac = self.inv.dot(&dr);
407 let inside = (0..3).all(|d| frac[d] >= 0.0 && frac[d] < 1.0);
408 mask.push(inside);
409 }
410 Array1::from_vec(mask)
411 }
412
413 pub fn delta_out(
416 &self,
417 xyzu1: FNx3View<'_>,
418 xyzu2: FNx3View<'_>,
419 out: &mut FNx3,
420 minimum_image: bool,
421 ) {
422 assert_eq!(xyzu1.nrows(), xyzu2.nrows());
423 let n = xyzu1.nrows();
424 if minimum_image {
425 for i in 0..n {
426 let dr = self.shortest_vector(xyzu1.row(i), xyzu2.row(i));
427 out.row_mut(i).assign(&dr);
428 }
429 } else {
430 for i in 0..n {
431 let dr = &xyzu2.row(i) - &xyzu1.row(i);
432 out.row_mut(i).assign(&dr);
433 }
434 }
435 }
436
437 pub fn delta(&self, xyzu1: FNx3View<'_>, xyzu2: FNx3View<'_>, minimum_image: bool) -> FNx3 {
439 assert_eq!(xyzu1.nrows(), xyzu2.nrows());
440 let n = xyzu1.nrows();
441 let mut out = FNx3::zeros((n, 3));
442 self.delta_out(xyzu1, xyzu2, &mut out, minimum_image);
443 out
444 }
445
446 pub fn wrap(&self, xyz: FNx3View<'_>) -> FNx3 {
448 let mut frac = self.to_frac(xyz);
449 let n = frac.nrows();
450 for i in 0..n {
451 for d in 0..3 {
452 if self.pbc[d] {
453 frac[[i, d]] -= frac[[i, d]].floor();
454 }
455 }
456 }
457 self.to_cart(frac.view())
458 }
459
460 pub fn get_corners(&self) -> FNx3 {
461 let l = self.lengths();
462 let (ox, oy, oz) = (self.origin[0], self.origin[1], self.origin[2]);
463 let (lx, ly, lz) = (l[0], l[1], l[2]);
464 array![
465 [ox, oy, oz],
466 [ox + lx, oy, oz],
467 [ox + lx, oy + ly, oz],
468 [ox, oy + ly, oz],
469 [ox, oy, oz + lz],
470 [ox + lx, oy, oz + lz],
471 [ox + lx, oy + ly, oz + lz],
472 [ox, oy + ly, oz + lz],
473 ]
474 }
475}
476
477impl Region for SimBox {
478 fn bounds(&self) -> FNx3 {
479 let lengths = self.lengths();
480 let mut b = Array2::zeros((3, 2));
481 for d in 0..3 {
482 b[[d, 0]] = self.origin[d];
483 b[[d, 1]] = self.origin[d] + lengths[d];
484 }
485 b
486 }
487
488 fn contains(&self, points: &FNx3) -> Array1<bool> {
489 self.isin(points.view())
490 }
491
492 fn contains_point(&self, point: &[F; 3]) -> bool {
493 let r = ArrayView1::from_shape(3, point).expect("contains_point shape");
494 let dr = &r - &self.origin.view();
495 let frac = self.inv.dot(&dr);
496 (0..3).all(|d| frac[d] >= 0.0 && frac[d] < 1.0)
497 }
498}
499
500fn detect_box_kind(h: &F3x3) -> BoxKind {
501 let eps: F = 1e-12;
502 let is_ortho = h[[0, 1]].abs() < eps
503 && h[[0, 2]].abs() < eps
504 && h[[1, 0]].abs() < eps
505 && h[[1, 2]].abs() < eps
506 && h[[2, 0]].abs() < eps
507 && h[[2, 1]].abs() < eps;
508 if is_ortho {
509 let len = array![h[[0, 0]], h[[1, 1]], h[[2, 2]]];
510 let inv_len = array![1.0 / len[0], 1.0 / len[1], 1.0 / len[2]];
511 BoxKind::Ortho { len, inv_len }
512 } else {
513 BoxKind::Triclinic
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 fn assert_close(a: F, b: F) {
522 assert!((a - b).abs() < 1e-6 as F, "{} != {}", a, b);
523 }
524
525 #[test]
526 fn cell_defined_distinct_from_pbc() {
527 let defined = SimBox::ortho(
529 array![2.0, 2.0, 2.0],
530 array![0.0, 0.0, 0.0],
531 [false, false, false],
532 )
533 .unwrap();
534 assert!(defined.is_cell_defined());
535 assert!(defined.is_free()); assert_close(defined.volume(), 8.0); let nocell = SimBox::new_cell(
541 ndarray::Array2::eye(3),
542 array![0.0, 0.0, 0.0],
543 [false, false, false],
544 false,
545 )
546 .unwrap();
547 assert!(!nocell.is_cell_defined());
548 let pts = array![[1.0, 2.0, 3.0]];
550 assert_eq!(nocell.wrap(pts.view()), pts);
551 }
552
553 #[test]
554 fn roundtrip_frac_cart() {
555 let bx = SimBox::ortho(
556 array![2.0, 3.0, 4.0],
557 array![0.5, -1.0, 2.0],
558 [true, true, true],
559 )
560 .expect("invalid box lengths");
561 let pts = array![[0.5, -1.0, 2.0], [2.5, 2.0, 6.0]];
562 let frac = bx.to_frac(pts.view());
563 let cart = bx.to_cart(frac.view());
564 assert!((&pts - &cart).iter().all(|v| v.abs() < 1e-5));
565 }
566
567 #[test]
568 fn wrap_into_cell() {
569 let bx = SimBox::cube(2.0, array![0.0, 0.0, 0.0], [true, true, true])
570 .expect("invalid box length");
571 let pts = array![[2.1, -0.1, 3.9], [-1.9, 4.2, 0.0]];
572 let wrapped = bx.wrap(pts.view());
573 let frac = bx.to_frac(wrapped.view());
574 for i in 0..wrapped.nrows() {
575 let fx = frac[[i, 0]];
576 let fy = frac[[i, 1]];
577 let fz = frac[[i, 2]];
578 assert!((0.0..1.0).contains(&fx));
579 assert!((0.0..1.0).contains(&fy));
580 assert!((0.0..1.0).contains(&fz));
581 }
582 }
583
584 #[test]
585 fn calc_distance_matches_components() {
586 let bx = SimBox::cube(3.0, array![0.0, 0.0, 0.0], [true, true, true])
587 .expect("invalid box length");
588 let a = array![0.1, 0.2, 0.3];
589 let b = array![2.9, 0.2, 0.3];
590 let d2 = bx.calc_distance2(a.view(), b.view());
591 let dr = bx.shortest_vector(a.view(), b.view());
592 let expected = dr.dot(&dr);
593 assert!((d2 - expected).abs() < 1e-6);
594 }
595
596 #[test]
597 fn test_lengths_ortho() {
598 let bx = SimBox::ortho(
599 array![2.0, 4.0, 5.0],
600 array![0.0, 0.0, 0.0],
601 [true, true, true],
602 )
603 .expect("invalid box lengths");
604 let lengths = bx.lengths();
605 assert_close(lengths[0], 2.0);
606 assert_close(lengths[1], 4.0);
607 assert_close(lengths[2], 5.0);
608 }
609
610 #[test]
611 fn test_tilts_values() {
612 let h = array![[2.0, 1.0, 2.0], [0.0, 4.0, 3.0], [0.0, 0.0, 5.0]];
613 let bx = SimBox::new(h, array![0.0, 0.0, 0.0], [true, true, true]).expect("invalid box");
614 let tilts = bx.tilts();
615 assert_close(tilts[0], 1.0);
616 assert_close(tilts[1], 2.0);
617 assert_close(tilts[2], 3.0);
618 }
619
620 #[test]
621 fn test_volume() {
622 let bx = SimBox::ortho(
623 array![2.0, 3.0, 4.0],
624 array![0.0, 0.0, 0.0],
625 [true, true, true],
626 )
627 .expect("invalid box lengths");
628 assert_close(bx.volume(), 24.0);
629 }
630
631 #[test]
632 fn test_wrap_single_and_multi() {
633 let bx = SimBox::cube(2.0, array![0.0, 0.0, 0.0], [true, true, true])
634 .expect("invalid box length");
635 let pts = array![[10.0, -5.0, -5.0], [0.0, 0.5, 0.0]];
636 let wrapped = bx.wrap(pts.view());
637 assert_close(wrapped[[0, 0]], 0.0);
638 assert_close(wrapped[[0, 1]], 1.0);
639 assert_close(wrapped[[0, 2]], 1.0);
640 assert_close(wrapped[[1, 0]], 0.0);
641 assert_close(wrapped[[1, 1]], 0.5);
642 assert_close(wrapped[[1, 2]], 0.0);
643 }
644
645 #[test]
646 fn test_fractional_and_cartesian() {
647 let bx = SimBox::cube(2.0, array![0.0, 0.0, 0.0], [true, true, true])
648 .expect("invalid box length");
649 let p = array![-1.0, -1.0, -1.0];
650 let frac = bx.make_fractional(p.view());
651 assert_close(frac[0], 0.5);
652 assert_close(frac[1], 0.5);
653 assert_close(frac[2], 0.5);
654 let cart = bx.make_cartesian(frac.view());
655 assert_close(cart[0], 1.0);
656 assert_close(cart[1], 1.0);
657 assert_close(cart[2], 1.0);
658 }
659
660 #[test]
661 fn test_to_frac_to_cart_roundtrip() {
662 let bx = SimBox::ortho(
663 array![2.0, 3.0, 4.0],
664 array![1.0, 2.0, 3.0],
665 [true, true, true],
666 )
667 .expect("invalid box lengths");
668 let pts = array![[1.0, 2.0, 3.0], [2.0, 3.0, 4.0]];
669 let frac = bx.to_frac(pts.view());
670 let cart = bx.to_cart(frac.view());
671 for i in 0..pts.nrows() {
672 for j in 0..3 {
673 assert_close(pts[[i, j]], cart[[i, j]]);
674 }
675 }
676 }
677
678 #[test]
679 fn test_shortest_vector_and_distance() {
680 let bx = SimBox::cube(2.0, array![0.0, 0.0, 0.0], [true, true, true])
681 .expect("invalid box length");
682 let a = array![0.1, 0.0, 0.0];
683 let b = array![1.9, 0.0, 0.0];
684 let dr = bx.shortest_vector(a.view(), b.view());
685 assert_close(dr[0], -0.2);
686 assert_close(dr[1], 0.0);
687 assert_close(dr[2], 0.0);
688 let d2 = bx.calc_distance2(a.view(), b.view());
689 assert_close(d2, 0.04);
690 }
691
692 #[test]
693 fn test_contains_point_non_pbc() {
694 let bx = SimBox::cube(2.0, array![0.0, 0.0, 0.0], [false, false, false])
695 .expect("invalid box length");
696 assert!(bx.contains_point(&[0.5, 0.5, 0.5]));
697 assert!(!bx.contains_point(&[-0.1, 0.5, 0.5]));
698 assert!(!bx.contains_point(&[2.1, 0.5, 0.5]));
699 }
700
701 #[test]
702 fn test_contains_mask() {
703 let bx = SimBox::cube(2.0, array![0.0, 0.0, 0.0], [true, true, true])
704 .expect("invalid box length");
705 let pts = array![[0.1, 0.1, 0.1], [2.1, 0.0, 0.0], [-0.1, 0.0, 0.0]];
706 let mask = bx.contains(&pts);
707 assert!(mask[0]);
708 assert!(!mask[1]);
709 assert!(!mask[2]);
710 }
711
712 #[test]
713 fn test_simbox_free_basic() {
714 let pts = array![[1.0 as F, 2.0, 3.0], [4.0, 5.0, 6.0]];
715 let bx = SimBox::free(pts.view(), 1.0).unwrap();
716 assert_eq!(bx.pbc(), [false, false, false]);
717 let o = bx.origin_view();
719 assert!((o[0] - 0.0).abs() < 1e-5);
720 assert!((o[1] - 1.0).abs() < 1e-5);
721 assert!((o[2] - 2.0).abs() < 1e-5);
722 let l = bx.lengths();
724 assert!((l[0] - 5.0).abs() < 1e-5);
725 assert!((l[1] - 5.0).abs() < 1e-5);
726 assert!((l[2] - 5.0).abs() < 1e-5);
727 }
728
729 #[test]
730 fn test_simbox_free_single_point() {
731 let pts = array![[1.0 as F, 2.0, 3.0]];
732 let bx = SimBox::free(pts.view(), 2.0).unwrap();
733 assert_eq!(bx.pbc(), [false, false, false]);
734 let l = bx.lengths();
736 assert!(l[0] >= 2.0);
737 assert!(l[1] >= 2.0);
738 assert!(l[2] >= 2.0);
739 }
740
741 #[test]
742 fn test_simbox_free_empty() {
743 use ndarray::Array2;
744 let pts = Array2::<F>::zeros((0, 3));
745 let bx = SimBox::free(pts.view(), 1.0).unwrap();
746 assert_eq!(bx.pbc(), [false, false, false]);
747 }
748
749 #[test]
750 fn test_simbox_pbc_accessor() {
751 let bx = SimBox::cube(1.0, array![0.0 as F, 0.0, 0.0], [true, false, true]).unwrap();
752 assert_eq!(bx.pbc(), [true, false, true]);
753 }
754}