Skip to main content

molpack/
movebad.rs

1//! movebad heuristic and flashsort.
2//! Port of `heuristics.f90` and `flashsort.f90`.
3
4use crate::constraints::EvalMode;
5use crate::context::PackContext;
6use crate::gencan::GencanWorkspace;
7use crate::initial::restmol;
8use crate::random::uniform01;
9use molrs::types::F;
10use rand::Rng;
11
12pub struct MoveBadConfig<'a> {
13    pub movefrac: F,
14    pub maxmove_per_type: &'a [usize],
15    pub movebadrandom: bool,
16    pub gencan_maxit: usize,
17}
18
19/// Move the worst molecules to better positions.
20/// Port of `movebad` from `heuristics.f90`.
21pub fn movebad(
22    x: &mut [F],
23    sys: &mut PackContext,
24    precision: F,
25    cfg: &MoveBadConfig<'_>,
26    rng: &mut impl Rng,
27    workspace: &mut GencanWorkspace,
28) {
29    // Zero per-atom accumulators
30    let ntotat = sys.ntotat;
31    for icart in 0..ntotat {
32        sys.fdist_atom[icart] = 0.0;
33        sys.frest_atom[icart] = 0.0;
34    }
35
36    // Compute f with move flag to collect per-atom scores.
37    // Packmol keeps radius=radius_ini during the whole movebad routine and restores
38    // the working radii only at the end (heuristics.f90 lines 45-47 and 145-147).
39    sys.move_flag = true;
40    sys.work.radiuswork.copy_from_slice(&sys.radius);
41    for i in 0..sys.ntotat {
42        sys.set_radius(i, sys.radius_ini[i]);
43    }
44    sys.evaluate(x, EvalMode::FOnly, None);
45    sys.move_flag = false;
46
47    let ntotmol = sys.ntotmol;
48
49    let mut icart_offset = 0usize;
50    for itype in 0..sys.ntype {
51        if !sys.comptype[itype] {
52            icart_offset += sys.nmols[itype] * sys.natoms[itype];
53            continue;
54        }
55
56        let nmols_itype = sys.nmols[itype];
57        let natoms_itype = sys.natoms[itype];
58
59        // Compute per-molecule violation score
60        sys.work.fmol.clear();
61        sys.work.fmol.resize(nmols_itype, 0.0);
62        let fmol = &mut sys.work.fmol;
63        let mut nbad = 0usize;
64        let mut icart = icart_offset;
65        for score in fmol.iter_mut().take(nmols_itype) {
66            let mut fdist_mol = 0.0 as F;
67            let mut frest_mol = 0.0 as F;
68            for _ in 0..natoms_itype {
69                fdist_mol = fdist_mol.max(sys.fdist_atom[icart]);
70                frest_mol = frest_mol.max(sys.frest_atom[icart]);
71                icart += 1;
72            }
73            if fdist_mol > precision || frest_mol > precision {
74                nbad += 1;
75                *score = fdist_mol + frest_mol;
76            }
77        }
78
79        if nbad == 0 {
80            icart_offset += nmols_itype * natoms_itype;
81            continue;
82        }
83
84        let frac = (nbad as F / nmols_itype as F).min(cfg.movefrac);
85        let nmove_base = (nmols_itype as F * frac) as isize;
86        let nmove = usize::min(
87            cfg.maxmove_per_type[itype],
88            isize::max(nmove_base, 1) as usize,
89        );
90
91        // Sort molecules by violation (flash1 — O(N) histogram sort)
92        let mflash = 1 + nmols_itype / 10;
93        sys.work.flash_ind.clear();
94        sys.work.flash_ind.extend(0..nmols_itype);
95        flash1(fmol, mflash, &mut sys.work.flash_ind, &mut sys.work.flash_l);
96
97        // Molecule offset in x for this type.
98        // Matches Packmol heuristics.f90 lines 105-108:
99        //   if(comptype(i)) imol = imol + nmols(i)  [only for i < itype]
100        // Only ACTIVE types contribute to the offset.
101        // In Phase 1 (compact x, one type active), all earlier types are inactive
102        // so mol_base = 0 — the active type's molecules start at x[0].
103        // In the main loop (full x, all types active), mol_base = sum(nmols[0..itype]).
104        let mol_base: usize = {
105            let mut base = 0usize;
106            for it in 0..itype {
107                if sys.comptype[it] {
108                    base += sys.nmols[it];
109                }
110            }
111            base
112        };
113
114        // Pre-collect (bad, good) molecule index pairs to avoid borrowing
115        // sys.work.flash_ind across the restmol mutable borrow of sys.
116        let move_pairs: Vec<(usize, usize)> = (0..nmove)
117            .map(|k| {
118                let ibad_mol = sys.work.flash_ind[nmols_itype - 1 - k];
119                let igood_mol = sys.work.flash_ind
120                    [(uniform01(rng) * nmols_itype as F * frac) as usize % nmols_itype.max(1)];
121                (ibad_mol, igood_mol)
122            })
123            .collect();
124
125        for &(ibad_mol, igood_mol) in &move_pairs {
126            let ilubar_bad = (mol_base + ibad_mol) * 3;
127            let ilugan_bad = ntotmol * 3 + (mol_base + ibad_mol) * 3;
128            let ilubar_good = (mol_base + igood_mol) * 3;
129            let ilugan_good = ntotmol * 3 + (mol_base + igood_mol) * 3;
130
131            let dmax = sys.dmax[itype];
132            if cfg.movebadrandom {
133                x[ilubar_bad] = sys.sizemin[0] + uniform01(rng) * (sys.sizemax[0] - sys.sizemin[0]);
134                x[ilubar_bad + 1] =
135                    sys.sizemin[1] + uniform01(rng) * (sys.sizemax[1] - sys.sizemin[1]);
136                x[ilubar_bad + 2] =
137                    sys.sizemin[2] + uniform01(rng) * (sys.sizemax[2] - sys.sizemin[2]);
138            } else {
139                // Move bad molecule near good molecule with random perturbation.
140                x[ilubar_bad] = x[ilubar_good] - 0.3 * dmax + 0.6 * uniform01(rng) * dmax;
141                x[ilubar_bad + 1] = x[ilubar_good + 1] - 0.3 * dmax + 0.6 * uniform01(rng) * dmax;
142                x[ilubar_bad + 2] = x[ilubar_good + 2] - 0.3 * dmax + 0.6 * uniform01(rng) * dmax;
143            }
144
145            // Copy angles from good molecule
146            x[ilugan_bad] = x[ilugan_good];
147            x[ilugan_bad + 1] = x[ilugan_good + 1];
148            x[ilugan_bad + 2] = x[ilugan_good + 2];
149
150            // Fit the moved molecule into constraints
151            restmol(
152                itype,
153                ilubar_bad,
154                x,
155                sys,
156                precision,
157                cfg.gencan_maxit,
158                true,
159                workspace,
160            );
161        }
162
163        icart_offset += nmols_itype * natoms_itype;
164    }
165
166    sys.evaluate(x, EvalMode::FOnly, None);
167    for i in 0..sys.ntotat {
168        sys.set_radius(i, sys.work.radiuswork[i]);
169    }
170}
171
172/// Flashsort (O(N) histogram sort) — sort array `a` ascending,
173/// updating index array `ind` to track permutation.
174/// Port of `flash1` from `flashsort.f90`.
175///
176/// `l_buf` is a reusable histogram buffer (resized as needed).
177pub fn flash1(a: &mut [F], m: usize, ind: &mut [usize], l_buf: &mut Vec<usize>) {
178    let n = a.len();
179    if n <= 1 {
180        return;
181    }
182    debug_assert_eq!(ind.len(), n);
183
184    let anmin = a.iter().copied().fold(F::INFINITY, F::min);
185    let nmax_idx = a
186        .iter()
187        .enumerate()
188        .max_by(|(_, x), (_, y)| x.total_cmp(y))
189        .map(|(i, _)| i)
190        .unwrap_or(0);
191
192    if a[0] == a[nmax_idx] {
193        return; // all equal
194    }
195
196    let c1 = (m as F - 1.0) / (a[nmax_idx] - anmin);
197    l_buf.clear();
198    l_buf.resize(m, 0);
199    let l = l_buf;
200
201    for ai in a.iter().take(n) {
202        let k = (c1 * (*ai - anmin)) as usize;
203        let k = k.min(m - 1);
204        l[k] += 1;
205    }
206    for k in 1..m {
207        l[k] += l[k - 1];
208    }
209
210    // Swap a[nmax] and a[0]
211    a.swap(nmax_idx, 0);
212    ind.swap(nmax_idx, 0);
213
214    // Permutation phase
215    let mut nmove = 0usize;
216    let mut j = 0usize;
217    let mut k = m;
218
219    while nmove < n - 1 {
220        while j >= l[k - 1] {
221            j += 1;
222            if j >= n {
223                break;
224            }
225            k = (c1 * (a[j] - anmin)) as usize;
226            k = k.min(m - 1) + 1;
227        }
228        if j >= n {
229            break;
230        }
231        let mut flash = a[j];
232        let mut iflash = ind[j];
233        while j != l[k - 1] {
234            k = (c1 * (flash - anmin)) as usize;
235            k = k.min(m - 1);
236            let lk = l[k] - 1;
237            l[k] -= 1;
238            let hold = a[lk];
239            let ihold = ind[lk];
240            a[lk] = flash;
241            ind[lk] = iflash;
242            flash = hold;
243            iflash = ihold;
244            nmove += 1;
245            // Recompute k for new flash
246            k = (c1 * (flash - anmin)) as usize;
247            k = k.min(m - 1) + 1;
248        }
249    }
250
251    // Insertion sort for cleanup (exact port of Fortran DO I=N-2,1,-1)
252    let mut i = n as isize - 2;
253    while i >= 1 {
254        let iu = i as usize;
255        if a[iu + 1] < a[iu] {
256            let hold = a[iu];
257            let ihold = ind[iu];
258            let mut j = iu;
259            while j + 1 < n && a[j + 1] < hold {
260                a[j] = a[j + 1];
261                ind[j] = ind[j + 1];
262                j += 1;
263            }
264            a[j] = hold;
265            ind[j] = ihold;
266        }
267        i -= 1;
268    }
269}