molpack/restraint/collective/
tabulated.rs1use molrs::types::F;
13
14use super::Restraint;
15use super::geometry::{plane_match_f, plane_match_fg, point_match_f, point_match_fg};
16
17#[derive(Debug, Clone)]
19struct Quantile {
20 xi: Vec<F>,
22 cdf: Vec<F>,
24}
25
26impl Quantile {
27 fn from_grid(xs: &[F], rho: &[F]) -> Self {
33 assert!(xs.len() >= 2, "tabulated grid needs at least 2 points");
34 assert_eq!(xs.len(), rho.len(), "tabulated xs/rho length mismatch");
35 assert!(
36 xs.windows(2).all(|w| w[1] > w[0]),
37 "tabulated grid xs must be strictly ascending"
38 );
39 assert!(rho.iter().all(|&r| r >= 0.0), "tabulated rho must be ≥ 0");
40
41 let n = xs.len();
42 let mut cdf = vec![0.0 as F; n];
43 for i in 1..n {
44 let dx = xs[i] - xs[i - 1];
45 cdf[i] = cdf[i - 1] + 0.5 * (rho[i] + rho[i - 1]) * dx;
46 }
47 let total = cdf[n - 1];
48 assert!(
49 total > 0.0,
50 "tabulated density must have positive total mass"
51 );
52 for c in cdf.iter_mut() {
53 *c /= total;
54 }
55 Self {
56 xi: xs.to_vec(),
57 cdf,
58 }
59 }
60
61 #[inline]
63 fn quantile(&self, p: F) -> F {
64 let n = self.cdf.len();
65 if p <= self.cdf[0] {
66 return self.xi[0];
67 }
68 if p >= self.cdf[n - 1] {
69 return self.xi[n - 1];
70 }
71 let i = self.cdf.partition_point(|&c| c < p);
73 let (c0, c1) = (self.cdf[i - 1], self.cdf[i]);
74 let (x0, x1) = (self.xi[i - 1], self.xi[i]);
75 if c1 > c0 {
76 x0 + (p - c0) / (c1 - c0) * (x1 - x0)
77 } else {
78 x0
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
90pub struct TabulatedPlane {
91 normal: [F; 3],
92 offset: F,
93 strength: F,
94 quant: Quantile,
95}
96
97impl TabulatedPlane {
98 pub fn new(normal: [F; 3], offset: F, strength: F, xs: &[F], rho: &[F]) -> Self {
106 let norm = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
107 assert!(norm > 0.0, "TabulatedPlane normal must be non-zero");
108 Self {
109 normal: [normal[0] / norm, normal[1] / norm, normal[2] / norm],
110 offset,
111 strength,
112 quant: Quantile::from_grid(xs, rho),
113 }
114 }
115}
116
117impl Restraint for TabulatedPlane {
118 fn f(&self, coords: &[[F; 3]], _scale: F, _scale2: F) -> F {
119 plane_match_f(coords, &self.normal, self.offset, self.strength, |p| {
120 self.quant.quantile(p)
121 })
122 }
123
124 fn fg(&self, coords: &[[F; 3]], _scale: F, _scale2: F, grads: &mut [[F; 3]]) -> F {
125 plane_match_fg(
126 coords,
127 &self.normal,
128 self.offset,
129 self.strength,
130 |p| self.quant.quantile(p),
131 grads,
132 )
133 }
134
135 fn name(&self) -> &'static str {
136 "TabulatedPlane"
137 }
138}
139
140#[derive(Debug, Clone)]
147pub struct TabulatedPoint {
148 center: [F; 3],
149 strength: F,
150 quant: Quantile,
151}
152
153impl TabulatedPoint {
154 pub fn new(center: [F; 3], strength: F, xs: &[F], rho: &[F]) -> Self {
161 Self {
162 center,
163 strength,
164 quant: Quantile::from_grid(xs, rho),
165 }
166 }
167}
168
169impl Restraint for TabulatedPoint {
170 fn f(&self, coords: &[[F; 3]], _scale: F, _scale2: F) -> F {
171 point_match_f(coords, &self.center, self.strength, |p| {
172 self.quant.quantile(p)
173 })
174 }
175
176 fn fg(&self, coords: &[[F; 3]], _scale: F, _scale2: F, grads: &mut [[F; 3]]) -> F {
177 point_match_fg(
178 coords,
179 &self.center,
180 self.strength,
181 |p| self.quant.quantile(p),
182 grads,
183 )
184 }
185
186 fn name(&self) -> &'static str {
187 "TabulatedPoint"
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::super::testutil::{assert_fd_grad, rng_uniform};
194 use super::*;
195
196 fn gaussian_grid(mu: F, sigma: F) -> (Vec<F>, Vec<F>) {
199 let lo = mu - 6.0 * sigma;
200 let hi = mu + 6.0 * sigma;
201 let n = 1200;
202 let xs: Vec<F> = (0..n)
203 .map(|i| lo + (hi - lo) * i as F / (n - 1) as F)
204 .collect();
205 let rho: Vec<F> = xs
206 .iter()
207 .map(|&x| (-0.5 * ((x - mu) / sigma).powi(2)).exp())
208 .collect();
209 (xs, rho)
210 }
211
212 #[test]
213 fn quantile_recovers_gaussian() {
214 use super::super::engine::probit;
215 let (xs, rho) = gaussian_grid(20.0, 5.0);
216 let q = Quantile::from_grid(&xs, &rho);
217 for &p in &[0.1, 0.25, 0.5, 0.75, 0.9] {
218 let want = 20.0 + 5.0 * probit(p);
219 assert!(
220 (q.quantile(p) - want).abs() < 0.05,
221 "tabulated q({p})={}, want {want}",
222 q.quantile(p)
223 );
224 }
225 }
226
227 #[test]
228 fn plane_gradient_matches_finite_difference() {
229 let (xs, rho) = gaussian_grid(20.0, 5.0);
230 let r = TabulatedPlane::new([0.0, 0.0, 1.0], 0.0, 1000.0, &xs, &rho);
231 let mut seed = 0x1357_2468u64;
232 let coords: Vec<[F; 3]> = (0..20)
233 .map(|i| {
234 [
235 rng_uniform(&mut seed, 0.0, 10.0),
236 rng_uniform(&mut seed, 0.0, 10.0),
237 2.0 * i as F + rng_uniform(&mut seed, 0.0, 0.4),
238 ]
239 })
240 .collect();
241 assert_fd_grad(&r, &coords);
242 }
243
244 #[test]
245 fn point_gradient_matches_finite_difference() {
246 let n = 400;
248 let xs: Vec<F> = (0..n).map(|i| 0.05 * i as F).collect();
249 let rho: Vec<F> = xs.iter().map(|&x| (-x / 4.0).exp()).collect();
250 let r = TabulatedPoint::new([0.0, 0.0, 0.0], 1000.0, &xs, &rho);
251 let mut seed = 0x2468_1357u64;
252 let coords: Vec<[F; 3]> = (0..20)
253 .map(|i| {
254 let rad = 2.0 + 1.5 * i as F + rng_uniform(&mut seed, 0.0, 0.3);
255 let t = rng_uniform(&mut seed, 0.3, 1.2);
256 [
257 rad * t.cos(),
258 rad * t.sin(),
259 rng_uniform(&mut seed, -3.0, 3.0),
260 ]
261 })
262 .collect();
263 assert_fd_grad(&r, &coords);
264 }
265
266 #[test]
267 fn penalty_zero_on_target_quantiles() {
268 let (xs, rho) = gaussian_grid(20.0, 5.0);
269 let r = TabulatedPlane::new([0.0, 0.0, 1.0], 0.0, 1000.0, &xs, &rho);
270 let n = 50;
271 let q = Quantile::from_grid(&xs, &rho);
272 let coords: Vec<[F; 3]> = (0..n)
273 .map(|k| [0.0, 0.0, q.quantile((k as F + 0.5) / n as F)])
274 .collect();
275 assert!(r.f(&coords, 1.0, 1.0) < 1e-6);
276 }
277}