1use std::fmt;
12
13use ndarray::Array1;
14use rand::SeedableRng;
15use rand::rngs::StdRng;
16
17use super::occupancy::{OccupancyGrid, OccupancyMode};
18use crate::spatial::simbox::BoxError;
19use crate::spatial::simbox::SimBox;
20use crate::types::{F, F3, Pbc3};
21
22pub(crate) const FIRST_POINT_TRIES: usize = 64;
25
26const STEP_TRIES: usize = 40;
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum WalkError {
32 InvalidConfig(String),
35 BoxError(String),
37 DeadEnd {
40 chain: usize,
42 monomer: usize,
44 },
45}
46
47impl fmt::Display for WalkError {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 WalkError::InvalidConfig(m) => write!(f, "invalid SARW configuration: {m}"),
51 WalkError::BoxError(m) => write!(f, "box construction failed: {m}"),
52 WalkError::DeadEnd { chain, monomer } => write!(
53 f,
54 "self-avoiding walk dead-ended on chain {chain} after reaching {monomer} monomers"
55 ),
56 }
57 }
58}
59
60impl std::error::Error for WalkError {}
61
62impl From<BoxError> for WalkError {
63 fn from(e: BoxError) -> Self {
64 WalkError::BoxError(format!("{e:?}"))
65 }
66}
67
68pub(crate) fn to_f3(p: [F; 3]) -> F3 {
70 Array1::from_vec(vec![p[0], p[1], p[2]])
71}
72
73pub(crate) fn apply_boundary(tip: [F; 3], mut cand: [F; 3], a: [F; 3], pbc: Pbc3) -> [F; 3] {
79 for ax in 0..3 {
80 if pbc[ax] {
81 cand[ax] = cand[ax].rem_euclid(a[ax]);
82 } else if cand[ax] < 0.0 || cand[ax] >= a[ax] {
83 cand[ax] = 2.0 * tip[ax] - cand[ax];
86 cand[ax] = cand[ax].clamp(0.0, a[ax] * (1.0 - 1e-12));
89 }
90 }
91 cand
92}
93
94pub trait GrowthStrategy {
103 fn occupancy_mode(&self, bond_length: F) -> OccupancyMode;
105
106 fn adjust_box_edge(&self, edge: F, bond_length: F) -> F {
109 let _ = bond_length;
110 edge
111 }
112
113 fn propose_first(&self, simbox: &SimBox, bond_length: F, rng: &mut StdRng) -> [F; 3];
115
116 fn propose_step(&self, tip: [F; 3], bond_length: F, rng: &mut StdRng) -> [F; 3];
119}
120
121pub struct SelfAvoidingWalk<S: GrowthStrategy> {
148 pub n_chains: usize,
150 pub chain_length: usize,
152 pub bond_length: F,
154 pub target_density: F,
156 pub pbc: Pbc3,
158 pub seed: u64,
160 pub strategy: S,
162}
163
164pub struct WalkOutput {
167 pub paths: Vec<Vec<F3>>,
170 pub simbox: SimBox,
172}
173
174impl<S: GrowthStrategy> SelfAvoidingWalk<S> {
175 pub fn generate(&self) -> Result<WalkOutput, WalkError> {
182 if self.bond_length <= 0.0 {
183 return Err(WalkError::InvalidConfig("bond_length must be > 0".into()));
184 }
185 if self.target_density <= 0.0 {
186 return Err(WalkError::InvalidConfig(
187 "target_density must be > 0".into(),
188 ));
189 }
190 if self.chain_length == 0 {
191 return Err(WalkError::InvalidConfig("chain_length must be > 0".into()));
192 }
193 if self.n_chains == 0 {
194 return Err(WalkError::InvalidConfig("n_chains must be > 0".into()));
195 }
196
197 let n_total = self.n_chains * self.chain_length;
198 let raw_edge = (n_total as F / self.target_density).cbrt();
199 let edge = self.strategy.adjust_box_edge(raw_edge, self.bond_length);
200 if edge <= 2.0 * self.bond_length {
203 return Err(WalkError::InvalidConfig(
204 "box edge too small for bond length; lower the density".into(),
205 ));
206 }
207 let simbox = SimBox::cube(edge, Array1::zeros(3), self.pbc)?;
208 let a = [edge, edge, edge];
209
210 let mode = self.strategy.occupancy_mode(self.bond_length);
211 let mut grid = OccupancyGrid::new(mode, &simbox, self.pbc);
212 let mut rng = StdRng::seed_from_u64(self.seed);
213 let mut paths: Vec<Vec<F3>> = Vec::with_capacity(self.n_chains);
214
215 let max_backtrack = 50 * self.chain_length + 1000;
216 const MAX_CHAIN_RESTARTS: usize = 8;
217
218 for c in 0..self.n_chains {
219 let mut best_reached = 0usize;
220 let mut grown: Option<Vec<[F; 3]>> = None;
221 for _ in 0..MAX_CHAIN_RESTARTS {
222 if let Some(chain) = self.grow_chain(
223 &simbox,
224 a,
225 &mut grid,
226 &mut rng,
227 max_backtrack,
228 &mut best_reached,
229 ) {
230 grown = Some(chain);
231 break;
232 }
233 }
234 let chain = grown.ok_or(WalkError::DeadEnd {
235 chain: c,
236 monomer: best_reached,
237 })?;
238 paths.push(chain.iter().map(|p| to_f3(*p)).collect());
239 }
240
241 Ok(WalkOutput { paths, simbox })
242 }
243
244 fn grow_chain(
248 &self,
249 simbox: &SimBox,
250 a: [F; 3],
251 grid: &mut OccupancyGrid,
252 rng: &mut StdRng,
253 max_backtrack: usize,
254 best_reached: &mut usize,
255 ) -> Option<Vec<[F; 3]>> {
256 let mut chain: Vec<[F; 3]> = Vec::with_capacity(self.chain_length);
257 let mut backtracks = 0usize;
258
259 while chain.len() < self.chain_length {
260 let placed = if let Some(&tip) = chain.last() {
261 let mut hit = None;
262 for _ in 0..STEP_TRIES {
263 let raw = self.strategy.propose_step(tip, self.bond_length, rng);
264 let cand = apply_boundary(tip, raw, a, self.pbc);
265 if grid.is_free(cand, Some(tip)) {
266 hit = Some(cand);
267 break;
268 }
269 }
270 hit
271 } else {
272 let mut hit = None;
273 for _ in 0..FIRST_POINT_TRIES {
274 let p = self.strategy.propose_first(simbox, self.bond_length, rng);
275 if grid.is_free(p, None) {
276 hit = Some(p);
277 break;
278 }
279 }
280 hit
281 };
282
283 match placed {
284 Some(p) => {
285 grid.insert(p);
286 chain.push(p);
287 if chain.len() > *best_reached {
288 *best_reached = chain.len();
289 }
290 }
291 None => {
292 if let Some(popped) = chain.pop() {
293 grid.remove(popped);
294 }
295 backtracks += 1;
296 if backtracks > max_backtrack {
297 for p in &chain {
299 grid.remove(*p);
300 }
301 return None;
302 }
303 }
304 }
305 }
306 Some(chain)
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::builder::{FccLattice, OffLattice};
314
315 const B: F = 1.53;
316
317 fn off() -> SelfAvoidingWalk<OffLattice> {
318 SelfAvoidingWalk {
319 n_chains: 3,
320 chain_length: 20,
321 bond_length: B,
322 target_density: 0.05,
323 pbc: [true, true, true],
324 seed: 9062,
325 strategy: OffLattice {
326 excluded_radius: 1.0,
327 },
328 }
329 }
330
331 fn fcc() -> SelfAvoidingWalk<FccLattice> {
332 SelfAvoidingWalk {
333 n_chains: 3,
334 chain_length: 20,
335 bond_length: B,
336 target_density: 0.05,
337 pbc: [true, true, true],
338 seed: 9062,
339 strategy: FccLattice,
340 }
341 }
342
343 fn fcc_reflective() -> SelfAvoidingWalk<FccLattice> {
344 SelfAvoidingWalk {
345 pbc: [false, false, false],
346 ..fcc()
347 }
348 }
349
350 fn off_reflective() -> SelfAvoidingWalk<OffLattice> {
351 SelfAvoidingWalk {
352 pbc: [false, false, false],
353 ..off()
354 }
355 }
356
357 fn out_off() -> WalkOutput {
358 off().generate().unwrap()
359 }
360 fn out_fcc() -> WalkOutput {
361 fcc().generate().unwrap()
362 }
363
364 fn pt(v: &F3) -> [F; 3] {
365 [v[0], v[1], v[2]]
366 }
367
368 fn min_image_dist(sb: &SimBox, x: &F3, y: &F3) -> F {
369 let d = sb.shortest_vector_impl(pt(x), pt(y));
370 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
371 }
372
373 #[test]
375 fn shape_is_exact() {
376 for paths in [out_off().paths, out_fcc().paths] {
377 assert_eq!(paths.len(), 3);
378 for chain in &paths {
379 assert_eq!(chain.len(), 20usize);
380 }
381 }
382 }
383
384 #[test]
386 fn deterministic_under_seed() {
387 for (a, b) in [
388 (off().generate().unwrap(), off().generate().unwrap()),
389 (fcc().generate().unwrap(), fcc().generate().unwrap()),
390 ] {
391 for (ca, cb) in a.paths.iter().zip(b.paths.iter()) {
392 for (pa, pb) in ca.iter().zip(cb.iter()) {
393 assert_eq!(pt(pa), pt(pb), "coordinates must match exactly");
394 }
395 }
396 }
397 }
398
399 #[test]
403 fn bond_length_invariant() {
404 let cases = [
405 (out_off(), 1e-9),
406 (out_fcc(), 1e-9),
407 (off_reflective().generate().unwrap(), 1e-9),
408 (fcc_reflective().generate().unwrap(), 1e-9),
409 ];
410 for (out, tol) in cases {
411 for chain in &out.paths {
412 for w in chain.windows(2) {
413 let d = min_image_dist(&out.simbox, &w[0], &w[1]);
414 assert!((d - B).abs() <= tol, "bond {d} != {B} (tol {tol})");
415 }
416 }
417 }
418 }
419
420 #[test]
423 fn offlattice_excluded_volume() {
424 let r = 1.0;
425 let out = out_off();
426 let all: Vec<&F3> = out.paths.iter().flatten().collect();
427 for i in 0..all.len() {
428 for j in (i + 1)..all.len() {
429 let d = min_image_dist(&out.simbox, all[i], all[j]);
430 assert!(d >= r - 1e-9, "pair distance {d} < excluded_radius {r}");
431 }
432 }
433 }
434
435 #[test]
438 fn fcc_no_collision() {
439 for out in [out_fcc(), fcc_reflective().generate().unwrap()] {
440 let all: Vec<&F3> = out.paths.iter().flatten().collect();
441 for i in 0..all.len() {
442 for j in (i + 1)..all.len() {
443 let d = min_image_dist(&out.simbox, all[i], all[j]);
444 assert!(d >= B - 1e-9, "pair distance {d} < nn spacing {B}");
445 }
446 }
447 }
448 }
449
450 #[test]
453 fn density_box_convention() {
454 let w = off();
455 let n_total = (w.n_chains * w.chain_length) as F;
456 let expected = n_total / w.target_density;
457 let v = w.generate().unwrap().simbox.volume();
458 assert!((v - expected).abs() / expected <= 1e-6, "off volume {v}");
459
460 let fv = fcc().generate().unwrap().simbox.volume();
461 assert!(
462 fv >= expected - 1e-6,
463 "fcc volume {fv} < requested {expected}"
464 );
465 }
466
467 #[test]
470 fn output_inside_box() {
471 for out in [
472 out_off(),
473 out_fcc(),
474 off_reflective().generate().unwrap(),
475 fcc_reflective().generate().unwrap(),
476 ] {
477 let edge = out.simbox.lengths()[0];
478 for p in out.paths.iter().flatten() {
479 for k in 0..3 {
480 assert!(
481 p[k] >= 0.0 && p[k] < edge,
482 "coord {} out of [0,{edge})",
483 p[k]
484 );
485 }
486 }
487 }
488 }
489
490 #[test]
492 fn invalid_config_errors() {
493 let bad = |w: SelfAvoidingWalk<OffLattice>| {
494 matches!(w.generate(), Err(WalkError::InvalidConfig(_)))
495 };
496 assert!(bad(SelfAvoidingWalk {
497 bond_length: 0.0,
498 ..off()
499 }));
500 assert!(bad(SelfAvoidingWalk {
501 target_density: 0.0,
502 ..off()
503 }));
504 assert!(bad(SelfAvoidingWalk {
505 chain_length: 0,
506 ..off()
507 }));
508 assert!(bad(SelfAvoidingWalk {
509 n_chains: 0,
510 ..off()
511 }));
512 }
513
514 #[test]
516 fn exhausted_growth_is_dead_end() {
517 let w = SelfAvoidingWalk {
518 n_chains: 4,
519 chain_length: 50,
520 bond_length: B,
521 target_density: 6.10,
522 pbc: [true, true, true],
523 seed: 1,
524 strategy: FccLattice,
525 };
526 assert!(matches!(w.generate(), Err(WalkError::DeadEnd { .. })));
527 }
528
529 #[test]
532 fn struct_injection_and_output_contract() {
533 let out: WalkOutput = SelfAvoidingWalk {
534 n_chains: 1,
535 chain_length: 5,
536 bond_length: B,
537 target_density: 0.05,
538 pbc: [true, true, true],
539 seed: 7,
540 strategy: FccLattice,
541 }
542 .generate()
543 .unwrap();
544 let _paths: &Vec<Vec<F3>> = &out.paths;
545 let _box: &SimBox = &out.simbox;
546 }
547}