1use rand::Rng;
6use rand::RngExt;
7
8#[derive(Debug, Clone)]
10pub struct RsaConfig {
11 pub radius: f64,
13 pub domain: [f64; 3],
15 pub max_attempts: usize,
17 pub min_gap: f64,
19}
20pub struct PolyDispersePacking {
22 pub distribution: SizeDistribution,
24 pub params: SizeDistributionParams,
26 pub domain: [f64; 3],
28 pub spheres: Vec<PackingSphere>,
30 pub max_attempts: usize,
32}
33impl PolyDispersePacking {
34 pub fn new(
36 distribution: SizeDistribution,
37 params: SizeDistributionParams,
38 domain: [f64; 3],
39 max_attempts: usize,
40 ) -> Self {
41 Self {
42 distribution,
43 params,
44 domain,
45 spheres: Vec::new(),
46 max_attempts,
47 }
48 }
49 pub fn sample_radius(&self, rng: &mut impl Rng) -> f64 {
51 match self.distribution {
52 SizeDistribution::Monodisperse => self.params.mean_radius,
53 SizeDistribution::Bidisperse => {
54 if rng.random::<f64>() < self.params.large_fraction {
55 self.params.mean_radius * self.params.size_ratio
56 } else {
57 self.params.mean_radius
58 }
59 }
60 SizeDistribution::LogNormal => {
61 let mu = self.params.mean_radius.ln();
62 let sigma = self.params.std_radius;
63 let z = self.normal_sample(rng);
64 let r = (mu + sigma * z).exp();
65 r.clamp(self.params.min_radius, self.params.max_radius)
66 }
67 SizeDistribution::Gaussian => {
68 let r = self.params.mean_radius + self.params.std_radius * self.normal_sample(rng);
69 r.clamp(self.params.min_radius, self.params.max_radius)
70 }
71 SizeDistribution::PowerLaw => {
72 let alpha = self.params.power_law_exponent;
73 let r_min = self.params.min_radius;
74 let r_max = self.params.max_radius;
75 let u: f64 = rng.random();
76 if (alpha + 1.0).abs() < 1e-12 {
77 r_min * (r_max / r_min).powf(u)
78 } else {
79 let p = alpha + 1.0;
80 (r_min.powf(p) + u * (r_max.powf(p) - r_min.powf(p))).powf(1.0 / p)
81 }
82 }
83 }
84 }
85 fn normal_sample(&self, rng: &mut impl Rng) -> f64 {
87 let u1: f64 = rng.random::<f64>().max(1e-300);
88 let u2: f64 = rng.random();
89 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
90 }
91 pub fn is_valid_placement(&self, center: [f64; 3], radius: f64) -> bool {
93 let [lx, ly, lz] = self.domain;
94 if center[0] - radius < 0.0
95 || center[0] + radius > lx
96 || center[1] - radius < 0.0
97 || center[1] + radius > ly
98 || center[2] - radius < 0.0
99 || center[2] + radius > lz
100 {
101 return false;
102 }
103 let candidate = PackingSphere::new(center, radius, 0);
104 for s in &self.spheres {
105 if s.overlaps(&candidate) {
106 return false;
107 }
108 }
109 true
110 }
111 pub fn run(&mut self, n_spheres: usize) -> usize {
115 let mut rng = rand::rng();
116 let mut attempts = 0;
117 while self.spheres.len() < n_spheres && attempts < self.max_attempts {
118 let r = self.sample_radius(&mut rng);
119 let [lx, ly, lz] = self.domain;
120 let cx = rng.random_range(r..(lx - r).max(r + 1e-12));
121 let cy = rng.random_range(r..(ly - r).max(r + 1e-12));
122 let cz = rng.random_range(r..(lz - r).max(r + 1e-12));
123 if self.is_valid_placement([cx, cy, cz], r) {
124 let id = self.spheres.len();
125 self.spheres.push(PackingSphere::new([cx, cy, cz], r, id));
126 }
127 attempts += 1;
128 }
129 self.spheres.len()
130 }
131 pub fn packing_fraction(&self) -> f64 {
133 let [lx, ly, lz] = self.domain;
134 let vol = lx * ly * lz;
135 let sv: f64 = self.spheres.iter().map(|s| s.volume()).sum();
136 sv / vol
137 }
138 pub fn radius_statistics(&self) -> (f64, f64, f64, f64) {
140 if self.spheres.is_empty() {
141 return (0.0, 0.0, 0.0, 0.0);
142 }
143 let radii: Vec<f64> = self.spheres.iter().map(|s| s.radius).collect();
144 let n = radii.len() as f64;
145 let mean = radii.iter().sum::<f64>() / n;
146 let variance = radii.iter().map(|&r| (r - mean).powi(2)).sum::<f64>() / n;
147 let std = variance.sqrt();
148 let min = radii.iter().cloned().fold(f64::INFINITY, f64::min);
149 let max = radii.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
150 (mean, std, min, max)
151 }
152 pub fn polydispersity_index(&self) -> f64 {
154 let (mean, std, _, _) = self.radius_statistics();
155 if mean > 1e-30 { std / mean } else { 0.0 }
156 }
157 pub fn size_histogram(&self, n_bins: usize) -> (Vec<f64>, Vec<usize>) {
161 if self.spheres.is_empty() || n_bins == 0 {
162 return (Vec::new(), Vec::new());
163 }
164 let (_, _, min_r, max_r) = self.radius_statistics();
165 let bin_width = (max_r - min_r) / n_bins as f64;
166 if bin_width < 1e-30 {
167 let centers = vec![min_r; n_bins];
168 let mut counts = vec![0usize; n_bins];
169 counts[0] = self.spheres.len();
170 return (centers, counts);
171 }
172 let mut counts = vec![0usize; n_bins];
173 for s in &self.spheres {
174 let bin = ((s.radius - min_r) / bin_width).floor() as usize;
175 let bin = bin.min(n_bins - 1);
176 counts[bin] += 1;
177 }
178 let centers: Vec<f64> = (0..n_bins)
179 .map(|i| min_r + (i as f64 + 0.5) * bin_width)
180 .collect();
181 (centers, counts)
182 }
183}
184pub struct VoidSpaceAnalysis {
189 pub spheres: Vec<PackingSphere>,
191 pub domain: [f64; 3],
193 pub grid_resolution: usize,
195}
196impl VoidSpaceAnalysis {
197 pub fn new(spheres: Vec<PackingSphere>, domain: [f64; 3], grid_resolution: usize) -> Self {
199 Self {
200 spheres,
201 domain,
202 grid_resolution,
203 }
204 }
205 pub fn point_in_sphere(&self, pt: [f64; 3]) -> bool {
207 for s in &self.spheres {
208 let dx = pt[0] - s.center[0];
209 let dy = pt[1] - s.center[1];
210 let dz = pt[2] - s.center[2];
211 if dx * dx + dy * dy + dz * dz <= s.radius * s.radius {
212 return true;
213 }
214 }
215 false
216 }
217 pub fn void_fraction_by_voxelization(&self) -> f64 {
219 let n = self.grid_resolution;
220 let [lx, ly, lz] = self.domain;
221 let dx = lx / n as f64;
222 let dy = ly / n as f64;
223 let dz = lz / n as f64;
224 let mut void_count = 0usize;
225 let total = n * n * n;
226 for ix in 0..n {
227 for iy in 0..n {
228 for iz in 0..n {
229 let pt = [
230 (ix as f64 + 0.5) * dx,
231 (iy as f64 + 0.5) * dy,
232 (iz as f64 + 0.5) * dz,
233 ];
234 if !self.point_in_sphere(pt) {
235 void_count += 1;
236 }
237 }
238 }
239 }
240 void_count as f64 / total as f64
241 }
242 pub fn pore_radius_at(&self, pt: [f64; 3]) -> f64 {
246 if self.point_in_sphere(pt) {
247 return 0.0;
248 }
249 let mut min_dist_to_surface = f64::INFINITY;
250 for s in &self.spheres {
251 let dx = pt[0] - s.center[0];
252 let dy = pt[1] - s.center[1];
253 let dz = pt[2] - s.center[2];
254 let dist_to_center = (dx * dx + dy * dy + dz * dz).sqrt();
255 let dist_to_surface = (dist_to_center - s.radius).max(0.0);
256 min_dist_to_surface = min_dist_to_surface.min(dist_to_surface);
257 }
258 let [lx, ly, lz] = self.domain;
259 let wall_dist = [pt[0], lx - pt[0], pt[1], ly - pt[1], pt[2], lz - pt[2]]
260 .iter()
261 .cloned()
262 .fold(f64::INFINITY, f64::min);
263 min_dist_to_surface.min(wall_dist)
264 }
265 pub fn pore_size_distribution(&self, n_samples: usize, n_bins: usize) -> Vec<(f64, usize)> {
270 let mut rng = rand::rng();
271 let [lx, ly, lz] = self.domain;
272 let mut pore_radii = Vec::new();
273 for _ in 0..n_samples {
274 let pt = [
275 rng.random_range(0.0..lx),
276 rng.random_range(0.0..ly),
277 rng.random_range(0.0..lz),
278 ];
279 if !self.point_in_sphere(pt) {
280 pore_radii.push(self.pore_radius_at(pt));
281 }
282 }
283 if pore_radii.is_empty() || n_bins == 0 {
284 return Vec::new();
285 }
286 let r_min = pore_radii.iter().cloned().fold(f64::INFINITY, f64::min);
287 let r_max = pore_radii.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
288 let bin_width = (r_max - r_min) / n_bins as f64;
289 if bin_width < 1e-30 {
290 return vec![(r_min, pore_radii.len())];
291 }
292 let mut counts = vec![0usize; n_bins];
293 for &r in &pore_radii {
294 let bin = ((r - r_min) / bin_width).floor() as usize;
295 counts[bin.min(n_bins - 1)] += 1;
296 }
297 (0..n_bins)
298 .map(|i| (r_min + (i as f64 + 0.5) * bin_width, counts[i]))
299 .collect()
300 }
301 pub fn estimate_percolation_threshold(&self) -> f64 {
306 0.3116
307 }
308 pub fn is_percolating(&self, void_fraction: f64) -> bool {
310 void_fraction > self.estimate_percolation_threshold()
311 }
312 pub fn specific_surface_area(&self) -> f64 {
314 let [lx, ly, lz] = self.domain;
315 let vol = lx * ly * lz;
316 let sa: f64 = self
317 .spheres
318 .iter()
319 .map(|s| 4.0 * std::f64::consts::PI * s.radius * s.radius)
320 .sum();
321 sa / vol
322 }
323 pub fn analyze(&self, n_psd_samples: usize, n_bins: usize) -> VoidSpaceResult {
325 let void_frac = self.void_fraction_by_voxelization();
326 let [lx, ly, lz] = self.domain;
327 let void_vol = void_frac * lx * ly * lz;
328 let psd = self.pore_size_distribution(n_psd_samples, n_bins);
329 VoidSpaceResult {
330 pore_size_distribution: psd,
331 percolation_threshold: self.estimate_percolation_threshold(),
332 void_volume: void_vol,
333 void_fraction: void_frac,
334 connectivity: if void_frac > 0.32 { 1.0 } else { 0.0 },
335 }
336 }
337}
338pub struct ConfiningGeometry {
343 pub config: ConfinementConfig,
345 pub spheres: Vec<PackingSphere>,
347 pub n_rejected: usize,
349}
350impl ConfiningGeometry {
351 pub fn new(config: ConfinementConfig) -> Self {
353 Self {
354 config,
355 spheres: Vec::new(),
356 n_rejected: 0,
357 }
358 }
359 pub fn point_inside_container(&self, center: [f64; 3], radius: f64) -> bool {
361 match self.config.shape {
362 ConfinementShape::Box => {
363 let [hx, hy, hz] = self.config.dimensions;
364 center[0] - radius >= -hx
365 && center[0] + radius <= hx
366 && center[1] - radius >= -hy
367 && center[1] + radius <= hy
368 && center[2] - radius >= -hz
369 && center[2] + radius <= hz
370 }
371 ConfinementShape::Cylinder => {
372 let r_cyl = self.config.dimensions[0];
373 let h_cyl = self.config.dimensions[1];
374 let r2 = center[0] * center[0] + center[1] * center[1];
375 r2.sqrt() + radius <= r_cyl
376 && center[2] - radius >= -h_cyl
377 && center[2] + radius <= h_cyl
378 }
379 ConfinementShape::SphericalContainer => {
380 let r_cont = self.config.dimensions[0];
381 let dist =
382 (center[0] * center[0] + center[1] * center[1] + center[2] * center[2]).sqrt();
383 dist + radius <= r_cont
384 }
385 }
386 }
387 pub fn random_point_in_container(&self, rng: &mut impl Rng) -> [f64; 3] {
389 match self.config.shape {
390 ConfinementShape::Box => {
391 let [hx, hy, hz] = self.config.dimensions;
392 [
393 rng.random_range(-hx..hx),
394 rng.random_range(-hy..hy),
395 rng.random_range(-hz..hz),
396 ]
397 }
398 ConfinementShape::Cylinder => {
399 let r_cyl = self.config.dimensions[0];
400 let h_cyl = self.config.dimensions[1];
401 loop {
402 let x = rng.random_range(-r_cyl..r_cyl);
403 let y = rng.random_range(-r_cyl..r_cyl);
404 let z = rng.random_range(-h_cyl..h_cyl);
405 if (x * x + y * y).sqrt() <= r_cyl {
406 return [x, y, z];
407 }
408 }
409 }
410 ConfinementShape::SphericalContainer => {
411 let r_cont = self.config.dimensions[0];
412 loop {
413 let x = rng.random_range(-r_cont..r_cont);
414 let y = rng.random_range(-r_cont..r_cont);
415 let z = rng.random_range(-r_cont..r_cont);
416 if (x * x + y * y + z * z).sqrt() <= r_cont {
417 return [x, y, z];
418 }
419 }
420 }
421 }
422 }
423 fn has_overlap(&self, center: [f64; 3], radius: f64) -> bool {
425 let candidate = PackingSphere::new(center, radius, 0);
426 for s in &self.spheres {
427 if s.overlaps(&candidate) {
428 return true;
429 }
430 }
431 false
432 }
433 pub fn try_place(&mut self, rng: &mut impl Rng) -> bool {
435 let r = self.config.sphere_radius;
436 let center = self.random_point_in_container(rng);
437 if self.point_inside_container(center, r) && !self.has_overlap(center, r) {
438 let id = self.spheres.len();
439 self.spheres.push(PackingSphere::new(center, r, id));
440 true
441 } else {
442 self.n_rejected += 1;
443 false
444 }
445 }
446 pub fn run(&mut self) -> usize {
448 let mut rng = rand::rng();
449 let mut consecutive_failures = 0;
450 loop {
451 if self.try_place(&mut rng) {
452 consecutive_failures = 0;
453 } else {
454 consecutive_failures += 1;
455 if consecutive_failures >= self.config.max_attempts {
456 break;
457 }
458 }
459 }
460 self.spheres.len()
461 }
462 pub fn container_volume(&self) -> f64 {
464 match self.config.shape {
465 ConfinementShape::Box => {
466 let [hx, hy, hz] = self.config.dimensions;
467 8.0 * hx * hy * hz
468 }
469 ConfinementShape::Cylinder => {
470 let r = self.config.dimensions[0];
471 let h = self.config.dimensions[1];
472 std::f64::consts::PI * r * r * 2.0 * h
473 }
474 ConfinementShape::SphericalContainer => {
475 let r = self.config.dimensions[0];
476 4.0 / 3.0 * std::f64::consts::PI * r * r * r
477 }
478 }
479 }
480 pub fn packing_fraction(&self) -> f64 {
482 let vol = self.container_volume();
483 let sv: f64 = self.spheres.iter().map(|s| s.volume()).sum();
484 if vol > 1e-30 { sv / vol } else { 0.0 }
485 }
486 pub fn boundary_layer_fraction(&self) -> f64 {
490 if self.spheres.is_empty() {
491 return 0.0;
492 }
493 let r = self.config.sphere_radius;
494 let layer_thickness = 2.0 * r;
495 let n_boundary = self
496 .spheres
497 .iter()
498 .filter(|s| match self.config.shape {
499 ConfinementShape::Box => {
500 let [hx, hy, hz] = self.config.dimensions;
501 let dist_x = hx - s.center[0].abs();
502 let dist_y = hy - s.center[1].abs();
503 let dist_z = hz - s.center[2].abs();
504 [dist_x, dist_y, dist_z]
505 .iter()
506 .any(|&d| d <= layer_thickness)
507 }
508 ConfinementShape::Cylinder => {
509 let r_cyl = self.config.dimensions[0];
510 let h_cyl = self.config.dimensions[1];
511 let dist_r =
512 r_cyl - (s.center[0] * s.center[0] + s.center[1] * s.center[1]).sqrt();
513 let dist_z = h_cyl - s.center[2].abs();
514 dist_r <= layer_thickness || dist_z <= layer_thickness
515 }
516 ConfinementShape::SphericalContainer => {
517 let r_cont = self.config.dimensions[0];
518 let dist_r = r_cont
519 - (s.center[0] * s.center[0]
520 + s.center[1] * s.center[1]
521 + s.center[2] * s.center[2])
522 .sqrt();
523 dist_r <= layer_thickness
524 }
525 })
526 .count();
527 n_boundary as f64 / self.spheres.len() as f64
528 }
529 pub fn radial_distribution_function(&self, n_bins: usize, r_max: f64) -> (Vec<f64>, Vec<f64>) {
533 let n = self.spheres.len();
534 if n < 2 || n_bins == 0 {
535 return (Vec::new(), Vec::new());
536 }
537 let dr = r_max / n_bins as f64;
538 let mut hist = vec![0usize; n_bins];
539 let mut n_pairs = 0usize;
540 for i in 0..n {
541 for j in (i + 1)..n {
542 let dist = self.spheres[i].distance_to(&self.spheres[j]);
543 if dist < r_max {
544 let bin = (dist / dr).floor() as usize;
545 if bin < n_bins {
546 hist[bin] += 2;
547 n_pairs += 1;
548 }
549 }
550 }
551 }
552 let vol = self.container_volume();
553 let rho = n as f64 / vol;
554 let r_values: Vec<f64> = (0..n_bins).map(|i| (i as f64 + 0.5) * dr).collect();
555 let g_r: Vec<f64> = (0..n_bins)
556 .map(|i| {
557 let r = r_values[i];
558 let shell_vol = 4.0 * std::f64::consts::PI * r * r * dr;
559 let expected = rho * shell_vol * n as f64;
560 if expected > 1e-30 {
561 hist[i] as f64 / expected
562 } else {
563 0.0
564 }
565 })
566 .collect();
567 let _ = n_pairs;
568 (r_values, g_r)
569 }
570}
571#[derive(Debug, Clone)]
573pub struct VoidSpaceResult {
574 pub pore_size_distribution: Vec<(f64, usize)>,
576 pub percolation_threshold: f64,
578 pub void_volume: f64,
580 pub void_fraction: f64,
582 pub connectivity: f64,
584}
585#[derive(Debug, Clone)]
587pub struct SizeDistributionParams {
588 pub mean_radius: f64,
590 pub std_radius: f64,
592 pub min_radius: f64,
594 pub max_radius: f64,
596 pub size_ratio: f64,
598 pub large_fraction: f64,
600 pub power_law_exponent: f64,
602}
603#[derive(Debug, Clone)]
605pub struct ConfinementConfig {
606 pub shape: ConfinementShape,
608 pub dimensions: [f64; 3],
611 pub sphere_radius: f64,
613 pub max_attempts: usize,
615}
616pub struct RandomPacking {
622 pub config: RsaConfig,
624 pub spheres: Vec<PackingSphere>,
626 pub n_rejected: usize,
628 pub jammed: bool,
630}
631impl RandomPacking {
632 pub fn new(config: RsaConfig) -> Self {
634 Self {
635 config,
636 spheres: Vec::new(),
637 n_rejected: 0,
638 jammed: false,
639 }
640 }
641 pub fn try_place_sphere(&mut self, rng: &mut impl Rng) -> bool {
645 let r = self.config.radius + self.config.min_gap;
646 let [lx, ly, lz] = self.config.domain;
647 let cx = rng.random_range(r..(lx - r).max(r + 1e-12));
648 let cy = rng.random_range(r..(ly - r).max(r + 1e-12));
649 let cz = rng.random_range(r..(lz - r).max(r + 1e-12));
650 let candidate = PackingSphere::new([cx, cy, cz], self.config.radius, self.spheres.len());
651 if self.is_valid_placement(&candidate) {
652 self.spheres.push(candidate);
653 true
654 } else {
655 self.n_rejected += 1;
656 false
657 }
658 }
659 pub fn is_valid_placement(&self, candidate: &PackingSphere) -> bool {
661 let gap = self.config.min_gap;
662 for s in &self.spheres {
663 let min_dist = s.radius + candidate.radius + gap;
664 let dx = s.center[0] - candidate.center[0];
665 let dy = s.center[1] - candidate.center[1];
666 let dz = s.center[2] - candidate.center[2];
667 if dx * dx + dy * dy + dz * dz < min_dist * min_dist {
668 return false;
669 }
670 }
671 true
672 }
673 pub fn run(&mut self) -> usize {
677 let mut rng = rand::rng();
678 let mut consecutive_failures = 0;
679 let jam_threshold = self.config.max_attempts;
680 loop {
681 if self.try_place_sphere(&mut rng) {
682 consecutive_failures = 0;
683 } else {
684 consecutive_failures += 1;
685 if consecutive_failures >= jam_threshold {
686 self.jammed = true;
687 break;
688 }
689 }
690 }
691 self.spheres.len()
692 }
693 pub fn packing_fraction(&self) -> f64 {
695 let [lx, ly, lz] = self.config.domain;
696 let domain_volume = lx * ly * lz;
697 let sphere_volume: f64 = self.spheres.iter().map(|s| s.volume()).sum();
698 sphere_volume / domain_volume
699 }
700 pub fn theoretical_jamming_limit() -> f64 {
704 0.3841
705 }
706 pub fn count_contacts(&self, tolerance: f64) -> usize {
708 let mut count = 0;
709 for i in 0..self.spheres.len() {
710 for j in (i + 1)..self.spheres.len() {
711 let gap = self.spheres[i].overlap_with(&self.spheres[j]);
712 if gap.abs() < tolerance {
713 count += 1;
714 }
715 }
716 }
717 count
718 }
719 pub fn place_at(&mut self, center: [f64; 3]) -> bool {
721 let candidate = PackingSphere::new(center, self.config.radius, self.spheres.len());
722 if self.is_valid_placement(&candidate) {
723 self.spheres.push(candidate);
724 true
725 } else {
726 false
727 }
728 }
729}
730pub struct OrderedPacking {
732 pub lattice: LatticeType,
734 pub radius: f64,
736 pub lattice_parameter: f64,
738 pub n_cells: [usize; 3],
740 pub spheres: Vec<PackingSphere>,
742}
743impl OrderedPacking {
744 pub fn new(lattice: LatticeType, radius: f64, n_cells: [usize; 3]) -> Self {
751 let a = match lattice {
752 LatticeType::Fcc | LatticeType::Hcp => 2.0 * radius * 2.0_f64.sqrt(),
753 LatticeType::Bcc => 4.0 * radius / 3.0_f64.sqrt(),
754 LatticeType::SimpleCubic => 2.0 * radius,
755 };
756 Self {
757 lattice,
758 radius,
759 lattice_parameter: a,
760 n_cells,
761 spheres: Vec::new(),
762 }
763 }
764 pub fn generate(&mut self) {
766 self.spheres.clear();
767 let a = self.lattice_parameter;
768 let [nx, ny, nz] = self.n_cells;
769 let basis: &[[f64; 3]] = match self.lattice {
770 LatticeType::Fcc => &[
771 [0.0, 0.0, 0.0],
772 [0.5, 0.5, 0.0],
773 [0.5, 0.0, 0.5],
774 [0.0, 0.5, 0.5],
775 ],
776 LatticeType::Hcp => &[
777 [0.0, 0.0, 0.0],
778 [0.5, 0.5, 0.0],
779 [0.0, 1.0 / 3.0, 0.5],
780 [0.5, 5.0 / 6.0, 0.5],
781 ],
782 LatticeType::Bcc => &[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
783 LatticeType::SimpleCubic => &[[0.0, 0.0, 0.0]],
784 };
785 for ix in 0..nx {
786 for iy in 0..ny {
787 for iz in 0..nz {
788 for b in basis {
789 let cx = (ix as f64 + b[0]) * a;
790 let cy = (iy as f64 + b[1]) * a;
791 let cz = (iz as f64 + b[2]) * a;
792 let id = self.spheres.len();
793 self.spheres
794 .push(PackingSphere::new([cx, cy, cz], self.radius, id));
795 }
796 }
797 }
798 }
799 }
800 pub fn theoretical_packing_fraction(&self) -> f64 {
802 match self.lattice {
803 LatticeType::Fcc | LatticeType::Hcp => std::f64::consts::PI / (3.0 * 2.0_f64.sqrt()),
804 LatticeType::Bcc => std::f64::consts::PI * 3.0_f64.sqrt() / 8.0,
805 LatticeType::SimpleCubic => std::f64::consts::PI / 6.0,
806 }
807 }
808 pub fn coordination_number(&self) -> usize {
810 match self.lattice {
811 LatticeType::Fcc | LatticeType::Hcp => 12,
812 LatticeType::Bcc => 8,
813 LatticeType::SimpleCubic => 6,
814 }
815 }
816 pub fn actual_packing_fraction(&self) -> f64 {
818 if self.spheres.is_empty() {
819 return 0.0;
820 }
821 let [nx, ny, nz] = self.n_cells;
822 let a = self.lattice_parameter;
823 let volume = (nx as f64 * a) * (ny as f64 * a) * (nz as f64 * a);
824 let sphere_vol: f64 = self.spheres.iter().map(|s| s.volume()).sum();
825 sphere_vol / volume
826 }
827 pub fn nearest_neighbor_distance(&self) -> f64 {
829 2.0 * self.radius
830 }
831 pub fn find_neighbors(&self, idx: usize, cutoff: f64) -> Vec<usize> {
833 let s = &self.spheres[idx];
834 self.spheres
835 .iter()
836 .enumerate()
837 .filter(|(i, other)| *i != idx && s.distance_to(other) <= cutoff)
838 .map(|(i, _)| i)
839 .collect()
840 }
841}
842#[derive(Debug, Clone, Copy, PartialEq)]
844pub enum SizeDistribution {
845 Monodisperse,
847 Bidisperse,
849 LogNormal,
851 Gaussian,
853 PowerLaw,
855}
856#[derive(Debug, Clone)]
858pub struct OptimizerConfig {
859 pub max_iterations: usize,
861 pub time_step: f64,
863 pub tolerance: f64,
865 pub damping: f64,
867 pub stiffness: f64,
869}
870#[derive(Debug, Clone, Copy, PartialEq)]
872pub enum LatticeType {
873 Fcc,
875 Hcp,
877 Bcc,
879 SimpleCubic,
881}
882pub struct PackingOptimizer {
887 pub config: OptimizerConfig,
889 pub spheres: Vec<PackingSphere>,
891 pub domain: [f64; 3],
893 pub convergence: Vec<f64>,
895 pub converged: bool,
897}
898impl PackingOptimizer {
899 pub fn new(spheres: Vec<PackingSphere>, domain: [f64; 3], config: OptimizerConfig) -> Self {
901 Self {
902 config,
903 spheres,
904 domain,
905 convergence: Vec::new(),
906 converged: false,
907 }
908 }
909 fn pair_force(&self, i: usize, j: usize) -> [f64; 3] {
913 let si = &self.spheres[i];
914 let sj = &self.spheres[j];
915 let dx = si.center[0] - sj.center[0];
916 let dy = si.center[1] - sj.center[1];
917 let dz = si.center[2] - sj.center[2];
918 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
919 let min_dist = si.radius + sj.radius;
920 if dist < min_dist && dist > 1e-30 {
921 let overlap = min_dist - dist;
922 let k = self.config.stiffness;
923 let f = k * overlap / dist;
924 [f * dx, f * dy, f * dz]
925 } else {
926 [0.0, 0.0, 0.0]
927 }
928 }
929 fn wall_force(&self, i: usize) -> [f64; 3] {
931 let s = &self.spheres[i];
932 let k = self.config.stiffness;
933 let [lx, ly, lz] = self.domain;
934 let mut f = [0.0f64; 3];
935 let dims = [
936 (s.center[0], 0.0, lx),
937 (s.center[1], 0.0, ly),
938 (s.center[2], 0.0, lz),
939 ];
940 for (d, (c, lo, hi)) in dims.iter().enumerate() {
941 if c - s.radius < *lo {
942 f[d] += k * (lo + s.radius - c);
943 }
944 if c + s.radius > *hi {
945 f[d] -= k * (c + s.radius - hi);
946 }
947 }
948 f
949 }
950 pub fn step(&mut self) -> f64 {
954 let n = self.spheres.len();
955 let mut forces = vec![[0.0f64; 3]; n];
956 for i in 0..n {
957 for j in (i + 1)..n {
958 let f = self.pair_force(i, j);
959 for d in 0..3 {
960 forces[i][d] += f[d];
961 forces[j][d] -= f[d];
962 }
963 }
964 let wf = self.wall_force(i);
965 for d in 0..3 {
966 forces[i][d] += wf[d];
967 }
968 }
969 let dt = self.config.time_step;
970 let damp = self.config.damping;
971 for (i, s) in self.spheres.iter_mut().enumerate() {
972 for (d, center_d) in s.center.iter_mut().enumerate().take(3) {
973 *center_d += dt * (1.0 - damp) * forces[i][d];
974 }
975 }
976 let mut max_overlap = 0.0f64;
977 for i in 0..n {
978 for j in (i + 1)..n {
979 let ov = self.spheres[i].overlap_with(&self.spheres[j]);
980 max_overlap = max_overlap.max(ov);
981 }
982 }
983 max_overlap
984 }
985 pub fn run(&mut self) {
987 self.convergence.clear();
988 for _ in 0..self.config.max_iterations {
989 let max_ov = self.step();
990 self.convergence.push(max_ov);
991 if max_ov < self.config.tolerance {
992 self.converged = true;
993 break;
994 }
995 }
996 }
997 pub fn potential_energy(&self) -> f64 {
999 let n = self.spheres.len();
1000 let k = self.config.stiffness;
1001 let mut energy = 0.0;
1002 for i in 0..n {
1003 for j in (i + 1)..n {
1004 let ov = self.spheres[i].overlap_with(&self.spheres[j]);
1005 if ov > 0.0 {
1006 energy += 0.5 * k * ov * ov;
1007 }
1008 }
1009 }
1010 energy
1011 }
1012 pub fn packing_fraction(&self) -> f64 {
1014 let [lx, ly, lz] = self.domain;
1015 let vol = lx * ly * lz;
1016 let sv: f64 = self.spheres.iter().map(|s| s.volume()).sum();
1017 sv / vol
1018 }
1019 pub fn max_overlap(&self) -> f64 {
1021 let n = self.spheres.len();
1022 let mut max_ov = 0.0f64;
1023 for i in 0..n {
1024 for j in (i + 1)..n {
1025 let ov = self.spheres[i].overlap_with(&self.spheres[j]);
1026 max_ov = max_ov.max(ov);
1027 }
1028 }
1029 max_ov
1030 }
1031}
1032#[derive(Debug, Clone, Copy, PartialEq)]
1034pub enum ConfinementShape {
1035 Box,
1037 Cylinder,
1039 SphericalContainer,
1041}
1042#[derive(Debug, Clone, PartialEq)]
1044pub struct PackingSphere {
1045 pub center: [f64; 3],
1047 pub radius: f64,
1049 pub id: usize,
1051}
1052impl PackingSphere {
1053 pub fn new(center: [f64; 3], radius: f64, id: usize) -> Self {
1055 Self { center, radius, id }
1056 }
1057 pub fn volume(&self) -> f64 {
1059 4.0 / 3.0 * std::f64::consts::PI * self.radius.powi(3)
1060 }
1061 pub fn overlaps(&self, other: &PackingSphere) -> bool {
1063 let min_dist = self.radius + other.radius;
1064 let dx = self.center[0] - other.center[0];
1065 let dy = self.center[1] - other.center[1];
1066 let dz = self.center[2] - other.center[2];
1067 dx * dx + dy * dy + dz * dz < min_dist * min_dist
1068 }
1069 pub fn distance_to(&self, other: &PackingSphere) -> f64 {
1071 let dx = self.center[0] - other.center[0];
1072 let dy = self.center[1] - other.center[1];
1073 let dz = self.center[2] - other.center[2];
1074 (dx * dx + dy * dy + dz * dz).sqrt()
1075 }
1076 pub fn overlap_with(&self, other: &PackingSphere) -> f64 {
1080 let min_dist = self.radius + other.radius;
1081 let dist = self.distance_to(other);
1082 min_dist - dist
1083 }
1084}