Skip to main content

molpack/restraint/geometric/
bounded.rs

1//! Bounded-region restraints (Packmol kinds 2–9): cube, box, sphere, ellipsoid.
2//!
3//! Moved verbatim from the original single-file `src/restraint.rs`; each
4//! `impl AtomRestraint` resolves the trait through the unchanged
5//! `crate::restraint` path.
6
7use crate::restraint::AtomRestraint;
8use molrs::types::F;
9
10/// Packmol kind 2 — quadratic penalty forcing atom inside axis-aligned cube.
11#[derive(Debug, Clone, Copy)]
12pub struct InsideCubeRestraint {
13    pub origin: [F; 3],
14    pub side: F,
15}
16
17impl InsideCubeRestraint {
18    pub fn new(origin: [F; 3], side: F) -> Self {
19        Self { origin, side }
20    }
21}
22
23impl AtomRestraint for InsideCubeRestraint {
24    fn f(&self, pos: &[F; 3], scale: F, _scale2: F) -> F {
25        let (x, y, z) = (pos[0], pos[1], pos[2]);
26        let (xmin, ymin, zmin) = (self.origin[0], self.origin[1], self.origin[2]);
27        let (xmax, ymax, zmax) = (xmin + self.side, ymin + self.side, zmin + self.side);
28        let a1 = (x - xmin).min(0.0);
29        let a2 = (y - ymin).min(0.0);
30        let a3 = (z - zmin).min(0.0);
31        let mut f = scale * (a1 * a1 + a2 * a2 + a3 * a3);
32        let a1 = (x - xmax).max(0.0);
33        let a2 = (y - ymax).max(0.0);
34        let a3 = (z - zmax).max(0.0);
35        f += scale * (a1 * a1 + a2 * a2 + a3 * a3);
36        f
37    }
38
39    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
40        let (x, y, z) = (pos[0], pos[1], pos[2]);
41        let (xmin, ymin, zmin) = (self.origin[0], self.origin[1], self.origin[2]);
42        let (xmax, ymax, zmax) = (xmin + self.side, ymin + self.side, zmin + self.side);
43        let a1 = x - xmin;
44        let a2 = y - ymin;
45        let a3 = z - zmin;
46        if a1 < 0.0 {
47            g[0] += scale * 2.0 * a1;
48        }
49        if a2 < 0.0 {
50            g[1] += scale * 2.0 * a2;
51        }
52        if a3 < 0.0 {
53            g[2] += scale * 2.0 * a3;
54        }
55        let a1 = x - xmax;
56        let a2 = y - ymax;
57        let a3 = z - zmax;
58        if a1 > 0.0 {
59            g[0] += scale * 2.0 * a1;
60        }
61        if a2 > 0.0 {
62            g[1] += scale * 2.0 * a2;
63        }
64        if a3 > 0.0 {
65            g[2] += scale * 2.0 * a3;
66        }
67        self.f(pos, scale, scale2)
68    }
69}
70
71/// Packmol kind 3 — quadratic penalty forcing atom inside axis-aligned box.
72///
73/// `periodic[k] == true` marks axis `k` as periodic: the pair-kernel
74/// minimum-image wrap uses this box's extent for axis `k`, and the cell
75/// list on axis `k` wraps instead of clamping. The soft confinement
76/// penalty (`f` / `fg`) is always active regardless of `periodic` — a
77/// periodic axis still keeps atoms in the reference image via the
78/// penalty; `periodic` only affects pair-distance and cell lookup.
79#[derive(Debug, Clone, Copy)]
80pub struct InsideBoxRestraint {
81    pub min: [F; 3],
82    pub max: [F; 3],
83    pub periodic: [bool; 3],
84}
85
86impl InsideBoxRestraint {
87    /// Construct a box restraint with explicit bounds and per-axis
88    /// periodicity flags. Pass `[false; 3]` for a purely-confining box;
89    /// `[true; 3]` for a fully-periodic box; mixed flags give slab
90    /// geometries (e.g. `[true, true, false]` = XY-periodic slab).
91    pub fn new(min: [F; 3], max: [F; 3], periodic: [bool; 3]) -> Self {
92        Self { min, max, periodic }
93    }
94
95    /// Construct an axis-aligned cubic box from an origin and isotropic
96    /// side length, with per-axis periodicity flags.
97    pub fn cube_from_origin(origin: [F; 3], side: F, periodic: [bool; 3]) -> Self {
98        Self {
99            min: origin,
100            max: [origin[0] + side, origin[1] + side, origin[2] + side],
101            periodic,
102        }
103    }
104
105    /// Construct an axis-aligned box from a molrs-core `SimBox`.
106    ///
107    /// The restraint bounds are `origin` → `origin + lengths`; caller
108    /// supplies the `periodic` flags explicitly (the `SimBox` type does
109    /// not currently carry per-axis periodicity). Off-axis cells
110    /// (triclinic tilts) are not representable as an axis-aligned
111    /// restraint — write a custom `impl AtomRestraint` for those.
112    pub fn from_simbox(simbox: &molrs::spatial::region::SimBox, periodic: [bool; 3]) -> Self {
113        let origin = simbox.origin_view();
114        let lengths = simbox.lengths();
115        let o = [origin[0], origin[1], origin[2]];
116        Self {
117            min: o,
118            max: [o[0] + lengths[0], o[1] + lengths[1], o[2] + lengths[2]],
119            periodic,
120        }
121    }
122}
123
124impl AtomRestraint for InsideBoxRestraint {
125    fn f(&self, pos: &[F; 3], scale: F, _scale2: F) -> F {
126        let (x, y, z) = (pos[0], pos[1], pos[2]);
127        let a1 = (x - self.min[0]).min(0.0);
128        let a2 = (y - self.min[1]).min(0.0);
129        let a3 = (z - self.min[2]).min(0.0);
130        let mut f = scale * (a1 * a1 + a2 * a2 + a3 * a3);
131        let a1 = (x - self.max[0]).max(0.0);
132        let a2 = (y - self.max[1]).max(0.0);
133        let a3 = (z - self.max[2]).max(0.0);
134        f += scale * (a1 * a1 + a2 * a2 + a3 * a3);
135        f
136    }
137
138    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
139        let (x, y, z) = (pos[0], pos[1], pos[2]);
140        let a1 = x - self.min[0];
141        let a2 = y - self.min[1];
142        let a3 = z - self.min[2];
143        if a1 < 0.0 {
144            g[0] += scale * 2.0 * a1;
145        }
146        if a2 < 0.0 {
147            g[1] += scale * 2.0 * a2;
148        }
149        if a3 < 0.0 {
150            g[2] += scale * 2.0 * a3;
151        }
152        let a1 = x - self.max[0];
153        let a2 = y - self.max[1];
154        let a3 = z - self.max[2];
155        if a1 > 0.0 {
156            g[0] += scale * 2.0 * a1;
157        }
158        if a2 > 0.0 {
159            g[1] += scale * 2.0 * a2;
160        }
161        if a3 > 0.0 {
162            g[2] += scale * 2.0 * a3;
163        }
164        self.f(pos, scale, scale2)
165    }
166
167    fn periodic_box(&self) -> Option<([F; 3], [F; 3], [bool; 3])> {
168        if self.periodic.iter().any(|&p| p) {
169            Some((self.min, self.max, self.periodic))
170        } else {
171            None
172        }
173    }
174}
175
176/// Packmol kind 4 — quadratic penalty forcing atom inside sphere.
177#[derive(Debug, Clone, Copy)]
178pub struct InsideSphereRestraint {
179    pub center: [F; 3],
180    pub radius: F,
181}
182
183impl InsideSphereRestraint {
184    pub fn new(center: [F; 3], radius: F) -> Self {
185        Self { center, radius }
186    }
187}
188
189impl AtomRestraint for InsideSphereRestraint {
190    fn f(&self, pos: &[F; 3], _scale: F, scale2: F) -> F {
191        let (x, y, z) = (pos[0], pos[1], pos[2]);
192        let c = self.center;
193        let w = (x - c[0]).powi(2) + (y - c[1]).powi(2) + (z - c[2]).powi(2) - self.radius.powi(2);
194        let a1 = w.max(0.0);
195        scale2 * a1 * a1
196    }
197
198    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
199        let (x, y, z) = (pos[0], pos[1], pos[2]);
200        let c = self.center;
201        let d = (x - c[0]).powi(2) + (y - c[1]).powi(2) + (z - c[2]).powi(2) - self.radius.powi(2);
202        if d > 0.0 {
203            g[0] += 4.0 * scale2 * (x - c[0]) * d;
204            g[1] += 4.0 * scale2 * (y - c[1]) * d;
205            g[2] += 4.0 * scale2 * (z - c[2]) * d;
206        }
207        self.f(pos, scale, scale2)
208    }
209}
210
211/// Packmol kind 5 — quadratic penalty forcing atom inside ellipsoid.
212#[derive(Debug, Clone, Copy)]
213pub struct InsideEllipsoidRestraint {
214    pub center: [F; 3],
215    pub axes: [F; 3],
216    pub exponent: F,
217}
218
219impl InsideEllipsoidRestraint {
220    pub fn new(center: [F; 3], axes: [F; 3], exponent: F) -> Self {
221        // Each axis halves the penalty via division by `axes[k]²`; a zero axis
222        // produces NaN/inf at eval time. The contract is positive semi-axes.
223        debug_assert!(
224            axes.iter().all(|&a| a > 0.0),
225            "ellipsoid semi-axes must be strictly positive, got {axes:?}"
226        );
227        Self {
228            center,
229            axes,
230            exponent,
231        }
232    }
233}
234
235impl AtomRestraint for InsideEllipsoidRestraint {
236    fn f(&self, pos: &[F; 3], _scale: F, scale2: F) -> F {
237        let (x, y, z) = (pos[0], pos[1], pos[2]);
238        let c = self.center;
239        let ax = self.axes;
240        let a1 = (x - c[0]).powi(2) / ax[0].powi(2);
241        let a2 = (y - c[1]).powi(2) / ax[1].powi(2);
242        let a3 = (z - c[2]).powi(2) / ax[2].powi(2);
243        let w = a1 + a2 + a3 - self.exponent.powi(2);
244        let v = w.max(0.0);
245        scale2 * v * v
246    }
247
248    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
249        let (x, y, z) = (pos[0], pos[1], pos[2]);
250        let c = self.center;
251        let a1 = x - c[0];
252        let b1 = y - c[1];
253        let c1 = z - c[2];
254        let a2 = self.axes[0].powi(2);
255        let b2 = self.axes[1].powi(2);
256        let c2 = self.axes[2].powi(2);
257        let d = a1.powi(2) / a2 + b1.powi(2) / b2 + c1.powi(2) / c2 - self.exponent.powi(2);
258        if d > 0.0 {
259            g[0] += scale2 * 4.0 * d * a1 / a2;
260            g[1] += scale2 * 4.0 * d * b1 / b2;
261            g[2] += scale2 * 4.0 * d * c1 / c2;
262        }
263        self.f(pos, scale, scale2)
264    }
265}
266
267/// Packmol kind 6 — linear penalty forcing atom outside axis-aligned cube.
268#[derive(Debug, Clone, Copy)]
269pub struct OutsideCubeRestraint {
270    pub origin: [F; 3],
271    pub side: F,
272}
273
274impl OutsideCubeRestraint {
275    pub fn new(origin: [F; 3], side: F) -> Self {
276        Self { origin, side }
277    }
278}
279
280impl AtomRestraint for OutsideCubeRestraint {
281    fn f(&self, pos: &[F; 3], scale: F, _scale2: F) -> F {
282        let (x, y, z) = (pos[0], pos[1], pos[2]);
283        let (xmin, ymin, zmin) = (self.origin[0], self.origin[1], self.origin[2]);
284        let (xmax, ymax, zmax) = (xmin + self.side, ymin + self.side, zmin + self.side);
285        if x > xmin && x < xmax && y > ymin && y < ymax && z > zmin && z < zmax {
286            let xmed = (xmax - xmin) / 2.0;
287            let ymed = (ymax - ymin) / 2.0;
288            let zmed = (zmax - zmin) / 2.0;
289            let a1 = if x <= xmin + xmed { x - xmin } else { xmax - x };
290            let a2 = if y <= ymin + ymed { y - ymin } else { ymax - y };
291            let a3 = if z <= zmin + zmed { z - zmin } else { zmax - z };
292            scale * (a1 + a2 + a3)
293        } else {
294            0.0
295        }
296    }
297
298    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
299        let (x, y, z) = (pos[0], pos[1], pos[2]);
300        let (xmin, ymin, zmin) = (self.origin[0], self.origin[1], self.origin[2]);
301        let (xmax, ymax, zmax) = (xmin + self.side, ymin + self.side, zmin + self.side);
302        if x > xmin && x < xmax && y > ymin && y < ymax && z > zmin && z < zmax {
303            let xmed = (xmax - xmin) / 2.0;
304            let ymed = (ymax - ymin) / 2.0;
305            let zmed = (zmax - zmin) / 2.0;
306            let (a1, a4) = if x <= xmin + xmed {
307                (1.0, 0.0)
308            } else {
309                (0.0, -1.0)
310            };
311            let (a2, a5) = if y <= ymin + ymed {
312                (1.0, 0.0)
313            } else {
314                (0.0, -1.0)
315            };
316            let (a3, a6) = if z <= zmin + zmed {
317                (1.0, 0.0)
318            } else {
319                (0.0, -1.0)
320            };
321            g[0] += scale * (a1 + a4);
322            g[1] += scale * (a2 + a5);
323            g[2] += scale * (a3 + a6);
324        }
325        self.f(pos, scale, scale2)
326    }
327}
328
329/// Packmol kind 7 — linear penalty forcing atom outside axis-aligned box.
330#[derive(Debug, Clone, Copy)]
331pub struct OutsideBoxRestraint {
332    pub min: [F; 3],
333    pub max: [F; 3],
334}
335
336impl OutsideBoxRestraint {
337    pub fn new(min: [F; 3], max: [F; 3]) -> Self {
338        Self { min, max }
339    }
340}
341
342impl AtomRestraint for OutsideBoxRestraint {
343    fn f(&self, pos: &[F; 3], scale: F, _scale2: F) -> F {
344        let (x, y, z) = (pos[0], pos[1], pos[2]);
345        let (xmin, ymin, zmin) = (self.min[0], self.min[1], self.min[2]);
346        let (xmax, ymax, zmax) = (self.max[0], self.max[1], self.max[2]);
347        if x > xmin && x < xmax && y > ymin && y < ymax && z > zmin && z < zmax {
348            let xmed = (xmax - xmin) / 2.0;
349            let ymed = (ymax - ymin) / 2.0;
350            let zmed = (zmax - zmin) / 2.0;
351            let a1 = if x <= xmin + xmed { x - xmin } else { xmax - x };
352            let a2 = if y <= ymin + ymed { y - ymin } else { ymax - y };
353            let a3 = if z <= zmin + zmed { z - zmin } else { zmax - z };
354            scale * (a1 + a2 + a3)
355        } else {
356            0.0
357        }
358    }
359
360    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
361        let (x, y, z) = (pos[0], pos[1], pos[2]);
362        let (xmin, ymin, zmin) = (self.min[0], self.min[1], self.min[2]);
363        let (xmax, ymax, zmax) = (self.max[0], self.max[1], self.max[2]);
364        if x > xmin && x < xmax && y > ymin && y < ymax && z > zmin && z < zmax {
365            let xmed = (xmax - xmin) / 2.0;
366            let ymed = (ymax - ymin) / 2.0;
367            let zmed = (zmax - zmin) / 2.0;
368            let (a1, a4) = if x <= xmin + xmed {
369                (1.0, 0.0)
370            } else {
371                (0.0, -1.0)
372            };
373            let (a2, a5) = if y <= ymin + ymed {
374                (1.0, 0.0)
375            } else {
376                (0.0, -1.0)
377            };
378            let (a3, a6) = if z <= zmin + zmed {
379                (1.0, 0.0)
380            } else {
381                (0.0, -1.0)
382            };
383            g[0] += scale * (a1 + a4);
384            g[1] += scale * (a2 + a5);
385            g[2] += scale * (a3 + a6);
386        }
387        self.f(pos, scale, scale2)
388    }
389}
390
391/// Packmol kind 8 — quadratic penalty forcing atom outside sphere.
392#[derive(Debug, Clone, Copy)]
393pub struct OutsideSphereRestraint {
394    pub center: [F; 3],
395    pub radius: F,
396}
397
398impl OutsideSphereRestraint {
399    pub fn new(center: [F; 3], radius: F) -> Self {
400        Self { center, radius }
401    }
402}
403
404impl AtomRestraint for OutsideSphereRestraint {
405    fn f(&self, pos: &[F; 3], _scale: F, scale2: F) -> F {
406        let (x, y, z) = (pos[0], pos[1], pos[2]);
407        let c = self.center;
408        let w = (x - c[0]).powi(2) + (y - c[1]).powi(2) + (z - c[2]).powi(2) - self.radius.powi(2);
409        let a1 = w.min(0.0);
410        scale2 * a1 * a1
411    }
412
413    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
414        let (x, y, z) = (pos[0], pos[1], pos[2]);
415        let c = self.center;
416        let d = (x - c[0]).powi(2) + (y - c[1]).powi(2) + (z - c[2]).powi(2) - self.radius.powi(2);
417        if d < 0.0 {
418            g[0] += 4.0 * scale2 * (x - c[0]) * d;
419            g[1] += 4.0 * scale2 * (y - c[1]) * d;
420            g[2] += 4.0 * scale2 * (z - c[2]) * d;
421        }
422        self.f(pos, scale, scale2)
423    }
424}
425
426/// Packmol kind 9 — quadratic penalty forcing atom outside ellipsoid.
427///
428/// Both `f` and `fg` apply the `scale2` factor — `f` previously omitted
429/// it (a transcription artefact of the original Fortran-port comment),
430/// which left `f` and `fg` 100× out of phase at the default
431/// `scale2 = 0.01` and made the optimizer's gradient look 100× flatter
432/// than the function value reported. The `scale2` factor here matches
433/// the documented "quadratic penalty group" convention (kinds 4 / 5 /
434/// 8 / 9 / 12 / 13) and the sister kind-5 [`InsideEllipsoidRestraint`].
435#[derive(Debug, Clone, Copy)]
436pub struct OutsideEllipsoidRestraint {
437    pub center: [F; 3],
438    pub axes: [F; 3],
439    pub exponent: F,
440}
441
442impl OutsideEllipsoidRestraint {
443    pub fn new(center: [F; 3], axes: [F; 3], exponent: F) -> Self {
444        // Each axis halves the penalty via division by `axes[k]²`; a zero axis
445        // produces NaN/inf at eval time. The contract is positive semi-axes.
446        debug_assert!(
447            axes.iter().all(|&a| a > 0.0),
448            "ellipsoid semi-axes must be strictly positive, got {axes:?}"
449        );
450        Self {
451            center,
452            axes,
453            exponent,
454        }
455    }
456}
457
458impl AtomRestraint for OutsideEllipsoidRestraint {
459    fn f(&self, pos: &[F; 3], _scale: F, scale2: F) -> F {
460        let (x, y, z) = (pos[0], pos[1], pos[2]);
461        let c = self.center;
462        let ax = self.axes;
463        let a1 = (x - c[0]).powi(2) / ax[0].powi(2);
464        let a2 = (y - c[1]).powi(2) / ax[1].powi(2);
465        let a3 = (z - c[2]).powi(2) / ax[2].powi(2);
466        let w = a1 + a2 + a3 - self.exponent.powi(2);
467        let v = w.min(0.0);
468        scale2 * v * v
469    }
470
471    fn fg(&self, pos: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
472        let (x, y, z) = (pos[0], pos[1], pos[2]);
473        let c = self.center;
474        let a1 = x - c[0];
475        let b1 = y - c[1];
476        let c1 = z - c[2];
477        let a2 = self.axes[0].powi(2);
478        let b2 = self.axes[1].powi(2);
479        let c2 = self.axes[2].powi(2);
480        let d = a1.powi(2) / a2 + b1.powi(2) / b2 + c1.powi(2) / c2 - self.exponent.powi(2);
481        if d < 0.0 {
482            let ds = scale2 * d;
483            g[0] += 4.0 * ds * a1 / a2;
484            g[1] += 4.0 * ds * b1 / b2;
485            g[2] += 4.0 * ds * c1 / c2;
486        }
487        self.f(pos, scale, scale2)
488    }
489}