1use crate::storage_dtype::{FloatArray1, FloatArray2};
42use crate::types::{Bond, ConFrame, ConFrameBuilder};
43use rustc_hash::FxHashMap;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum IdentityCheck {
52 #[default]
54 Strict,
55 Lenient,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum CorrespondenceError {
62 EmptySequence,
64 Duplicate {
66 frame_index: usize,
67 atom_id: u64,
68 atom_index: usize,
70 first_index: usize,
71 },
72 Missing {
74 frame_index: usize,
75 atom_id: u64,
76 reference_index: usize,
78 },
79 Extra {
81 frame_index: usize,
82 atom_id: u64,
83 atom_index: usize,
84 },
85 Symbol {
86 frame_index: usize,
87 atom_id: u64,
88 atom_index: usize,
89 reference_symbol: String,
90 found_symbol: String,
91 },
92 Mass {
93 frame_index: usize,
94 atom_id: u64,
95 atom_index: usize,
96 reference_mass: f64,
97 found_mass: f64,
98 },
99 Fixed {
100 frame_index: usize,
101 atom_id: u64,
102 atom_index: usize,
103 reference_fixed: [bool; 3],
104 found_fixed: [bool; 3],
105 },
106 SingularCell {
108 determinant: f64,
109 },
110 NonFiniteDelta,
111 NonFiniteCoordinate {
112 frame_index: usize,
113 atom_id: u64,
114 atom_index: usize,
115 },
116 SectionLength {
117 section: &'static str,
118 rows: usize,
119 atoms: usize,
120 },
121 BondEndpoint {
122 index: u32,
123 atoms: usize,
124 },
125}
126
127impl CorrespondenceError {
128 pub fn frame_index(&self) -> Option<usize> {
130 match self {
131 Self::EmptySequence | Self::SingularCell { .. } | Self::NonFiniteDelta => None,
132 Self::Duplicate { frame_index, .. }
133 | Self::Missing { frame_index, .. }
134 | Self::Extra { frame_index, .. }
135 | Self::Symbol { frame_index, .. }
136 | Self::Mass { frame_index, .. }
137 | Self::Fixed { frame_index, .. }
138 | Self::NonFiniteCoordinate { frame_index, .. } => Some(*frame_index),
139 Self::SectionLength { .. } | Self::BondEndpoint { .. } => None,
140 }
141 }
142
143 pub fn atom_id(&self) -> Option<u64> {
145 match self {
146 Self::Duplicate { atom_id, .. }
147 | Self::Missing { atom_id, .. }
148 | Self::Extra { atom_id, .. }
149 | Self::Symbol { atom_id, .. }
150 | Self::Mass { atom_id, .. }
151 | Self::Fixed { atom_id, .. }
152 | Self::NonFiniteCoordinate { atom_id, .. } => Some(*atom_id),
153 _ => None,
154 }
155 }
156
157 pub fn atom_index(&self) -> Option<usize> {
162 match self {
163 Self::Duplicate { atom_index, .. }
164 | Self::Extra { atom_index, .. }
165 | Self::Symbol { atom_index, .. }
166 | Self::Mass { atom_index, .. }
167 | Self::Fixed { atom_index, .. }
168 | Self::NonFiniteCoordinate { atom_index, .. } => Some(*atom_index),
169 Self::Missing {
170 reference_index, ..
171 } => Some(*reference_index),
172 _ => None,
173 }
174 }
175}
176
177impl std::fmt::Display for CorrespondenceError {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 match self {
180 Self::EmptySequence => {
181 write!(f, "atom-id consistency check needs at least one frame")
182 }
183 Self::Duplicate {
184 frame_index,
185 atom_id,
186 atom_index,
187 first_index,
188 } => write!(
189 f,
190 "duplicate atom_id {atom_id} in frame {frame_index} at index {atom_index} (first index {first_index})"
191 ),
192 Self::Missing {
193 frame_index,
194 atom_id,
195 reference_index,
196 } => write!(
197 f,
198 "frame {frame_index} is missing atom_id {atom_id} (reference index {reference_index})"
199 ),
200 Self::Extra {
201 frame_index,
202 atom_id,
203 atom_index,
204 } => write!(
205 f,
206 "frame {frame_index} atom_id {atom_id} at index {atom_index} is not in the reference frame"
207 ),
208 Self::Symbol {
209 frame_index,
210 atom_id,
211 atom_index,
212 reference_symbol,
213 found_symbol,
214 } => write!(
215 f,
216 "frame {frame_index} atom_id {atom_id} at index {atom_index} has symbol {found_symbol}; reference symbol is {reference_symbol}"
217 ),
218 Self::Mass {
219 frame_index,
220 atom_id,
221 atom_index,
222 reference_mass,
223 found_mass,
224 } => write!(
225 f,
226 "frame {frame_index} atom_id {atom_id} at index {atom_index} has mass {found_mass}; reference mass is {reference_mass} (tolerance {})",
227 ConFrameBuilder::TYPE_MASS_ABS_TOL
228 ),
229 Self::Fixed {
230 frame_index,
231 atom_id,
232 atom_index,
233 reference_fixed,
234 found_fixed,
235 } => write!(
236 f,
237 "frame {frame_index} atom_id {atom_id} at index {atom_index} has fixed mask {found_fixed:?}; reference fixed mask is {reference_fixed:?}"
238 ),
239 Self::SingularCell { determinant } => write!(
240 f,
241 "minimum-image displacement: reference cell matrix is singular (determinant {determinant})"
242 ),
243 Self::NonFiniteDelta => {
244 write!(
245 f,
246 "minimum-image displacement: cartesian delta is non-finite"
247 )
248 }
249 Self::NonFiniteCoordinate {
250 frame_index,
251 atom_id,
252 atom_index,
253 } => write!(
254 f,
255 "frame {frame_index} atom_id {atom_id} at index {atom_index} has a non-finite coordinate"
256 ),
257 Self::SectionLength {
258 section,
259 rows,
260 atoms,
261 } => write!(f, "{section} has {rows} rows for {atoms} atoms"),
262 Self::BondEndpoint { index, atoms } => {
263 write!(
264 f,
265 "bond endpoint {index} is outside the atom list ({atoms} atoms)"
266 )
267 }
268 }
269 }
270}
271
272impl std::error::Error for CorrespondenceError {}
273
274pub fn atom_id_permutation(
279 reference: &ConFrame,
280 other: &ConFrame,
281 check: IdentityCheck,
282) -> Result<Vec<usize>, CorrespondenceError> {
283 atom_id_permutation_at(reference, other, 1, check)
284}
285
286fn atom_id_permutation_at(
287 reference: &ConFrame,
288 other: &ConFrame,
289 other_index: usize,
290 check: IdentityCheck,
291) -> Result<Vec<usize>, CorrespondenceError> {
292 index_ids(&reference.atom_data, 0)?;
293 let other_index_map = index_ids(&other.atom_data, other_index)?;
294 let ref_mass = masses_of(reference)?;
295 let other_mass = masses_of(other)?;
296 let mut perm = Vec::with_capacity(reference.atom_data.len());
297 for (ri, atom) in reference.atom_data.iter().enumerate() {
298 let Some(&bi) = other_index_map.get(&atom.atom_id) else {
299 return Err(CorrespondenceError::Missing {
300 frame_index: other_index,
301 atom_id: atom.atom_id,
302 reference_index: ri,
303 });
304 };
305 let found = &other.atom_data[bi];
306 if found.symbol != atom.symbol {
307 return Err(CorrespondenceError::Symbol {
308 frame_index: other_index,
309 atom_id: atom.atom_id,
310 atom_index: bi,
311 reference_symbol: atom.symbol.to_string(),
312 found_symbol: found.symbol.to_string(),
313 });
314 }
315 if check == IdentityCheck::Strict {
316 let reference_mass = ref_mass[ri];
317 let found_mass = other_mass[bi];
318 if !reference_mass.is_finite()
319 || !found_mass.is_finite()
320 || (reference_mass - found_mass).abs() > ConFrameBuilder::TYPE_MASS_ABS_TOL
321 {
322 return Err(CorrespondenceError::Mass {
323 frame_index: other_index,
324 atom_id: atom.atom_id,
325 atom_index: bi,
326 reference_mass,
327 found_mass,
328 });
329 }
330 if found.fixed() != atom.fixed() {
331 return Err(CorrespondenceError::Fixed {
332 frame_index: other_index,
333 atom_id: atom.atom_id,
334 atom_index: bi,
335 reference_fixed: atom.fixed(),
336 found_fixed: found.fixed(),
337 });
338 }
339 }
340 perm.push(bi);
341 }
342 if other.atom_data.len() != reference.atom_data.len() {
343 let mut seen = vec![false; other.atom_data.len()];
344 for &bi in &perm {
345 seen[bi] = true;
346 }
347 for (i, atom) in other.atom_data.iter().enumerate() {
348 if !seen[i] {
349 return Err(CorrespondenceError::Extra {
350 frame_index: other_index,
351 atom_id: atom.atom_id,
352 atom_index: i,
353 });
354 }
355 }
356 }
357 Ok(perm)
358}
359
360fn index_ids(
361 atoms: &[crate::types::AtomDatum],
362 frame_index: usize,
363) -> Result<FxHashMap<u64, usize>, CorrespondenceError> {
364 let mut map = FxHashMap::with_capacity_and_hasher(atoms.len(), Default::default());
365 for (i, atom) in atoms.iter().enumerate() {
366 if let Some(first) = map.insert(atom.atom_id, i) {
367 return Err(CorrespondenceError::Duplicate {
368 frame_index,
369 atom_id: atom.atom_id,
370 atom_index: i,
371 first_index: first,
372 });
373 }
374 }
375 Ok(map)
376}
377
378fn masses_of(frame: &ConFrame) -> Result<Vec<f64>, CorrespondenceError> {
379 let n = frame.atom_data.len();
380 if frame.masses.len() != n {
381 return Err(CorrespondenceError::SectionLength {
382 section: "masses",
383 rows: frame.masses.len(),
384 atoms: n,
385 });
386 }
387 Ok((0..n).map(|i| frame.masses.get_f64(i)).collect())
388}
389
390pub fn reorder_like(
398 reference: &ConFrame,
399 other: &ConFrame,
400 check: IdentityCheck,
401) -> Result<ConFrame, CorrespondenceError> {
402 let perm = atom_id_permutation(reference, other, check)?;
403 apply_permutation(other, &perm)
404}
405
406fn apply_permutation(frame: &ConFrame, perm: &[usize]) -> Result<ConFrame, CorrespondenceError> {
407 let n = perm.len();
408 let mut out = frame.clone();
409 let mut atoms = Vec::with_capacity(n);
410 for &j in perm {
411 let mut atom = frame.atom_data[j].clone();
412 let p = position_of(frame, j)?;
413 atom.set_x(p[0]);
414 atom.set_y(p[1]);
415 atom.set_z(p[2]);
416 overlay_sections(&mut atom, frame, j)?;
417 atoms.push(atom);
418 }
419 out.atom_data = atoms;
420 out.positions = permute_rows(&frame.positions, perm, "positions")?;
421 out.velocities = permute_rows(&frame.velocities, perm, "velocities")?.into();
422 out.forces = permute_rows(&frame.forces, perm, "forces")?.into();
423 out.magmoms = permute_rows(&frame.magmoms, perm, "magmoms")?.into();
424 out.displacements = permute_rows(&frame.displacements, perm, "displacements")?.into();
425 out.spreads = permute_rows(&frame.spreads, perm, "spreads")?.into();
426 out.atom_energies = permute_col(&frame.atom_energies, perm, "energies")?.into();
427 out.charges = permute_col(&frame.charges, perm, "charges")?.into();
428 out.spins = permute_col(&frame.spins, perm, "spins")?.into();
429 out.masses = permute_col(&frame.masses, perm, "masses")?;
430 let ids: Vec<u64> = perm.iter().map(|&j| frame.atom_data[j].atom_id).collect();
431 out.atom_ids = ndarray::Array1::from_vec(ids).into_shared();
432 if frame.positions.nrows() == n {
435 for (i, atom) in out.atom_data.iter().enumerate() {
436 out.positions.set_f64_row(i, [atom.x(), atom.y(), atom.z()]);
437 }
438 }
439 refresh_type_runs(&mut out);
440 remap_bonds(&mut out, perm)?;
441 Ok(out)
442}
443
444fn position_of(frame: &ConFrame, i: usize) -> Result<[f64; 3], CorrespondenceError> {
445 let n = frame.atom_data.len();
446 if frame.positions.nrows() == n {
447 return Ok(frame.positions.as_f64_row(i));
448 }
449 if frame.positions.nrows() == 0 {
450 let atom = &frame.atom_data[i];
451 return Ok([atom.x(), atom.y(), atom.z()]);
452 }
453 Err(CorrespondenceError::SectionLength {
454 section: "positions",
455 rows: frame.positions.nrows(),
456 atoms: n,
457 })
458}
459
460fn overlay_sections(
461 atom: &mut crate::types::AtomDatum,
462 frame: &ConFrame,
463 j: usize,
464) -> Result<(), CorrespondenceError> {
465 let n = frame.atom_data.len();
466 if frame.velocities.nrows() == n {
467 atom.set_velocity(Some(frame.velocities.as_f64_row(j)));
468 } else if frame.velocities.nrows() != 0 {
469 return Err(CorrespondenceError::SectionLength {
470 section: "velocities",
471 rows: frame.velocities.nrows(),
472 atoms: n,
473 });
474 }
475 if frame.forces.nrows() == n {
476 atom.set_force(Some(frame.forces.as_f64_row(j)));
477 } else if frame.forces.nrows() != 0 {
478 return Err(CorrespondenceError::SectionLength {
479 section: "forces",
480 rows: frame.forces.nrows(),
481 atoms: n,
482 });
483 }
484 if frame.atom_energies.len() == n {
485 atom.set_energy(Some(frame.atom_energies.get_f64(j)));
486 } else if !frame.atom_energies.is_empty() {
487 return Err(CorrespondenceError::SectionLength {
488 section: "energies",
489 rows: frame.atom_energies.len(),
490 atoms: n,
491 });
492 }
493 if frame.charges.len() == n {
494 atom.set_charge(Some(frame.charges.get_f64(j)));
495 } else if !frame.charges.is_empty() {
496 return Err(CorrespondenceError::SectionLength {
497 section: "charges",
498 rows: frame.charges.len(),
499 atoms: n,
500 });
501 }
502 if frame.spins.len() == n {
503 atom.set_spin(Some(frame.spins.get_f64(j)));
504 } else if !frame.spins.is_empty() {
505 return Err(CorrespondenceError::SectionLength {
506 section: "spins",
507 rows: frame.spins.len(),
508 atoms: n,
509 });
510 }
511 if frame.magmoms.nrows() == n {
512 atom.set_magmom(Some(frame.magmoms.as_f64_row(j)));
513 } else if frame.magmoms.nrows() != 0 {
514 return Err(CorrespondenceError::SectionLength {
515 section: "magmoms",
516 rows: frame.magmoms.nrows(),
517 atoms: n,
518 });
519 }
520 if frame.displacements.nrows() == n {
521 atom.set_displacement(Some(frame.displacements.as_f64_row(j)));
522 } else if frame.displacements.nrows() != 0 {
523 return Err(CorrespondenceError::SectionLength {
524 section: "displacements",
525 rows: frame.displacements.nrows(),
526 atoms: n,
527 });
528 }
529 if frame.spreads.nrows() == n {
530 atom.set_spread(Some(frame.spreads.as_f64_row(j)));
531 } else if frame.spreads.nrows() != 0 {
532 return Err(CorrespondenceError::SectionLength {
533 section: "spreads",
534 rows: frame.spreads.nrows(),
535 atoms: n,
536 });
537 }
538 Ok(())
539}
540
541fn permute_rows(
542 src: &FloatArray2,
543 perm: &[usize],
544 section: &'static str,
545) -> Result<FloatArray2, CorrespondenceError> {
546 let n = perm.len();
547 if src.nrows() == 0 {
548 return Ok(FloatArray2::zeros(src.kind(), 0, 3));
549 }
550 if src.nrows() != n {
551 return Err(CorrespondenceError::SectionLength {
552 section,
553 rows: src.nrows(),
554 atoms: n,
555 });
556 }
557 let mut out = FloatArray2::zeros(src.kind(), n, src.ncols().max(3));
558 for (i, &j) in perm.iter().enumerate() {
559 out.set_f64_row(i, src.as_f64_row(j));
560 }
561 Ok(out)
562}
563
564fn permute_col(
565 src: &FloatArray1,
566 perm: &[usize],
567 section: &'static str,
568) -> Result<FloatArray1, CorrespondenceError> {
569 let n = perm.len();
570 if src.is_empty() {
571 return Ok(FloatArray1::zeros(src.kind(), 0));
572 }
573 if src.len() != n {
574 return Err(CorrespondenceError::SectionLength {
575 section,
576 rows: src.len(),
577 atoms: n,
578 });
579 }
580 let mut out = FloatArray1::zeros(src.kind(), n);
581 for (i, &j) in perm.iter().enumerate() {
582 out.set_f64(i, src.get_f64(j));
583 }
584 Ok(out)
585}
586
587fn refresh_type_runs(frame: &mut ConFrame) {
588 let n = frame.atom_data.len();
589 if n == 0 {
590 frame.header.natm_types = 0;
591 frame.header.natms_per_type.clear();
592 frame.header.masses_per_type.clear();
593 return;
594 }
595 let mut counts = Vec::new();
596 let mut masses = Vec::new();
597 let mut run_symbol = frame.atom_data[0].symbol.clone();
598 let mut run_count = 0usize;
599 let mut run_mass = frame.masses.get_f64(0);
600 for i in 0..n {
601 if frame.atom_data[i].symbol != run_symbol {
602 counts.push(run_count);
603 masses.push(run_mass);
604 run_symbol = frame.atom_data[i].symbol.clone();
605 run_count = 0;
606 run_mass = frame.masses.get_f64(i);
607 }
608 run_count += 1;
609 }
610 counts.push(run_count);
611 masses.push(run_mass);
612 frame.header.natm_types = counts.len();
613 frame.header.natms_per_type = counts;
614 frame.header.masses_per_type = masses;
615}
616
617fn remap_bonds(frame: &mut ConFrame, perm: &[usize]) -> Result<(), CorrespondenceError> {
618 let bonds = frame.bonds();
619 if bonds.is_empty() {
620 return Ok(());
621 }
622 let n = perm.len();
623 let mut inverse = vec![0usize; n];
624 for (i, &j) in perm.iter().enumerate() {
625 inverse[j] = i;
626 }
627 let mut mapped = Vec::with_capacity(bonds.len());
628 for bond in bonds {
629 if bond.i as usize >= n {
630 return Err(CorrespondenceError::BondEndpoint {
631 index: bond.i,
632 atoms: n,
633 });
634 }
635 if bond.j as usize >= n {
636 return Err(CorrespondenceError::BondEndpoint {
637 index: bond.j,
638 atoms: n,
639 });
640 }
641 mapped.push(Bond {
642 i: inverse[bond.i as usize] as u32,
643 j: inverse[bond.j as usize] as u32,
644 order: bond.order,
645 });
646 }
647 frame.header.set_bonds(&mapped);
648 Ok(())
649}
650
651pub fn check_atom_id_consistency(
656 frames: &[ConFrame],
657 check: IdentityCheck,
658) -> Result<(), CorrespondenceError> {
659 let refs: Vec<&ConFrame> = frames.iter().collect();
660 check_atom_id_consistency_refs(&refs, check)
661}
662
663pub(crate) fn check_atom_id_consistency_refs(
664 frames: &[&ConFrame],
665 check: IdentityCheck,
666) -> Result<(), CorrespondenceError> {
667 let Some(reference) = frames.first() else {
668 return Err(CorrespondenceError::EmptySequence);
669 };
670 if frames.len() == 1 {
671 index_ids(&reference.atom_data, 0)?;
672 return Ok(());
673 }
674 for (i, frame) in frames.iter().enumerate().skip(1) {
675 atom_id_permutation_at(reference, frame, i, check)?;
676 }
677 Ok(())
678}
679
680pub fn minimum_image_displacements(
687 reference: &ConFrame,
688 other: &ConFrame,
689 check: IdentityCheck,
690) -> Result<Vec<[f64; 3]>, CorrespondenceError> {
691 let perm = atom_id_permutation(reference, other, check)?;
692 let cell = reference.header.cell_matrix();
693 let pbc = reference.header.pbc_effective();
694 let mut out = Vec::with_capacity(perm.len());
695 for (i, &j) in perm.iter().enumerate() {
696 let ra = position_of(reference, i)?;
697 let rb = position_of(other, j)?;
698 if !finite3(ra) {
699 return Err(CorrespondenceError::NonFiniteCoordinate {
700 frame_index: 0,
701 atom_id: reference.atom_data[i].atom_id,
702 atom_index: i,
703 });
704 }
705 if !finite3(rb) {
706 return Err(CorrespondenceError::NonFiniteCoordinate {
707 frame_index: 1,
708 atom_id: other.atom_data[j].atom_id,
709 atom_index: j,
710 });
711 }
712 let delta = [rb[0] - ra[0], rb[1] - ra[1], rb[2] - ra[2]];
713 out.push(minimum_image_delta(cell, pbc, delta)?);
714 }
715 Ok(out)
716}
717
718fn finite3(v: [f64; 3]) -> bool {
719 v.iter().all(|x| x.is_finite())
720}
721
722pub fn minimum_image_delta(
729 cell: [[f64; 3]; 3],
730 pbc: [bool; 3],
731 delta: [f64; 3],
732) -> Result<[f64; 3], CorrespondenceError> {
733 if !finite3(delta) {
734 return Err(CorrespondenceError::NonFiniteDelta);
735 }
736 let (inv, _) = invert_cell(cell).ok_or(CorrespondenceError::SingularCell {
737 determinant: det3(cell),
738 })?;
739 let mut frac = mul_row(delta, inv);
740 for axis in 0..3 {
741 if pbc[axis] {
742 frac[axis] -= frac[axis].round();
743 }
744 }
745 Ok(mul_row(frac, cell))
746}
747
748fn det3(m: [[f64; 3]; 3]) -> f64 {
749 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
750 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
751 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
752}
753
754fn invert_cell(m: [[f64; 3]; 3]) -> Option<([[f64; 3]; 3], f64)> {
755 let det = det3(m);
756 if !det.is_finite() || det.abs() < 1e-18 {
757 return None;
758 }
759 let inv_det = 1.0 / det;
760 let c00 = m[1][1] * m[2][2] - m[1][2] * m[2][1];
761 let c01 = -(m[1][0] * m[2][2] - m[1][2] * m[2][0]);
762 let c02 = m[1][0] * m[2][1] - m[1][1] * m[2][0];
763 let c10 = -(m[0][1] * m[2][2] - m[0][2] * m[2][1]);
764 let c11 = m[0][0] * m[2][2] - m[0][2] * m[2][0];
765 let c12 = -(m[0][0] * m[2][1] - m[0][1] * m[2][0]);
766 let c20 = m[0][1] * m[1][2] - m[0][2] * m[1][1];
767 let c21 = -(m[0][0] * m[1][2] - m[0][2] * m[1][0]);
768 let c22 = m[0][0] * m[1][1] - m[0][1] * m[1][0];
769 let inv = [
770 [c00 * inv_det, c10 * inv_det, c20 * inv_det],
771 [c01 * inv_det, c11 * inv_det, c21 * inv_det],
772 [c02 * inv_det, c12 * inv_det, c22 * inv_det],
773 ];
774 Some((inv, det))
775}
776
777fn mul_row(v: [f64; 3], m: [[f64; 3]; 3]) -> [f64; 3] {
779 [
780 v[0] * m[0][0] + v[1] * m[1][0] + v[2] * m[2][0],
781 v[0] * m[0][1] + v[1] * m[1][1] + v[2] * m[2][1],
782 v[0] * m[0][2] + v[1] * m[1][2] + v[2] * m[2][2],
783 ]
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use crate::types::cell_vectors;
790
791 fn close(a: [f64; 3], b: [f64; 3]) {
792 for k in 0..3 {
793 assert!(
794 (a[k] - b[k]).abs() < 1e-9,
795 "component {k}: {} vs {}",
796 a[k],
797 b[k]
798 );
799 }
800 }
801
802 #[test]
803 fn orthogonal_wrap_uses_the_cell_matrix() {
804 let cell = cell_vectors([10.0, 12.0, 14.0], [90.0, 90.0, 90.0]);
805 let wrapped = minimum_image_delta(cell, [true, true, true], [9.0, 0.0, 0.0]).unwrap();
806 close(wrapped, [-1.0, 0.0, 0.0]);
807 let open = minimum_image_delta(cell, [false, true, true], [9.0, 9.0, 0.0]).unwrap();
808 close(open, [9.0, -3.0, 0.0]);
809 }
810
811 #[test]
812 fn a_lattice_vector_wraps_to_zero() {
813 let cell = cell_vectors([10.0, 12.0, 14.0], [80.0, 90.0, 70.0]);
814 let along_a = minimum_image_delta(cell, [true, true, true], cell[0]).unwrap();
815 close(along_a, [0.0, 0.0, 0.0]);
816 let along_b = minimum_image_delta(cell, [true, true, true], cell[1]).unwrap();
817 close(along_b, [0.0, 0.0, 0.0]);
818 }
819
820 #[test]
821 fn a_flat_cell_is_rejected() {
822 let err = minimum_image_delta([[0.0; 3]; 3], [true; 3], [1.0, 0.0, 0.0]).unwrap_err();
823 assert!(matches!(err, CorrespondenceError::SingularCell { .. }));
824 assert!(err.to_string().contains("singular"));
825 }
826}