1use std::sync::Arc;
4
5use crate::cell::{cell_ind, icell_to_cell, index_cell};
6use crate::constraints::{Constraints, EvalMode, EvalOutput};
7use crate::restraint::{AtomRestraint, Restraint};
8use molrs::Element;
9use molrs::types::F;
10
11use super::model::ModelData;
12use super::state::{RuntimeState, RuntimeStateMut};
13use super::work_buffers::WorkBuffers;
14
15pub type RestraintRef = usize;
17
18pub const ATOM_FLAG_FIXED: u32 = 1 << 0;
20pub const ATOM_FLAG_SHORT: u32 = 1 << 1;
23
24pub const NONE_IDX: u32 = u32::MAX;
32
33#[repr(C)]
54#[derive(Clone, Copy, Debug, Default)]
55pub struct AtomProps {
56 pub ibmol: u32,
57 pub ibtype: u32,
58 pub fscale: F,
59 pub radius: F,
60 pub radius_ini: F,
61 pub flags: u32,
62 _padding: u32,
67}
68
69pub const ATOM_PROPS_SIZE: usize = 40;
75const _ATOM_PROPS_IS_40_BYTES: [(); ATOM_PROPS_SIZE] = [(); std::mem::size_of::<AtomProps>()];
76
77const NEIGHBOR_OFFSETS_F: [(isize, isize, isize); 13] = [
79 (1, 0, 0),
80 (0, 1, 0),
81 (0, 0, 1),
82 (1, -1, 0),
83 (1, 0, -1),
84 (0, 1, -1),
85 (0, 1, 1),
86 (1, 1, 0),
87 (1, 0, 1),
88 (1, -1, -1),
89 (1, -1, 1),
90 (1, 1, -1),
91 (1, 1, 1),
92];
93
94const NEIGHBOR_OFFSETS_G: [(isize, isize, isize); 13] = [
96 (1, 0, 0),
97 (0, 1, 0),
98 (0, 0, 1),
99 (0, 1, 1),
100 (0, 1, -1),
101 (1, 1, 0),
102 (1, 0, 1),
103 (1, -1, 0),
104 (1, 0, -1),
105 (1, 1, 1),
106 (1, 1, -1),
107 (1, -1, 1),
108 (1, -1, -1),
109];
110
111pub struct PackContext {
114 pub constraints: Constraints,
116
117 pub xcart: Vec<[F; 3]>,
120 pub elements: Vec<Option<Element>>,
122
123 pub coor: Vec<[F; 3]>,
126
127 pub radius: Vec<F>,
130 pub radius_ini: Vec<F>,
132 pub fscale: Vec<F>,
134
135 pub use_short_radius: Vec<bool>,
137 pub short_radius: Vec<F>,
138 pub short_radius_scale: Vec<F>,
139 pub any_short_radius: bool,
144 pub any_fixed_atoms: bool,
149 n_fixed_atoms: usize,
153 n_short_radius: usize,
155
156 pub atom_props: Vec<AtomProps>,
165
166 pub fdist: F,
169 pub frest: F,
171 pub fdist_atom: Vec<F>,
173 pub frest_atom: Vec<F>,
175
176 pub nmols: Vec<usize>,
179 pub natoms: Vec<usize>,
181 pub idfirst: Vec<usize>,
183 pub ntype: usize,
185 pub ntype_with_fixed: usize,
187 pub ntotmol: usize,
189 pub ntotat: usize,
191 pub nfixedat: usize,
193
194 pub constrain_rot: Vec<[bool; 3]>,
198 pub rot_bound: Vec<[[F; 2]; 3]>,
201
202 pub restraints: Vec<Arc<dyn AtomRestraint>>,
205 pub iratom_offsets: Vec<usize>,
208 pub iratom_data: Vec<RestraintRef>,
210 pub collective: Vec<(usize, Arc<dyn Restraint>)>,
215
216 pub ibtype: Vec<usize>,
219 pub ibmol: Vec<usize>,
221 pub fixedatom: Vec<bool>,
223 pub comptype: Vec<bool>,
225
226 pub ncells: [usize; 3],
228 pub cell_length: [F; 3],
229 pub pbc_length: [F; 3],
230 pub pbc_min: [F; 3],
231 pub pbc_periodic: [bool; 3],
236
237 pub latomfirst: Vec<u32>,
241 pub latomnext: Vec<u32>,
243 pub latomfix: Vec<u32>,
245 pub lcellfirst: u32,
247 pub lcellnext: Vec<u32>,
249 pub empty_cell: Vec<bool>,
251 pub fixed_cells: Vec<usize>,
253 pub active_cells: Vec<usize>,
255 pub neighbor_cells_f: Vec<[usize; 13]>,
257 pub neighbor_cells_g: Vec<[usize; 13]>,
262
263 pub init1: bool,
266 pub move_flag: bool,
268 pub parallel_pair_eval: bool,
277
278 pub scale: F,
280 pub scale2: F,
281
282 pub sizemin: [F; 3],
284 pub sizemax: [F; 3],
285
286 pub dmax: Vec<F>,
288
289 pub work: WorkBuffers,
291
292 pub frame: molrs::Frame,
296
297 ncf: usize,
299 ncg: usize,
300}
301
302impl PackContext {
303 pub fn new(ntotat: usize, ntotmol: usize, ntype: usize) -> Self {
305 let ncells = [1, 1, 1];
306 let ncell_total = ncells[0] * ncells[1] * ncells[2];
307 debug_assert!(
308 ntotat < NONE_IDX as usize,
309 "ntotat={ntotat} must fit in u32 (< NONE_IDX)"
310 );
311 Self {
312 constraints: Constraints,
313 xcart: vec![[0.0; 3]; ntotat],
314 elements: vec![None; ntotat],
315 coor: Vec::new(),
316 radius: vec![0.0; ntotat],
317 radius_ini: vec![0.0; ntotat],
318 fscale: vec![1.0; ntotat],
319 use_short_radius: vec![false; ntotat],
320 short_radius: vec![0.0; ntotat],
321 short_radius_scale: vec![0.0; ntotat],
322 any_short_radius: false,
323 any_fixed_atoms: false,
324 n_fixed_atoms: 0,
325 n_short_radius: 0,
326 atom_props: vec![AtomProps::default(); ntotat],
327 fdist: 0.0,
328 frest: 0.0,
329 fdist_atom: vec![0.0; ntotat],
330 frest_atom: vec![0.0; ntotat],
331 nmols: Vec::new(),
332 natoms: Vec::new(),
333 idfirst: Vec::new(),
334 ntype,
335 ntype_with_fixed: ntype,
336 ntotmol,
337 ntotat,
338 nfixedat: 0,
339 constrain_rot: vec![[false; 3]; ntype],
340 rot_bound: vec![[[0.0; 2]; 3]; ntype],
341 restraints: Vec::new(),
342 iratom_offsets: vec![0; ntotat + 1],
343 iratom_data: Vec::new(),
344 collective: Vec::new(),
345 ibtype: vec![0; ntotat],
346 ibmol: vec![0; ntotat],
347 fixedatom: vec![false; ntotat],
348 comptype: vec![true; ntype],
349 ncells,
350 cell_length: [1.0; 3],
351 pbc_length: [1.0; 3],
352 pbc_min: [0.0; 3],
353 pbc_periodic: [false; 3],
354 latomfirst: vec![NONE_IDX; ncell_total],
355 latomnext: vec![NONE_IDX; ntotat],
356 latomfix: vec![NONE_IDX; ncell_total],
357 lcellfirst: NONE_IDX,
358 lcellnext: vec![NONE_IDX; ncell_total],
359 empty_cell: vec![true; ncell_total],
360 fixed_cells: Vec::new(),
361 active_cells: Vec::new(),
362 neighbor_cells_f: vec![[0; 13]; ncell_total],
363 neighbor_cells_g: vec![[0; 13]; ncell_total],
364 init1: false,
365 move_flag: false,
366 parallel_pair_eval: false,
367 scale: 1.0,
368 scale2: crate::numerics::DEFAULT_SCALE2,
369 sizemin: [0.0; 3],
370 sizemax: [0.0; 3],
371 dmax: vec![0.0; ntype],
372 work: WorkBuffers::new(ntotat),
373 frame: molrs::Frame::new(),
374 ncf: 0,
375 ncg: 0,
376 }
377 }
378
379 #[inline]
381 pub fn model(&self) -> ModelData<'_> {
382 ModelData { ctx: self }
383 }
384
385 #[inline]
387 pub fn runtime(&self) -> RuntimeState<'_> {
388 RuntimeState { ctx: self }
389 }
390
391 #[inline]
393 pub fn runtime_mut(&mut self) -> RuntimeStateMut<'_> {
394 RuntimeStateMut { ctx: self }
395 }
396
397 #[inline]
399 pub fn evaluate(&mut self, x: &[F], mode: EvalMode, gradient: Option<&mut [F]>) -> EvalOutput {
400 let constraints = self.constraints;
401 constraints.evaluate(x, self, mode, gradient)
402 }
403
404 pub fn resize_cell_arrays(&mut self) {
406 let nc = self.ncells[0] * self.ncells[1] * self.ncells[2];
407 debug_assert!(
408 nc < NONE_IDX as usize,
409 "ncell_total={nc} must fit in u32 (< NONE_IDX)"
410 );
411 self.latomfirst = vec![NONE_IDX; nc];
412 self.latomfix = vec![NONE_IDX; nc];
413 self.lcellnext = vec![NONE_IDX; nc];
414 self.empty_cell = vec![true; nc];
415 self.fixed_cells.clear();
416 self.active_cells.clear();
417 self.neighbor_cells_f = vec![[0; 13]; nc];
418 self.neighbor_cells_g = vec![[0; 13]; nc];
419 self.rebuild_neighbor_cells();
420 }
421
422 pub fn resetcells(&mut self) {
425 self.lcellfirst = NONE_IDX;
426 for &icell in &self.active_cells {
427 self.latomfirst[icell] = NONE_IDX;
428 self.lcellnext[icell] = NONE_IDX;
429 self.empty_cell[icell] = true;
430 }
431 self.active_cells.clear();
432
433 for &icell in &self.fixed_cells {
434 self.latomfirst[icell] = self.latomfix[icell];
435 self.empty_cell[icell] = false;
436 self.lcellnext[icell] = self.lcellfirst;
437 self.lcellfirst = icell as u32;
438 self.active_cells.push(icell);
439 }
440
441 let free_atoms = self.ntotat - self.nfixedat;
443 self.latomnext[..free_atoms].fill(NONE_IDX);
444 }
445
446 #[inline]
447 pub fn reset_eval_counters(&mut self) {
448 self.ncf = 0;
449 self.ncg = 0;
450 }
451
452 pub fn sync_atom_props(&mut self) {
460 let n = self.ntotat;
461 if self.atom_props.len() != n {
462 self.atom_props.resize(n, AtomProps::default());
463 }
464 let mut n_fixed = 0usize;
465 let mut n_short = 0usize;
466 for i in 0..n {
467 let fixed = self.fixedatom[i];
468 let use_short = self.use_short_radius[i];
469 if fixed {
470 n_fixed += 1;
471 }
472 if use_short {
473 n_short += 1;
474 }
475 let mut flags = 0u32;
476 if fixed {
477 flags |= ATOM_FLAG_FIXED;
478 }
479 if use_short {
480 flags |= ATOM_FLAG_SHORT;
481 }
482 self.atom_props[i] = AtomProps {
483 ibmol: self.ibmol[i] as u32,
484 ibtype: self.ibtype[i] as u32,
485 flags,
486 _padding: 0,
487 fscale: self.fscale[i],
488 radius: self.radius[i],
489 radius_ini: self.radius_ini[i],
490 };
491 }
492 self.n_fixed_atoms = n_fixed;
493 self.n_short_radius = n_short;
494 self.any_fixed_atoms = n_fixed > 0;
495 self.any_short_radius = n_short > 0;
496 }
497
498 #[inline]
503 pub fn set_radius(&mut self, i: usize, value: F) {
504 self.radius[i] = value;
505 if i < self.atom_props.len() {
506 self.atom_props[i].radius = value;
507 }
508 }
509
510 #[inline]
513 pub fn set_fscale(&mut self, i: usize, value: F) {
514 self.fscale[i] = value;
515 if i < self.atom_props.len() {
516 self.atom_props[i].fscale = value;
517 }
518 }
519
520 #[inline]
524 pub fn set_fixed_atom(&mut self, i: usize, is_fixed: bool) {
525 let was_fixed = self.fixedatom[i];
526 if was_fixed == is_fixed {
527 return;
528 }
529 self.fixedatom[i] = is_fixed;
530 if i < self.atom_props.len() {
531 let flags = &mut self.atom_props[i].flags;
532 if is_fixed {
533 *flags |= ATOM_FLAG_FIXED;
534 } else {
535 *flags &= !ATOM_FLAG_FIXED;
536 }
537 }
538 if is_fixed {
539 self.n_fixed_atoms += 1;
540 } else {
541 self.n_fixed_atoms -= 1;
542 }
543 self.any_fixed_atoms = self.n_fixed_atoms > 0;
544 }
545
546 #[inline]
549 pub fn set_use_short_radius(&mut self, i: usize, use_short: bool) {
550 let was = self.use_short_radius[i];
551 if was == use_short {
552 return;
553 }
554 self.use_short_radius[i] = use_short;
555 if i < self.atom_props.len() {
556 let flags = &mut self.atom_props[i].flags;
557 if use_short {
558 *flags |= ATOM_FLAG_SHORT;
559 } else {
560 *flags &= !ATOM_FLAG_SHORT;
561 }
562 }
563 if use_short {
564 self.n_short_radius += 1;
565 } else {
566 self.n_short_radius -= 1;
567 }
568 self.any_short_radius = self.n_short_radius > 0;
569 }
570
571 #[inline]
573 pub fn set_ibmol(&mut self, i: usize, value: usize) {
574 self.ibmol[i] = value;
575 if i < self.atom_props.len() {
576 self.atom_props[i].ibmol = value as u32;
577 }
578 }
579
580 #[inline]
582 pub fn set_ibtype(&mut self, i: usize, value: usize) {
583 self.ibtype[i] = value;
584 if i < self.atom_props.len() {
585 self.atom_props[i].ibtype = value as u32;
586 }
587 }
588
589 #[inline(always)]
600 pub fn debug_assert_atom_props_sync(&self) {
601 if !cfg!(debug_assertions) {
602 return;
603 }
604 let n = self.ntotat;
605 assert_eq!(
606 self.atom_props.len(),
607 n,
608 "atom_props length {} != ntotat {} — call sync_atom_props after a resize",
609 self.atom_props.len(),
610 n
611 );
612 let mut n_fixed = 0usize;
613 let mut n_short = 0usize;
614 for i in 0..n {
615 let ap = &self.atom_props[i];
616 let expected_fixed = self.fixedatom[i];
617 let expected_short = self.use_short_radius[i];
618 let expected_flags = if expected_fixed { ATOM_FLAG_FIXED } else { 0 }
619 | if expected_short { ATOM_FLAG_SHORT } else { 0 };
620 if expected_fixed {
621 n_fixed += 1;
622 }
623 if expected_short {
624 n_short += 1;
625 }
626 assert_eq!(
627 ap.ibmol, self.ibmol[i] as u32,
628 "atom_props[{i}].ibmol drift: mirror={} vec={}",
629 ap.ibmol, self.ibmol[i]
630 );
631 assert_eq!(
632 ap.ibtype, self.ibtype[i] as u32,
633 "atom_props[{i}].ibtype drift"
634 );
635 assert_eq!(
636 ap.fscale, self.fscale[i],
637 "atom_props[{i}].fscale drift — did you write sys.fscale[{i}] directly?"
638 );
639 assert_eq!(
640 ap.radius, self.radius[i],
641 "atom_props[{i}].radius drift — use set_radius()"
642 );
643 assert_eq!(
644 ap.radius_ini, self.radius_ini[i],
645 "atom_props[{i}].radius_ini drift"
646 );
647 assert_eq!(
648 ap.flags, expected_flags,
649 "atom_props[{i}].flags drift — did you write sys.fixedatom/use_short_radius directly?"
650 );
651 }
652 assert_eq!(
653 self.n_fixed_atoms, n_fixed,
654 "n_fixed_atoms counter drift: stored={} derived={}",
655 self.n_fixed_atoms, n_fixed
656 );
657 assert_eq!(
658 self.n_short_radius, n_short,
659 "n_short_radius counter drift: stored={} derived={}",
660 self.n_short_radius, n_short
661 );
662 assert_eq!(self.any_fixed_atoms, n_fixed > 0);
663 assert_eq!(self.any_short_radius, n_short > 0);
664 }
665
666 #[inline]
667 pub fn increment_ncf(&mut self) {
668 self.ncf += 1;
669 }
670
671 #[inline]
672 pub fn increment_ncg(&mut self) {
673 self.ncg += 1;
674 }
675
676 #[inline]
677 pub fn ncf(&self) -> usize {
678 self.ncf
679 }
680
681 #[inline]
682 pub fn ncg(&self) -> usize {
683 self.ncg
684 }
685
686 fn rebuild_neighbor_cells(&mut self) {
687 let (nx, ny, nz) = (self.ncells[0], self.ncells[1], self.ncells[2]);
688 let nc = nx * ny * nz;
689 for icell in 0..nc {
690 let cell = icell_to_cell(icell, &self.ncells);
691 let (ci, cj, ck) = (cell[0], cell[1], cell[2]);
692
693 let mut nbs_f = [0usize; 13];
694 for (idx, &(di, dj, dk)) in NEIGHBOR_OFFSETS_F.iter().enumerate() {
695 let ncell = [
696 cell_ind(ci as isize + di, nx),
697 cell_ind(cj as isize + dj, ny),
698 cell_ind(ck as isize + dk, nz),
699 ];
700 nbs_f[idx] = index_cell(&ncell, &self.ncells);
701 }
702 self.neighbor_cells_f[icell] = nbs_f;
703
704 let mut nbs_g = [0usize; 13];
705 for (idx, &(di, dj, dk)) in NEIGHBOR_OFFSETS_G.iter().enumerate() {
706 let ncell = [
707 cell_ind(ci as isize + di, nx),
708 cell_ind(cj as isize + dj, ny),
709 cell_ind(ck as isize + dk, nz),
710 ];
711 nbs_g[idx] = index_cell(&ncell, &self.ncells);
712 }
713 self.neighbor_cells_g[icell] = nbs_g;
714 }
715 }
716}
717
718#[cfg(test)]
719mod atom_props_tests {
720 use super::*;
721
722 fn tiny_ctx(ntotat: usize) -> PackContext {
723 let mut sys = PackContext::new(ntotat, ntotat, 1);
724 for i in 0..ntotat {
725 sys.ibmol[i] = i;
726 sys.ibtype[i] = 0;
727 sys.radius[i] = 1.0;
728 sys.radius_ini[i] = 1.0;
729 sys.fscale[i] = 1.0;
730 }
731 sys.sync_atom_props();
732 sys
733 }
734
735 #[test]
736 fn atom_props_size_is_40_bytes_on_f64() {
737 assert_eq!(std::mem::size_of::<AtomProps>(), 40);
740 assert_eq!(std::mem::align_of::<AtomProps>(), 8);
741 assert_eq!(ATOM_PROPS_SIZE, 40);
742 }
743
744 #[test]
745 fn sync_atom_props_populates_mirror_and_flags() {
746 let mut sys = PackContext::new(3, 3, 1);
747 sys.ibmol = vec![10, 20, 30];
748 sys.ibtype = vec![1, 2, 3];
749 sys.fscale = vec![0.5, 0.25, 0.125];
750 sys.radius = vec![1.1, 2.2, 3.3];
751 sys.radius_ini = vec![1.0, 2.0, 3.0];
752 sys.fixedatom = vec![false, true, false];
753 sys.use_short_radius = vec![false, false, true];
754 sys.sync_atom_props();
755
756 for i in 0..3 {
757 assert_eq!(sys.atom_props[i].ibmol, sys.ibmol[i] as u32);
758 assert_eq!(sys.atom_props[i].ibtype, sys.ibtype[i] as u32);
759 assert_eq!(sys.atom_props[i].fscale, sys.fscale[i]);
760 assert_eq!(sys.atom_props[i].radius, sys.radius[i]);
761 assert_eq!(sys.atom_props[i].radius_ini, sys.radius_ini[i]);
762 }
763 assert_eq!(sys.atom_props[0].flags, 0);
764 assert_eq!(sys.atom_props[1].flags, ATOM_FLAG_FIXED);
765 assert_eq!(sys.atom_props[2].flags, ATOM_FLAG_SHORT);
766 assert!(sys.any_fixed_atoms);
767 assert!(sys.any_short_radius);
768 sys.debug_assert_atom_props_sync();
769 }
770
771 #[test]
772 fn set_radius_keeps_mirror_in_sync() {
773 let mut sys = tiny_ctx(4);
774 sys.set_radius(2, 7.25);
775 assert_eq!(sys.radius[2], 7.25);
776 assert_eq!(sys.atom_props[2].radius, 7.25);
777 assert_eq!(sys.atom_props[0].radius, 1.0);
779 assert_eq!(sys.atom_props[3].radius, 1.0);
780 sys.debug_assert_atom_props_sync();
781 }
782
783 #[test]
784 fn set_fscale_keeps_mirror_in_sync() {
785 let mut sys = tiny_ctx(4);
786 sys.set_fscale(1, 0.125);
787 assert_eq!(sys.fscale[1], 0.125);
788 assert_eq!(sys.atom_props[1].fscale, 0.125);
789 sys.debug_assert_atom_props_sync();
790 }
791
792 #[test]
793 fn set_fixed_atom_updates_mirror_flag_counter_and_summary() {
794 let mut sys = tiny_ctx(3);
795 assert!(!sys.any_fixed_atoms);
796 assert_eq!(sys.n_fixed_atoms, 0);
797
798 sys.set_fixed_atom(1, true);
799 assert!(sys.fixedatom[1]);
800 assert_eq!(sys.atom_props[1].flags & ATOM_FLAG_FIXED, ATOM_FLAG_FIXED);
801 assert_eq!(sys.n_fixed_atoms, 1);
802 assert!(sys.any_fixed_atoms);
803 sys.debug_assert_atom_props_sync();
804
805 sys.set_fixed_atom(1, true);
807 assert_eq!(sys.n_fixed_atoms, 1);
808
809 sys.set_fixed_atom(0, true);
810 assert_eq!(sys.n_fixed_atoms, 2);
811 sys.set_fixed_atom(1, false);
812 assert_eq!(sys.n_fixed_atoms, 1);
813 assert!(sys.any_fixed_atoms);
814 sys.set_fixed_atom(0, false);
815 assert_eq!(sys.n_fixed_atoms, 0);
816 assert!(!sys.any_fixed_atoms);
817 sys.debug_assert_atom_props_sync();
818 }
819
820 #[test]
821 fn set_use_short_radius_updates_mirror_flag_counter_and_summary() {
822 let mut sys = tiny_ctx(3);
823 sys.set_use_short_radius(2, true);
824 assert!(sys.use_short_radius[2]);
825 assert_eq!(sys.atom_props[2].flags & ATOM_FLAG_SHORT, ATOM_FLAG_SHORT);
826 assert_eq!(sys.n_short_radius, 1);
827 assert!(sys.any_short_radius);
828 sys.debug_assert_atom_props_sync();
829
830 sys.set_use_short_radius(2, false);
831 assert_eq!(sys.n_short_radius, 0);
832 assert!(!sys.any_short_radius);
833 assert_eq!(sys.atom_props[2].flags & ATOM_FLAG_SHORT, 0);
834 sys.debug_assert_atom_props_sync();
835 }
836
837 #[test]
838 fn set_fixed_and_short_flags_coexist_on_same_atom() {
839 let mut sys = tiny_ctx(2);
840 sys.set_fixed_atom(0, true);
841 sys.set_use_short_radius(0, true);
842 assert_eq!(
843 sys.atom_props[0].flags,
844 ATOM_FLAG_FIXED | ATOM_FLAG_SHORT,
845 "both flags must combine without clobbering each other"
846 );
847 sys.set_fixed_atom(0, false);
848 assert_eq!(
849 sys.atom_props[0].flags, ATOM_FLAG_SHORT,
850 "clearing FIXED must leave SHORT intact"
851 );
852 sys.debug_assert_atom_props_sync();
853 }
854
855 #[test]
856 fn set_ibmol_and_set_ibtype_keep_mirror_in_sync() {
857 let mut sys = tiny_ctx(3);
858 sys.set_ibmol(1, 42);
859 assert_eq!(sys.ibmol[1], 42);
860 assert_eq!(sys.atom_props[1].ibmol, 42);
861 sys.set_ibtype(2, 7);
862 assert_eq!(sys.ibtype[2], 7);
863 assert_eq!(sys.atom_props[2].ibtype, 7);
864 sys.debug_assert_atom_props_sync();
865 }
866
867 #[test]
877 #[cfg(debug_assertions)]
878 #[should_panic(expected = "atom_props")]
879 fn debug_invariant_catches_direct_fixedatom_write() {
880 let mut sys = tiny_ctx(2);
881 sys.fixedatom[0] = true; sys.debug_assert_atom_props_sync();
883 }
884
885 #[test]
886 #[cfg(debug_assertions)]
887 #[should_panic(expected = "atom_props")]
888 fn debug_invariant_catches_direct_fscale_write() {
889 let mut sys = tiny_ctx(2);
890 sys.fscale[1] = 99.0;
891 sys.debug_assert_atom_props_sync();
892 }
893
894 #[test]
899 fn counters_match_sync_after_mixed_mutations() {
900 let mut sys = tiny_ctx(10);
901 for i in [0usize, 3, 7] {
902 sys.set_fixed_atom(i, true);
903 }
904 for i in [2usize, 5] {
905 sys.set_use_short_radius(i, true);
906 }
907 let pre_n_fixed = sys.n_fixed_atoms;
908 let pre_n_short = sys.n_short_radius;
909
910 sys.sync_atom_props();
912 assert_eq!(sys.n_fixed_atoms, pre_n_fixed);
913 assert_eq!(sys.n_short_radius, pre_n_short);
914 assert_eq!(sys.n_fixed_atoms, 3);
915 assert_eq!(sys.n_short_radius, 2);
916 }
917}