1use molrs::types::F;
34
35use crate::restraint::AtomRestraint;
36
37#[derive(Debug, Clone, Copy)]
44pub struct Aabb {
45 pub min: [F; 3],
46 pub max: [F; 3],
47}
48
49pub trait Region: Send + Sync + std::fmt::Debug {
51 fn contains(&self, x: &[F; 3]) -> bool;
53
54 fn signed_distance(&self, x: &[F; 3]) -> F;
59
60 fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
66 let h: F = 1e-6;
67 let mut g = [0.0; 3];
68 for k in 0..3 {
69 let mut xp = *x;
70 xp[k] += h;
71 let mut xm = *x;
72 xm[k] -= h;
73 g[k] = (self.signed_distance(&xp) - self.signed_distance(&xm)) / (2.0 * h);
74 }
75 g
76 }
77
78 fn bounding_box(&self) -> Option<Aabb> {
81 None
82 }
83}
84
85#[derive(Debug, Clone, Copy)]
93pub struct And<A, B>(pub A, pub B);
94
95impl<A: Region, B: Region> Region for And<A, B> {
96 fn contains(&self, x: &[F; 3]) -> bool {
97 self.0.contains(x) && self.1.contains(x)
98 }
99 fn signed_distance(&self, x: &[F; 3]) -> F {
100 self.0.signed_distance(x).max(self.1.signed_distance(x))
101 }
102 fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
103 if self.0.signed_distance(x) >= self.1.signed_distance(x) {
104 self.0.signed_distance_grad(x)
105 } else {
106 self.1.signed_distance_grad(x)
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy)]
114pub struct Or<A, B>(pub A, pub B);
115
116impl<A: Region, B: Region> Region for Or<A, B> {
117 fn contains(&self, x: &[F; 3]) -> bool {
118 self.0.contains(x) || self.1.contains(x)
119 }
120 fn signed_distance(&self, x: &[F; 3]) -> F {
121 self.0.signed_distance(x).min(self.1.signed_distance(x))
122 }
123 fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
124 if self.0.signed_distance(x) <= self.1.signed_distance(x) {
125 self.0.signed_distance_grad(x)
126 } else {
127 self.1.signed_distance_grad(x)
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy)]
135pub struct Not<A>(pub A);
136
137impl<A: Region> Region for Not<A> {
138 fn contains(&self, x: &[F; 3]) -> bool {
139 !self.0.contains(x)
140 }
141 fn signed_distance(&self, x: &[F; 3]) -> F {
142 -self.0.signed_distance(x)
143 }
144 fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
145 let g = self.0.signed_distance_grad(x);
146 [-g[0], -g[1], -g[2]]
147 }
148}
149
150pub trait RegionExt: Region + Sized {
164 fn and<B: Region>(self, other: B) -> And<Self, B> {
165 And(self, other)
166 }
167 fn or<B: Region>(self, other: B) -> Or<Self, B> {
168 Or(self, other)
169 }
170 fn not(self) -> Not<Self> {
171 Not(self)
172 }
173
174 fn into_restraint(self) -> RegionRestraint<Self> {
177 RegionRestraint(self)
178 }
179}
180
181impl<R: Region + Sized> RegionExt for R {}
182
183#[derive(Debug, Clone, Copy)]
212pub struct RegionRestraint<R: Region>(pub R);
213
214impl<R: Region + 'static> AtomRestraint for RegionRestraint<R> {
215 fn f(&self, x: &[F; 3], _scale: F, scale2: F) -> F {
216 let d = self.0.signed_distance(x);
217 let v = d.max(0.0);
218 scale2 * v * v
219 }
220
221 fn fg(&self, x: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
222 let d = self.0.signed_distance(x);
223 if d > 0.0 {
224 let grad = self.0.signed_distance_grad(x);
225 let coeff = 2.0 * scale2 * d;
226 g[0] += coeff * grad[0];
227 g[1] += coeff * grad[1];
228 g[2] += coeff * grad[2];
229 }
230 self.f(x, scale, scale2)
231 }
232}
233
234#[derive(Debug, Clone, Copy)]
240pub struct InsideBoxRegion {
241 pub min: [F; 3],
242 pub max: [F; 3],
243}
244
245impl InsideBoxRegion {
246 pub fn new(min: [F; 3], max: [F; 3]) -> Self {
247 Self { min, max }
248 }
249}
250
251impl Region for InsideBoxRegion {
252 fn contains(&self, x: &[F; 3]) -> bool {
253 (0..3).all(|k| x[k] >= self.min[k] && x[k] <= self.max[k])
254 }
255
256 fn signed_distance(&self, x: &[F; 3]) -> F {
257 let mut d = F::NEG_INFINITY;
261 for ((xk, &lo_k), &hi_k) in x.iter().zip(self.min.iter()).zip(self.max.iter()) {
262 d = d.max(lo_k - xk).max(xk - hi_k);
263 }
264 d
265 }
266
267 fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
268 let mut best_d = F::NEG_INFINITY;
270 let mut best_axis = 0usize;
271 let mut best_sign = 0.0 as F;
272 for (k, ((xk, &lo_k), &hi_k)) in x
273 .iter()
274 .zip(self.min.iter())
275 .zip(self.max.iter())
276 .enumerate()
277 {
278 let lo = lo_k - xk; let hi = xk - hi_k; if lo > best_d {
281 best_d = lo;
282 best_axis = k;
283 best_sign = -1.0;
284 }
285 if hi > best_d {
286 best_d = hi;
287 best_axis = k;
288 best_sign = 1.0;
289 }
290 }
291 let mut g = [0.0; 3];
292 g[best_axis] = best_sign;
293 g
294 }
295
296 fn bounding_box(&self) -> Option<Aabb> {
297 Some(Aabb {
298 min: self.min,
299 max: self.max,
300 })
301 }
302}
303
304#[derive(Debug, Clone, Copy)]
306pub struct InsideSphereRegion {
307 pub center: [F; 3],
308 pub radius: F,
309}
310
311impl InsideSphereRegion {
312 pub fn new(center: [F; 3], radius: F) -> Self {
313 Self { center, radius }
314 }
315}
316
317impl Region for InsideSphereRegion {
318 fn contains(&self, x: &[F; 3]) -> bool {
319 let c = self.center;
320 let d2 = (x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2);
321 d2 <= self.radius.powi(2)
322 }
323
324 fn signed_distance(&self, x: &[F; 3]) -> F {
325 let c = self.center;
326 let d = ((x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2)).sqrt();
327 d - self.radius
328 }
329
330 fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
331 let c = self.center;
332 let (dx, dy, dz) = (x[0] - c[0], x[1] - c[1], x[2] - c[2]);
333 let d = (dx * dx + dy * dy + dz * dz).sqrt();
334 if d < 1e-12 {
335 [0.0; 3]
336 } else {
337 [dx / d, dy / d, dz / d]
338 }
339 }
340
341 fn bounding_box(&self) -> Option<Aabb> {
342 let r = self.radius;
343 let c = self.center;
344 Some(Aabb {
345 min: [c[0] - r, c[1] - r, c[2] - r],
346 max: [c[0] + r, c[1] + r, c[2] + r],
347 })
348 }
349}
350
351#[derive(Debug, Clone, Copy)]
354pub struct OutsideSphereRegion {
355 pub center: [F; 3],
356 pub radius: F,
357}
358
359impl OutsideSphereRegion {
360 pub fn new(center: [F; 3], radius: F) -> Self {
361 Self { center, radius }
362 }
363}
364
365impl Region for OutsideSphereRegion {
366 fn contains(&self, x: &[F; 3]) -> bool {
367 let c = self.center;
368 let d2 = (x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2);
369 d2 >= self.radius.powi(2)
370 }
371
372 fn signed_distance(&self, x: &[F; 3]) -> F {
373 let c = self.center;
374 let d = ((x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2)).sqrt();
375 self.radius - d
376 }
377
378 fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
379 let c = self.center;
380 let (dx, dy, dz) = (x[0] - c[0], x[1] - c[1], x[2] - c[2]);
381 let d = (dx * dx + dy * dy + dz * dz).sqrt();
382 if d < 1e-12 {
383 [0.0; 3]
384 } else {
385 [-dx / d, -dy / d, -dz / d]
386 }
387 }
388}
389
390#[cfg(test)]
395mod tests {
396 use super::*;
397
398 const TOL: F = 1e-6;
399
400 #[test]
403 fn and_is_intersection() {
404 let a = InsideBoxRegion::new([0.0; 3], [10.0; 3]);
405 let b = InsideSphereRegion::new([5.0; 3], 4.0);
406 let c = And(a, b);
407 assert!(c.contains(&[5.0, 5.0, 5.0]));
409 assert!(!c.contains(&[1.0, 1.0, 1.0]));
411 assert!(!c.contains(&[-1.0, -1.0, -1.0]));
414 }
415
416 #[test]
417 fn or_is_union() {
418 let a = InsideSphereRegion::new([0.0; 3], 5.0);
419 let b = InsideSphereRegion::new([20.0, 0.0, 0.0], 5.0);
420 let u = Or(a, b);
421 assert!(u.contains(&[0.0, 0.0, 0.0]));
422 assert!(u.contains(&[20.0, 0.0, 0.0]));
423 assert!(!u.contains(&[10.0, 0.0, 0.0]));
424 }
425
426 #[test]
427 fn not_is_complement() {
428 let a = InsideSphereRegion::new([0.0; 3], 5.0);
429 let n = Not(a);
430 assert!(!n.contains(&[0.0, 0.0, 0.0]));
431 assert!(n.contains(&[10.0, 0.0, 0.0]));
432 }
433
434 #[test]
435 fn de_morgan_and() {
436 let a = InsideSphereRegion::new([0.0; 3], 5.0);
438 let b = InsideBoxRegion::new([-3.0; 3], [3.0; 3]);
439 let lhs = Not(And(a, b));
440 let rhs = Or(Not(a), Not(b));
441 for pt in &[
442 [0.0, 0.0, 0.0],
443 [4.0, 0.0, 0.0],
444 [10.0, 0.0, 0.0],
445 [1.0, 1.0, 1.0],
446 ] {
447 assert_eq!(
448 lhs.contains(pt),
449 rhs.contains(pt),
450 "de Morgan mismatch at {pt:?}"
451 );
452 }
453 }
454
455 #[test]
458 fn sphere_signed_distance_sign() {
459 let s = InsideSphereRegion::new([0.0; 3], 5.0);
460 assert!(s.signed_distance(&[0.0, 0.0, 0.0]) < 0.0);
461 assert!((s.signed_distance(&[5.0, 0.0, 0.0])).abs() < TOL);
462 assert!(s.signed_distance(&[10.0, 0.0, 0.0]) > 0.0);
463 }
464
465 #[test]
466 fn box_signed_distance_sign() {
467 let b = InsideBoxRegion::new([0.0; 3], [10.0; 3]);
468 assert!(b.signed_distance(&[5.0, 5.0, 5.0]) < 0.0);
469 assert!(b.signed_distance(&[15.0, 5.0, 5.0]) > 0.0);
470 assert!(b.signed_distance(&[-5.0, 5.0, 5.0]) > 0.0);
471 }
472
473 #[test]
474 fn contains_matches_signed_distance() {
475 let regions: Vec<Box<dyn Region>> = vec![
476 Box::new(InsideSphereRegion::new([0.0; 3], 3.0)),
477 Box::new(InsideBoxRegion::new([0.0; 3], [5.0; 3])),
478 Box::new(OutsideSphereRegion::new([0.0; 3], 2.0)),
479 ];
480 let pts = [
481 [0.0, 0.0, 0.0],
482 [1.0, 1.0, 1.0],
483 [3.0, 3.0, 3.0],
484 [-1.0, 0.0, 0.0],
485 ];
486 for r in ®ions {
487 for pt in &pts {
488 let sd = r.signed_distance(pt);
489 if sd < -TOL {
491 assert!(r.contains(pt), "sd={sd}<0 but !contains at {pt:?}");
492 }
493 if sd > TOL {
494 assert!(!r.contains(pt), "sd={sd}>0 but contains at {pt:?}");
495 }
496 }
497 }
498 }
499
500 #[test]
503 fn from_region_penalty_zero_inside() {
504 let r = RegionRestraint(InsideSphereRegion::new([0.0; 3], 5.0));
505 assert_eq!(r.f(&[0.0, 0.0, 0.0], 1.0, 1.0), 0.0);
506 }
507
508 #[test]
509 fn from_region_penalty_positive_outside() {
510 let r = RegionRestraint(InsideSphereRegion::new([0.0; 3], 5.0));
511 assert!(r.f(&[10.0, 0.0, 0.0], 1.0, 1.0) > 0.0);
512 }
513
514 #[test]
515 fn from_region_gradient_matches_finite_diff() {
516 let r = RegionRestraint(
517 InsideBoxRegion::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0])
518 .and(Not(InsideSphereRegion::new([5.0, 5.0, 5.0], 2.0))),
519 );
520 let x = [15.0, 5.0, 5.0];
522 let mut g = [0.0; 3];
523 let _ = r.fg(&x, 1.0, 1.0, &mut g);
524
525 let h: F = 1e-5;
527 for k in 0..3 {
528 let mut xp = x;
529 xp[k] += h;
530 let mut xm = x;
531 xm[k] -= h;
532 let fd = (r.f(&xp, 1.0, 1.0) - r.f(&xm, 1.0, 1.0)) / (2.0 * h);
533 assert!(
534 (g[k] - fd).abs() < 1e-3,
535 "gradient mismatch axis {k}: analytic={} fd={} err={}",
536 g[k],
537 fd,
538 (g[k] - fd).abs()
539 );
540 }
541 }
542
543 #[test]
544 fn region_ext_chain() {
545 let shell = InsideSphereRegion::new([0.0; 3], 10.0)
547 .and(InsideSphereRegion::new([0.0; 3], 5.0).not());
548 assert!(shell.contains(&[7.0, 0.0, 0.0]));
549 assert!(!shell.contains(&[3.0, 0.0, 0.0]));
550 assert!(!shell.contains(&[15.0, 0.0, 0.0]));
551 }
552
553 #[test]
554 fn gradient_accumulates_not_overwrite() {
555 let r = RegionRestraint(InsideSphereRegion::new([0.0; 3], 1.0));
556 let mut g = [100.0; 3];
557 let _ = r.fg(&[2.0, 0.0, 0.0], 1.0, 1.0, &mut g);
558 assert!(g[0] > 100.0, "should accumulate");
559 assert!((g[1] - 100.0).abs() < TOL);
560 assert!((g[2] - 100.0).abs() < TOL);
561 }
562}