use molrs::types::F;
use crate::restraint::AtomRestraint;
#[derive(Debug, Clone, Copy)]
pub struct Aabb {
pub min: [F; 3],
pub max: [F; 3],
}
pub trait Region: Send + Sync + std::fmt::Debug {
fn contains(&self, x: &[F; 3]) -> bool;
fn signed_distance(&self, x: &[F; 3]) -> F;
fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
let h: F = 1e-6;
let mut g = [0.0; 3];
for k in 0..3 {
let mut xp = *x;
xp[k] += h;
let mut xm = *x;
xm[k] -= h;
g[k] = (self.signed_distance(&xp) - self.signed_distance(&xm)) / (2.0 * h);
}
g
}
fn bounding_box(&self) -> Option<Aabb> {
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct And<A, B>(pub A, pub B);
impl<A: Region, B: Region> Region for And<A, B> {
fn contains(&self, x: &[F; 3]) -> bool {
self.0.contains(x) && self.1.contains(x)
}
fn signed_distance(&self, x: &[F; 3]) -> F {
self.0.signed_distance(x).max(self.1.signed_distance(x))
}
fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
if self.0.signed_distance(x) >= self.1.signed_distance(x) {
self.0.signed_distance_grad(x)
} else {
self.1.signed_distance_grad(x)
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Or<A, B>(pub A, pub B);
impl<A: Region, B: Region> Region for Or<A, B> {
fn contains(&self, x: &[F; 3]) -> bool {
self.0.contains(x) || self.1.contains(x)
}
fn signed_distance(&self, x: &[F; 3]) -> F {
self.0.signed_distance(x).min(self.1.signed_distance(x))
}
fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
if self.0.signed_distance(x) <= self.1.signed_distance(x) {
self.0.signed_distance_grad(x)
} else {
self.1.signed_distance_grad(x)
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Not<A>(pub A);
impl<A: Region> Region for Not<A> {
fn contains(&self, x: &[F; 3]) -> bool {
!self.0.contains(x)
}
fn signed_distance(&self, x: &[F; 3]) -> F {
-self.0.signed_distance(x)
}
fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
let g = self.0.signed_distance_grad(x);
[-g[0], -g[1], -g[2]]
}
}
pub trait RegionExt: Region + Sized {
fn and<B: Region>(self, other: B) -> And<Self, B> {
And(self, other)
}
fn or<B: Region>(self, other: B) -> Or<Self, B> {
Or(self, other)
}
fn not(self) -> Not<Self> {
Not(self)
}
fn into_restraint(self) -> RegionRestraint<Self> {
RegionRestraint(self)
}
}
impl<R: Region + Sized> RegionExt for R {}
#[derive(Debug, Clone, Copy)]
pub struct RegionRestraint<R: Region>(pub R);
impl<R: Region + 'static> AtomRestraint for RegionRestraint<R> {
fn f(&self, x: &[F; 3], _scale: F, scale2: F) -> F {
let d = self.0.signed_distance(x);
let v = d.max(0.0);
scale2 * v * v
}
fn fg(&self, x: &[F; 3], scale: F, scale2: F, g: &mut [F; 3]) -> F {
let d = self.0.signed_distance(x);
if d > 0.0 {
let grad = self.0.signed_distance_grad(x);
let coeff = 2.0 * scale2 * d;
g[0] += coeff * grad[0];
g[1] += coeff * grad[1];
g[2] += coeff * grad[2];
}
self.f(x, scale, scale2)
}
}
#[derive(Debug, Clone, Copy)]
pub struct InsideBoxRegion {
pub min: [F; 3],
pub max: [F; 3],
}
impl InsideBoxRegion {
pub fn new(min: [F; 3], max: [F; 3]) -> Self {
Self { min, max }
}
}
impl Region for InsideBoxRegion {
fn contains(&self, x: &[F; 3]) -> bool {
(0..3).all(|k| x[k] >= self.min[k] && x[k] <= self.max[k])
}
fn signed_distance(&self, x: &[F; 3]) -> F {
let mut d = F::NEG_INFINITY;
for ((xk, &lo_k), &hi_k) in x.iter().zip(self.min.iter()).zip(self.max.iter()) {
d = d.max(lo_k - xk).max(xk - hi_k);
}
d
}
fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
let mut best_d = F::NEG_INFINITY;
let mut best_axis = 0usize;
let mut best_sign = 0.0 as F;
for (k, ((xk, &lo_k), &hi_k)) in x
.iter()
.zip(self.min.iter())
.zip(self.max.iter())
.enumerate()
{
let lo = lo_k - xk; let hi = xk - hi_k; if lo > best_d {
best_d = lo;
best_axis = k;
best_sign = -1.0;
}
if hi > best_d {
best_d = hi;
best_axis = k;
best_sign = 1.0;
}
}
let mut g = [0.0; 3];
g[best_axis] = best_sign;
g
}
fn bounding_box(&self) -> Option<Aabb> {
Some(Aabb {
min: self.min,
max: self.max,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct InsideSphereRegion {
pub center: [F; 3],
pub radius: F,
}
impl InsideSphereRegion {
pub fn new(center: [F; 3], radius: F) -> Self {
Self { center, radius }
}
}
impl Region for InsideSphereRegion {
fn contains(&self, x: &[F; 3]) -> bool {
let c = self.center;
let d2 = (x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2);
d2 <= self.radius.powi(2)
}
fn signed_distance(&self, x: &[F; 3]) -> F {
let c = self.center;
let d = ((x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2)).sqrt();
d - self.radius
}
fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
let c = self.center;
let (dx, dy, dz) = (x[0] - c[0], x[1] - c[1], x[2] - c[2]);
let d = (dx * dx + dy * dy + dz * dz).sqrt();
if d < 1e-12 {
[0.0; 3]
} else {
[dx / d, dy / d, dz / d]
}
}
fn bounding_box(&self) -> Option<Aabb> {
let r = self.radius;
let c = self.center;
Some(Aabb {
min: [c[0] - r, c[1] - r, c[2] - r],
max: [c[0] + r, c[1] + r, c[2] + r],
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct OutsideSphereRegion {
pub center: [F; 3],
pub radius: F,
}
impl OutsideSphereRegion {
pub fn new(center: [F; 3], radius: F) -> Self {
Self { center, radius }
}
}
impl Region for OutsideSphereRegion {
fn contains(&self, x: &[F; 3]) -> bool {
let c = self.center;
let d2 = (x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2);
d2 >= self.radius.powi(2)
}
fn signed_distance(&self, x: &[F; 3]) -> F {
let c = self.center;
let d = ((x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2) + (x[2] - c[2]).powi(2)).sqrt();
self.radius - d
}
fn signed_distance_grad(&self, x: &[F; 3]) -> [F; 3] {
let c = self.center;
let (dx, dy, dz) = (x[0] - c[0], x[1] - c[1], x[2] - c[2]);
let d = (dx * dx + dy * dy + dz * dz).sqrt();
if d < 1e-12 {
[0.0; 3]
} else {
[-dx / d, -dy / d, -dz / d]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: F = 1e-6;
#[test]
fn and_is_intersection() {
let a = InsideBoxRegion::new([0.0; 3], [10.0; 3]);
let b = InsideSphereRegion::new([5.0; 3], 4.0);
let c = And(a, b);
assert!(c.contains(&[5.0, 5.0, 5.0]));
assert!(!c.contains(&[1.0, 1.0, 1.0]));
assert!(!c.contains(&[-1.0, -1.0, -1.0]));
}
#[test]
fn or_is_union() {
let a = InsideSphereRegion::new([0.0; 3], 5.0);
let b = InsideSphereRegion::new([20.0, 0.0, 0.0], 5.0);
let u = Or(a, b);
assert!(u.contains(&[0.0, 0.0, 0.0]));
assert!(u.contains(&[20.0, 0.0, 0.0]));
assert!(!u.contains(&[10.0, 0.0, 0.0]));
}
#[test]
fn not_is_complement() {
let a = InsideSphereRegion::new([0.0; 3], 5.0);
let n = Not(a);
assert!(!n.contains(&[0.0, 0.0, 0.0]));
assert!(n.contains(&[10.0, 0.0, 0.0]));
}
#[test]
fn de_morgan_and() {
let a = InsideSphereRegion::new([0.0; 3], 5.0);
let b = InsideBoxRegion::new([-3.0; 3], [3.0; 3]);
let lhs = Not(And(a, b));
let rhs = Or(Not(a), Not(b));
for pt in &[
[0.0, 0.0, 0.0],
[4.0, 0.0, 0.0],
[10.0, 0.0, 0.0],
[1.0, 1.0, 1.0],
] {
assert_eq!(
lhs.contains(pt),
rhs.contains(pt),
"de Morgan mismatch at {pt:?}"
);
}
}
#[test]
fn sphere_signed_distance_sign() {
let s = InsideSphereRegion::new([0.0; 3], 5.0);
assert!(s.signed_distance(&[0.0, 0.0, 0.0]) < 0.0);
assert!((s.signed_distance(&[5.0, 0.0, 0.0])).abs() < TOL);
assert!(s.signed_distance(&[10.0, 0.0, 0.0]) > 0.0);
}
#[test]
fn box_signed_distance_sign() {
let b = InsideBoxRegion::new([0.0; 3], [10.0; 3]);
assert!(b.signed_distance(&[5.0, 5.0, 5.0]) < 0.0);
assert!(b.signed_distance(&[15.0, 5.0, 5.0]) > 0.0);
assert!(b.signed_distance(&[-5.0, 5.0, 5.0]) > 0.0);
}
#[test]
fn contains_matches_signed_distance() {
let regions: Vec<Box<dyn Region>> = vec![
Box::new(InsideSphereRegion::new([0.0; 3], 3.0)),
Box::new(InsideBoxRegion::new([0.0; 3], [5.0; 3])),
Box::new(OutsideSphereRegion::new([0.0; 3], 2.0)),
];
let pts = [
[0.0, 0.0, 0.0],
[1.0, 1.0, 1.0],
[3.0, 3.0, 3.0],
[-1.0, 0.0, 0.0],
];
for r in ®ions {
for pt in &pts {
let sd = r.signed_distance(pt);
if sd < -TOL {
assert!(r.contains(pt), "sd={sd}<0 but !contains at {pt:?}");
}
if sd > TOL {
assert!(!r.contains(pt), "sd={sd}>0 but contains at {pt:?}");
}
}
}
}
#[test]
fn from_region_penalty_zero_inside() {
let r = RegionRestraint(InsideSphereRegion::new([0.0; 3], 5.0));
assert_eq!(r.f(&[0.0, 0.0, 0.0], 1.0, 1.0), 0.0);
}
#[test]
fn from_region_penalty_positive_outside() {
let r = RegionRestraint(InsideSphereRegion::new([0.0; 3], 5.0));
assert!(r.f(&[10.0, 0.0, 0.0], 1.0, 1.0) > 0.0);
}
#[test]
fn from_region_gradient_matches_finite_diff() {
let r = RegionRestraint(
InsideBoxRegion::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0])
.and(Not(InsideSphereRegion::new([5.0, 5.0, 5.0], 2.0))),
);
let x = [15.0, 5.0, 5.0];
let mut g = [0.0; 3];
let _ = r.fg(&x, 1.0, 1.0, &mut g);
let h: F = 1e-5;
for k in 0..3 {
let mut xp = x;
xp[k] += h;
let mut xm = x;
xm[k] -= h;
let fd = (r.f(&xp, 1.0, 1.0) - r.f(&xm, 1.0, 1.0)) / (2.0 * h);
assert!(
(g[k] - fd).abs() < 1e-3,
"gradient mismatch axis {k}: analytic={} fd={} err={}",
g[k],
fd,
(g[k] - fd).abs()
);
}
}
#[test]
fn region_ext_chain() {
let shell = InsideSphereRegion::new([0.0; 3], 10.0)
.and(InsideSphereRegion::new([0.0; 3], 5.0).not());
assert!(shell.contains(&[7.0, 0.0, 0.0]));
assert!(!shell.contains(&[3.0, 0.0, 0.0]));
assert!(!shell.contains(&[15.0, 0.0, 0.0]));
}
#[test]
fn gradient_accumulates_not_overwrite() {
let r = RegionRestraint(InsideSphereRegion::new([0.0; 3], 1.0));
let mut g = [100.0; 3];
let _ = r.fg(&[2.0, 0.0, 0.0], 1.0, 1.0, &mut g);
assert!(g[0] > 100.0, "should accumulate");
assert!((g[1] - 100.0).abs() < TOL);
assert!((g[2] - 100.0).abs() < TOL);
}
}