molpack/restraint/collective/
exponential.rs1use molrs::types::F;
12
13use super::Restraint;
14use super::geometry::{plane_match_f, plane_match_fg, point_match_f, point_match_fg, unit};
15
16#[inline]
18fn exponential_quantile(p: F, lambda: F) -> F {
19 -lambda * (1.0 - p).ln()
20}
21
22#[derive(Debug, Clone)]
33pub struct ExponentialPlane {
34 normal: [F; 3],
35 offset: F,
36 strength: F,
37 lambda: F,
38}
39
40impl ExponentialPlane {
41 pub fn new(normal: [F; 3], offset: F, strength: F, lambda: F) -> Self {
49 assert!(lambda > 0.0, "ExponentialPlane lambda must be positive");
50 Self {
51 normal: unit(normal),
52 offset,
53 strength,
54 lambda,
55 }
56 }
57
58 #[inline]
59 fn quantile(&self) -> impl Fn(F) -> F + '_ {
60 move |p| exponential_quantile(p, self.lambda)
61 }
62}
63
64impl Restraint for ExponentialPlane {
65 fn f(&self, coords: &[[F; 3]], _scale: F, _scale2: F) -> F {
66 plane_match_f(
67 coords,
68 &self.normal,
69 self.offset,
70 self.strength,
71 self.quantile(),
72 )
73 }
74
75 fn fg(&self, coords: &[[F; 3]], _scale: F, _scale2: F, grads: &mut [[F; 3]]) -> F {
76 plane_match_fg(
77 coords,
78 &self.normal,
79 self.offset,
80 self.strength,
81 self.quantile(),
82 grads,
83 )
84 }
85
86 fn name(&self) -> &'static str {
87 "ExponentialPlane"
88 }
89}
90
91#[derive(Debug, Clone)]
99pub struct ExponentialPoint {
100 center: [F; 3],
101 strength: F,
102 lambda: F,
103}
104
105impl ExponentialPoint {
106 pub fn new(center: [F; 3], strength: F, lambda: F) -> Self {
113 assert!(lambda > 0.0, "ExponentialPoint lambda must be positive");
114 Self {
115 center,
116 strength,
117 lambda,
118 }
119 }
120
121 #[inline]
122 fn quantile(&self) -> impl Fn(F) -> F + '_ {
123 move |p| exponential_quantile(p, self.lambda)
124 }
125}
126
127impl Restraint for ExponentialPoint {
128 fn f(&self, coords: &[[F; 3]], _scale: F, _scale2: F) -> F {
129 point_match_f(coords, &self.center, self.strength, self.quantile())
130 }
131
132 fn fg(&self, coords: &[[F; 3]], _scale: F, _scale2: F, grads: &mut [[F; 3]]) -> F {
133 point_match_fg(coords, &self.center, self.strength, self.quantile(), grads)
134 }
135
136 fn name(&self) -> &'static str {
137 "ExponentialPoint"
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::super::testutil::{assert_fd_grad, rng_uniform};
144 use super::*;
145
146 #[test]
147 fn quantile_is_nonnegative_and_increasing() {
148 let q = |p| exponential_quantile(p, 3.0);
149 assert!((q(0.0)).abs() < 1e-12); assert!(q(0.25) > 0.0 && q(0.75) > q(0.25));
151 }
152
153 #[test]
154 fn plane_gradient_matches_finite_difference() {
155 let r = ExponentialPlane::new([0.0, 0.0, 1.0], 0.0, 1000.0, 4.0);
156 let mut seed = 0xABCD_1234u64;
157 let coords: Vec<[F; 3]> = (0..20)
159 .map(|i| {
160 [
161 rng_uniform(&mut seed, 0.0, 8.0),
162 rng_uniform(&mut seed, 0.0, 8.0),
163 1.5 * i as F + rng_uniform(&mut seed, 0.0, 0.3),
164 ]
165 })
166 .collect();
167 assert_fd_grad(&r, &coords);
168 }
169
170 #[test]
171 fn point_gradient_matches_finite_difference() {
172 let r = ExponentialPoint::new([0.0, 0.0, 0.0], 1000.0, 4.0);
173 let mut seed = 0x5151_7777u64;
174 let coords: Vec<[F; 3]> = (0..20)
175 .map(|i| {
176 let rad = 2.0 + 1.5 * i as F + rng_uniform(&mut seed, 0.0, 0.3);
177 let t = rng_uniform(&mut seed, 0.3, 1.2);
178 [
179 rad * t.cos(),
180 rad * t.sin(),
181 rng_uniform(&mut seed, -3.0, 3.0),
182 ]
183 })
184 .collect();
185 assert_fd_grad(&r, &coords);
186 }
187
188 #[test]
189 fn penalty_zero_on_target_quantiles() {
190 let r = ExponentialPlane::new([0.0, 0.0, 1.0], 0.0, 1000.0, 4.0);
191 let n = 50;
192 let coords: Vec<[F; 3]> = (0..n)
193 .map(|k| [0.0, 0.0, exponential_quantile((k as F + 0.5) / n as F, 4.0)])
194 .collect();
195 assert!(r.f(&coords, 1.0, 1.0) < 1e-6);
196 }
197}