1#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use super::functions::*;
8use rand::Rng;
9use rand::RngExt;
10
11#[derive(Debug, Clone)]
13pub struct RsaConfig {
14 pub radius: f64,
16 pub domain: [f64; 3],
18 pub max_attempts: usize,
20 pub min_gap: f64,
22}
23pub struct PolyDispersePacking {
25 pub distribution: SizeDistribution,
27 pub params: SizeDistributionParams,
29 pub domain: [f64; 3],
31 pub spheres: Vec<PackingSphere>,
33 pub max_attempts: usize,
35}
36impl PolyDispersePacking {
37 pub fn new(
39 distribution: SizeDistribution,
40 params: SizeDistributionParams,
41 domain: [f64; 3],
42 max_attempts: usize,
43 ) -> Self {
44 Self {
45 distribution,
46 params,
47 domain,
48 spheres: Vec::new(),
49 max_attempts,
50 }
51 }
52 pub fn sample_radius(&self, rng: &mut impl Rng) -> f64 {
54 match self.distribution {
55 SizeDistribution::Monodisperse => self.params.mean_radius,
56 SizeDistribution::Bidisperse => {
57 if rng.random::<f64>() < self.params.large_fraction {
58 self.params.mean_radius * self.params.size_ratio
59 } else {
60 self.params.mean_radius
61 }
62 }
63 SizeDistribution::LogNormal => {
64 let mu = self.params.mean_radius.ln();
65 let sigma = self.params.std_radius;
66 let z = self.normal_sample(rng);
67 let r = (mu + sigma * z).exp();
68 r.clamp(self.params.min_radius, self.params.max_radius)
69 }
70 SizeDistribution::Gaussian => {
71 let r = self.params.mean_radius + self.params.std_radius * self.normal_sample(rng);
72 r.clamp(self.params.min_radius, self.params.max_radius)
73 }
74 SizeDistribution::PowerLaw => {
75 let alpha = self.params.power_law_exponent;
76 let r_min = self.params.min_radius;
77 let r_max = self.params.max_radius;
78 let u: f64 = rng.random();
79 if (alpha + 1.0).abs() < 1e-12 {
80 r_min * (r_max / r_min).powf(u)
81 } else {
82 let p = alpha + 1.0;
83 (r_min.powf(p) + u * (r_max.powf(p) - r_min.powf(p))).powf(1.0 / p)
84 }
85 }
86 }
87 }
88 fn normal_sample(&self, rng: &mut impl Rng) -> f64 {
90 let u1: f64 = rng.random::<f64>().max(1e-300);
91 let u2: f64 = rng.random();
92 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
93 }
94 pub fn is_valid_placement(&self, center: [f64; 3], radius: f64) -> bool {
96 let [lx, ly, lz] = self.domain;
97 if center[0] - radius < 0.0
98 || center[0] + radius > lx
99 || center[1] - radius < 0.0
100 || center[1] + radius > ly
101 || center[2] - radius < 0.0
102 || center[2] + radius > lz
103 {
104 return false;
105 }
106 let candidate = PackingSphere::new(center, radius, 0);
107 for s in &self.spheres {
108 if s.overlaps(&candidate) {
109 return false;
110 }
111 }
112 true
113 }
114 pub fn run(&mut self, n_spheres: usize) -> usize {
118 let mut rng = rand::rng();
119 let mut attempts = 0;
120 while self.spheres.len() < n_spheres && attempts < self.max_attempts {
121 let r = self.sample_radius(&mut rng);
122 let [lx, ly, lz] = self.domain;
123 let cx = rng.random_range(r..(lx - r).max(r + 1e-12));
124 let cy = rng.random_range(r..(ly - r).max(r + 1e-12));
125 let cz = rng.random_range(r..(lz - r).max(r + 1e-12));
126 if self.is_valid_placement([cx, cy, cz], r) {
127 let id = self.spheres.len();
128 self.spheres.push(PackingSphere::new([cx, cy, cz], r, id));
129 }
130 attempts += 1;
131 }
132 self.spheres.len()
133 }
134 pub fn packing_fraction(&self) -> f64 {
136 let [lx, ly, lz] = self.domain;
137 let vol = lx * ly * lz;
138 let sv: f64 = self.spheres.iter().map(|s| s.volume()).sum();
139 sv / vol
140 }
141 pub fn radius_statistics(&self) -> (f64, f64, f64, f64) {
143 if self.spheres.is_empty() {
144 return (0.0, 0.0, 0.0, 0.0);
145 }
146 let radii: Vec<f64> = self.spheres.iter().map(|s| s.radius).collect();
147 let n = radii.len() as f64;
148 let mean = radii.iter().sum::<f64>() / n;
149 let variance = radii.iter().map(|&r| (r - mean).powi(2)).sum::<f64>() / n;
150 let std = variance.sqrt();
151 let min = radii.iter().cloned().fold(f64::INFINITY, f64::min);
152 let max = radii.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
153 (mean, std, min, max)
154 }
155 pub fn polydispersity_index(&self) -> f64 {
157 let (mean, std, _, _) = self.radius_statistics();
158 if mean > 1e-30 { std / mean } else { 0.0 }
159 }
160 pub fn size_histogram(&self, n_bins: usize) -> (Vec<f64>, Vec<usize>) {
164 if self.spheres.is_empty() || n_bins == 0 {
165 return (Vec::new(), Vec::new());
166 }
167 let (_, _, min_r, max_r) = self.radius_statistics();
168 let bin_width = (max_r - min_r) / n_bins as f64;
169 if bin_width < 1e-30 {
170 let centers = vec![min_r; n_bins];
171 let mut counts = vec![0usize; n_bins];
172 counts[0] = self.spheres.len();
173 return (centers, counts);
174 }
175 let mut counts = vec![0usize; n_bins];
176 for s in &self.spheres {
177 let bin = ((s.radius - min_r) / bin_width).floor() as usize;
178 let bin = bin.min(n_bins - 1);
179 counts[bin] += 1;
180 }
181 let centers: Vec<f64> = (0..n_bins)
182 .map(|i| min_r + (i as f64 + 0.5) * bin_width)
183 .collect();
184 (centers, counts)
185 }
186}
187pub struct VoidSpaceAnalysis {
192 pub spheres: Vec<PackingSphere>,
194 pub domain: [f64; 3],
196 pub grid_resolution: usize,
198}
199impl VoidSpaceAnalysis {
200 pub fn new(spheres: Vec<PackingSphere>, domain: [f64; 3], grid_resolution: usize) -> Self {
202 Self {
203 spheres,
204 domain,
205 grid_resolution,
206 }
207 }
208 pub fn point_in_sphere(&self, pt: [f64; 3]) -> bool {
210 for s in &self.spheres {
211 let dx = pt[0] - s.center[0];
212 let dy = pt[1] - s.center[1];
213 let dz = pt[2] - s.center[2];
214 if dx * dx + dy * dy + dz * dz <= s.radius * s.radius {
215 return true;
216 }
217 }
218 false
219 }
220 pub fn void_fraction_by_voxelization(&self) -> f64 {
222 let n = self.grid_resolution;
223 let [lx, ly, lz] = self.domain;
224 let dx = lx / n as f64;
225 let dy = ly / n as f64;
226 let dz = lz / n as f64;
227 let mut void_count = 0usize;
228 let total = n * n * n;
229 for ix in 0..n {
230 for iy in 0..n {
231 for iz in 0..n {
232 let pt = [
233 (ix as f64 + 0.5) * dx,
234 (iy as f64 + 0.5) * dy,
235 (iz as f64 + 0.5) * dz,
236 ];
237 if !self.point_in_sphere(pt) {
238 void_count += 1;
239 }
240 }
241 }
242 }
243 void_count as f64 / total as f64
244 }
245 pub fn pore_radius_at(&self, pt: [f64; 3]) -> f64 {
249 if self.point_in_sphere(pt) {
250 return 0.0;
251 }
252 let mut min_dist_to_surface = f64::INFINITY;
253 for s in &self.spheres {
254 let dx = pt[0] - s.center[0];
255 let dy = pt[1] - s.center[1];
256 let dz = pt[2] - s.center[2];
257 let dist_to_center = (dx * dx + dy * dy + dz * dz).sqrt();
258 let dist_to_surface = (dist_to_center - s.radius).max(0.0);
259 min_dist_to_surface = min_dist_to_surface.min(dist_to_surface);
260 }
261 let [lx, ly, lz] = self.domain;
262 let wall_dist = [pt[0], lx - pt[0], pt[1], ly - pt[1], pt[2], lz - pt[2]]
263 .iter()
264 .cloned()
265 .fold(f64::INFINITY, f64::min);
266 min_dist_to_surface.min(wall_dist)
267 }
268 pub fn pore_size_distribution(&self, n_samples: usize, n_bins: usize) -> Vec<(f64, usize)> {
273 let mut rng = rand::rng();
274 let [lx, ly, lz] = self.domain;
275 let mut pore_radii = Vec::new();
276 for _ in 0..n_samples {
277 let pt = [
278 rng.random_range(0.0..lx),
279 rng.random_range(0.0..ly),
280 rng.random_range(0.0..lz),
281 ];
282 if !self.point_in_sphere(pt) {
283 pore_radii.push(self.pore_radius_at(pt));
284 }
285 }
286 if pore_radii.is_empty() || n_bins == 0 {
287 return Vec::new();
288 }
289 let r_min = pore_radii.iter().cloned().fold(f64::INFINITY, f64::min);
290 let r_max = pore_radii.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
291 let bin_width = (r_max - r_min) / n_bins as f64;
292 if bin_width < 1e-30 {
293 return vec![(r_min, pore_radii.len())];
294 }
295 let mut counts = vec![0usize; n_bins];
296 for &r in &pore_radii {
297 let bin = ((r - r_min) / bin_width).floor() as usize;
298 counts[bin.min(n_bins - 1)] += 1;
299 }
300 (0..n_bins)
301 .map(|i| (r_min + (i as f64 + 0.5) * bin_width, counts[i]))
302 .collect()
303 }
304 pub fn estimate_percolation_threshold(&self) -> f64 {
309 0.3116
310 }
311 pub fn is_percolating(&self, void_fraction: f64) -> bool {
313 void_fraction > self.estimate_percolation_threshold()
314 }
315 pub fn specific_surface_area(&self) -> f64 {
317 let [lx, ly, lz] = self.domain;
318 let vol = lx * ly * lz;
319 let sa: f64 = self
320 .spheres
321 .iter()
322 .map(|s| 4.0 * std::f64::consts::PI * s.radius * s.radius)
323 .sum();
324 sa / vol
325 }
326 pub fn analyze(&self, n_psd_samples: usize, n_bins: usize) -> VoidSpaceResult {
328 let void_frac = self.void_fraction_by_voxelization();
329 let [lx, ly, lz] = self.domain;
330 let void_vol = void_frac * lx * ly * lz;
331 let psd = self.pore_size_distribution(n_psd_samples, n_bins);
332 VoidSpaceResult {
333 pore_size_distribution: psd,
334 percolation_threshold: self.estimate_percolation_threshold(),
335 void_volume: void_vol,
336 void_fraction: void_frac,
337 connectivity: if void_frac > 0.32 { 1.0 } else { 0.0 },
338 }
339 }
340}
341pub struct ConfiningGeometry {
346 pub config: ConfinementConfig,
348 pub spheres: Vec<PackingSphere>,
350 pub n_rejected: usize,
352}
353impl ConfiningGeometry {
354 pub fn new(config: ConfinementConfig) -> Self {
356 Self {
357 config,
358 spheres: Vec::new(),
359 n_rejected: 0,
360 }
361 }
362 pub fn point_inside_container(&self, center: [f64; 3], radius: f64) -> bool {
364 match self.config.shape {
365 ConfinementShape::Box => {
366 let [hx, hy, hz] = self.config.dimensions;
367 center[0] - radius >= -hx
368 && center[0] + radius <= hx
369 && center[1] - radius >= -hy
370 && center[1] + radius <= hy
371 && center[2] - radius >= -hz
372 && center[2] + radius <= hz
373 }
374 ConfinementShape::Cylinder => {
375 let r_cyl = self.config.dimensions[0];
376 let h_cyl = self.config.dimensions[1];
377 let r2 = center[0] * center[0] + center[1] * center[1];
378 r2.sqrt() + radius <= r_cyl
379 && center[2] - radius >= -h_cyl
380 && center[2] + radius <= h_cyl
381 }
382 ConfinementShape::SphericalContainer => {
383 let r_cont = self.config.dimensions[0];
384 let dist =
385 (center[0] * center[0] + center[1] * center[1] + center[2] * center[2]).sqrt();
386 dist + radius <= r_cont
387 }
388 }
389 }
390 pub fn random_point_in_container(&self, rng: &mut impl Rng) -> [f64; 3] {
392 match self.config.shape {
393 ConfinementShape::Box => {
394 let [hx, hy, hz] = self.config.dimensions;
395 [
396 rng.random_range(-hx..hx),
397 rng.random_range(-hy..hy),
398 rng.random_range(-hz..hz),
399 ]
400 }
401 ConfinementShape::Cylinder => {
402 let r_cyl = self.config.dimensions[0];
403 let h_cyl = self.config.dimensions[1];
404 loop {
405 let x = rng.random_range(-r_cyl..r_cyl);
406 let y = rng.random_range(-r_cyl..r_cyl);
407 let z = rng.random_range(-h_cyl..h_cyl);
408 if (x * x + y * y).sqrt() <= r_cyl {
409 return [x, y, z];
410 }
411 }
412 }
413 ConfinementShape::SphericalContainer => {
414 let r_cont = self.config.dimensions[0];
415 loop {
416 let x = rng.random_range(-r_cont..r_cont);
417 let y = rng.random_range(-r_cont..r_cont);
418 let z = rng.random_range(-r_cont..r_cont);
419 if (x * x + y * y + z * z).sqrt() <= r_cont {
420 return [x, y, z];
421 }
422 }
423 }
424 }
425 }
426 fn has_overlap(&self, center: [f64; 3], radius: f64) -> bool {
428 let candidate = PackingSphere::new(center, radius, 0);
429 for s in &self.spheres {
430 if s.overlaps(&candidate) {
431 return true;
432 }
433 }
434 false
435 }
436 pub fn try_place(&mut self, rng: &mut impl Rng) -> bool {
438 let r = self.config.sphere_radius;
439 let center = self.random_point_in_container(rng);
440 if self.point_inside_container(center, r) && !self.has_overlap(center, r) {
441 let id = self.spheres.len();
442 self.spheres.push(PackingSphere::new(center, r, id));
443 true
444 } else {
445 self.n_rejected += 1;
446 false
447 }
448 }
449 pub fn run(&mut self) -> usize {
451 let mut rng = rand::rng();
452 let mut consecutive_failures = 0;
453 loop {
454 if self.try_place(&mut rng) {
455 consecutive_failures = 0;
456 } else {
457 consecutive_failures += 1;
458 if consecutive_failures >= self.config.max_attempts {
459 break;
460 }
461 }
462 }
463 self.spheres.len()
464 }
465 pub fn container_volume(&self) -> f64 {
467 match self.config.shape {
468 ConfinementShape::Box => {
469 let [hx, hy, hz] = self.config.dimensions;
470 8.0 * hx * hy * hz
471 }
472 ConfinementShape::Cylinder => {
473 let r = self.config.dimensions[0];
474 let h = self.config.dimensions[1];
475 std::f64::consts::PI * r * r * 2.0 * h
476 }
477 ConfinementShape::SphericalContainer => {
478 let r = self.config.dimensions[0];
479 4.0 / 3.0 * std::f64::consts::PI * r * r * r
480 }
481 }
482 }
483 pub fn packing_fraction(&self) -> f64 {
485 let vol = self.container_volume();
486 let sv: f64 = self.spheres.iter().map(|s| s.volume()).sum();
487 if vol > 1e-30 { sv / vol } else { 0.0 }
488 }
489 pub fn boundary_layer_fraction(&self) -> f64 {
493 if self.spheres.is_empty() {
494 return 0.0;
495 }
496 let r = self.config.sphere_radius;
497 let layer_thickness = 2.0 * r;
498 let n_boundary = self
499 .spheres
500 .iter()
501 .filter(|s| match self.config.shape {
502 ConfinementShape::Box => {
503 let [hx, hy, hz] = self.config.dimensions;
504 let dist_x = hx - s.center[0].abs();
505 let dist_y = hy - s.center[1].abs();
506 let dist_z = hz - s.center[2].abs();
507 [dist_x, dist_y, dist_z]
508 .iter()
509 .any(|&d| d <= layer_thickness)
510 }
511 ConfinementShape::Cylinder => {
512 let r_cyl = self.config.dimensions[0];
513 let h_cyl = self.config.dimensions[1];
514 let dist_r =
515 r_cyl - (s.center[0] * s.center[0] + s.center[1] * s.center[1]).sqrt();
516 let dist_z = h_cyl - s.center[2].abs();
517 dist_r <= layer_thickness || dist_z <= layer_thickness
518 }
519 ConfinementShape::SphericalContainer => {
520 let r_cont = self.config.dimensions[0];
521 let dist_r = r_cont
522 - (s.center[0] * s.center[0]
523 + s.center[1] * s.center[1]
524 + s.center[2] * s.center[2])
525 .sqrt();
526 dist_r <= layer_thickness
527 }
528 })
529 .count();
530 n_boundary as f64 / self.spheres.len() as f64
531 }
532 pub fn radial_distribution_function(&self, n_bins: usize, r_max: f64) -> (Vec<f64>, Vec<f64>) {
536 let n = self.spheres.len();
537 if n < 2 || n_bins == 0 {
538 return (Vec::new(), Vec::new());
539 }
540 let dr = r_max / n_bins as f64;
541 let mut hist = vec![0usize; n_bins];
542 let mut n_pairs = 0usize;
543 for i in 0..n {
544 for j in (i + 1)..n {
545 let dist = self.spheres[i].distance_to(&self.spheres[j]);
546 if dist < r_max {
547 let bin = (dist / dr).floor() as usize;
548 if bin < n_bins {
549 hist[bin] += 2;
550 n_pairs += 1;
551 }
552 }
553 }
554 }
555 let vol = self.container_volume();
556 let rho = n as f64 / vol;
557 let r_values: Vec<f64> = (0..n_bins).map(|i| (i as f64 + 0.5) * dr).collect();
558 let g_r: Vec<f64> = (0..n_bins)
559 .map(|i| {
560 let r = r_values[i];
561 let shell_vol = 4.0 * std::f64::consts::PI * r * r * dr;
562 let expected = rho * shell_vol * n as f64;
563 if expected > 1e-30 {
564 hist[i] as f64 / expected
565 } else {
566 0.0
567 }
568 })
569 .collect();
570 let _ = n_pairs;
571 (r_values, g_r)
572 }
573}
574#[derive(Debug, Clone)]
576pub struct VoidSpaceResult {
577 pub pore_size_distribution: Vec<(f64, usize)>,
579 pub percolation_threshold: f64,
581 pub void_volume: f64,
583 pub void_fraction: f64,
585 pub connectivity: f64,
587}
588#[derive(Debug, Clone)]
590pub struct SizeDistributionParams {
591 pub mean_radius: f64,
593 pub std_radius: f64,
595 pub min_radius: f64,
597 pub max_radius: f64,
599 pub size_ratio: f64,
601 pub large_fraction: f64,
603 pub power_law_exponent: f64,
605}
606#[derive(Debug, Clone)]
608pub struct ConfinementConfig {
609 pub shape: ConfinementShape,
611 pub dimensions: [f64; 3],
614 pub sphere_radius: f64,
616 pub max_attempts: usize,
618}
619pub struct RandomPacking {
625 pub config: RsaConfig,
627 pub spheres: Vec<PackingSphere>,
629 pub n_rejected: usize,
631 pub jammed: bool,
633}
634impl RandomPacking {
635 pub fn new(config: RsaConfig) -> Self {
637 Self {
638 config,
639 spheres: Vec::new(),
640 n_rejected: 0,
641 jammed: false,
642 }
643 }
644 pub fn try_place_sphere(&mut self, rng: &mut impl Rng) -> bool {
648 let r = self.config.radius + self.config.min_gap;
649 let [lx, ly, lz] = self.config.domain;
650 let cx = rng.random_range(r..(lx - r).max(r + 1e-12));
651 let cy = rng.random_range(r..(ly - r).max(r + 1e-12));
652 let cz = rng.random_range(r..(lz - r).max(r + 1e-12));
653 let candidate = PackingSphere::new([cx, cy, cz], self.config.radius, self.spheres.len());
654 if self.is_valid_placement(&candidate) {
655 self.spheres.push(candidate);
656 true
657 } else {
658 self.n_rejected += 1;
659 false
660 }
661 }
662 pub fn is_valid_placement(&self, candidate: &PackingSphere) -> bool {
664 let gap = self.config.min_gap;
665 for s in &self.spheres {
666 let min_dist = s.radius + candidate.radius + gap;
667 let dx = s.center[0] - candidate.center[0];
668 let dy = s.center[1] - candidate.center[1];
669 let dz = s.center[2] - candidate.center[2];
670 if dx * dx + dy * dy + dz * dz < min_dist * min_dist {
671 return false;
672 }
673 }
674 true
675 }
676 pub fn run(&mut self) -> usize {
680 let mut rng = rand::rng();
681 let mut consecutive_failures = 0;
682 let jam_threshold = self.config.max_attempts;
683 loop {
684 if self.try_place_sphere(&mut rng) {
685 consecutive_failures = 0;
686 } else {
687 consecutive_failures += 1;
688 if consecutive_failures >= jam_threshold {
689 self.jammed = true;
690 break;
691 }
692 }
693 }
694 self.spheres.len()
695 }
696 pub fn packing_fraction(&self) -> f64 {
698 let [lx, ly, lz] = self.config.domain;
699 let domain_volume = lx * ly * lz;
700 let sphere_volume: f64 = self.spheres.iter().map(|s| s.volume()).sum();
701 sphere_volume / domain_volume
702 }
703 pub fn theoretical_jamming_limit() -> f64 {
707 0.3841
708 }
709 pub fn count_contacts(&self, tolerance: f64) -> usize {
711 let mut count = 0;
712 for i in 0..self.spheres.len() {
713 for j in (i + 1)..self.spheres.len() {
714 let gap = self.spheres[i].overlap_with(&self.spheres[j]);
715 if gap.abs() < tolerance {
716 count += 1;
717 }
718 }
719 }
720 count
721 }
722 pub fn place_at(&mut self, center: [f64; 3]) -> bool {
724 let candidate = PackingSphere::new(center, self.config.radius, self.spheres.len());
725 if self.is_valid_placement(&candidate) {
726 self.spheres.push(candidate);
727 true
728 } else {
729 false
730 }
731 }
732}
733pub struct OrderedPacking {
735 pub lattice: LatticeType,
737 pub radius: f64,
739 pub lattice_parameter: f64,
741 pub n_cells: [usize; 3],
743 pub spheres: Vec<PackingSphere>,
745}
746impl OrderedPacking {
747 pub fn new(lattice: LatticeType, radius: f64, n_cells: [usize; 3]) -> Self {
754 let a = match lattice {
755 LatticeType::Fcc | LatticeType::Hcp => 2.0 * radius * 2.0_f64.sqrt(),
756 LatticeType::Bcc => 4.0 * radius / 3.0_f64.sqrt(),
757 LatticeType::SimpleCubic => 2.0 * radius,
758 };
759 Self {
760 lattice,
761 radius,
762 lattice_parameter: a,
763 n_cells,
764 spheres: Vec::new(),
765 }
766 }
767 pub fn generate(&mut self) {
769 self.spheres.clear();
770 let a = self.lattice_parameter;
771 let [nx, ny, nz] = self.n_cells;
772 let basis: &[[f64; 3]] = match self.lattice {
773 LatticeType::Fcc => &[
774 [0.0, 0.0, 0.0],
775 [0.5, 0.5, 0.0],
776 [0.5, 0.0, 0.5],
777 [0.0, 0.5, 0.5],
778 ],
779 LatticeType::Hcp => &[
780 [0.0, 0.0, 0.0],
781 [0.5, 0.5, 0.0],
782 [0.0, 1.0 / 3.0, 0.5],
783 [0.5, 5.0 / 6.0, 0.5],
784 ],
785 LatticeType::Bcc => &[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
786 LatticeType::SimpleCubic => &[[0.0, 0.0, 0.0]],
787 };
788 for ix in 0..nx {
789 for iy in 0..ny {
790 for iz in 0..nz {
791 for b in basis {
792 let cx = (ix as f64 + b[0]) * a;
793 let cy = (iy as f64 + b[1]) * a;
794 let cz = (iz as f64 + b[2]) * a;
795 let id = self.spheres.len();
796 self.spheres
797 .push(PackingSphere::new([cx, cy, cz], self.radius, id));
798 }
799 }
800 }
801 }
802 }
803 pub fn theoretical_packing_fraction(&self) -> f64 {
805 match self.lattice {
806 LatticeType::Fcc | LatticeType::Hcp => std::f64::consts::PI / (3.0 * 2.0_f64.sqrt()),
807 LatticeType::Bcc => std::f64::consts::PI * 3.0_f64.sqrt() / 8.0,
808 LatticeType::SimpleCubic => std::f64::consts::PI / 6.0,
809 }
810 }
811 pub fn coordination_number(&self) -> usize {
813 match self.lattice {
814 LatticeType::Fcc | LatticeType::Hcp => 12,
815 LatticeType::Bcc => 8,
816 LatticeType::SimpleCubic => 6,
817 }
818 }
819 pub fn actual_packing_fraction(&self) -> f64 {
821 if self.spheres.is_empty() {
822 return 0.0;
823 }
824 let [nx, ny, nz] = self.n_cells;
825 let a = self.lattice_parameter;
826 let volume = (nx as f64 * a) * (ny as f64 * a) * (nz as f64 * a);
827 let sphere_vol: f64 = self.spheres.iter().map(|s| s.volume()).sum();
828 sphere_vol / volume
829 }
830 pub fn nearest_neighbor_distance(&self) -> f64 {
832 2.0 * self.radius
833 }
834 pub fn find_neighbors(&self, idx: usize, cutoff: f64) -> Vec<usize> {
836 let s = &self.spheres[idx];
837 self.spheres
838 .iter()
839 .enumerate()
840 .filter(|(i, other)| *i != idx && s.distance_to(other) <= cutoff)
841 .map(|(i, _)| i)
842 .collect()
843 }
844}
845#[derive(Debug, Clone, Copy, PartialEq)]
847pub enum SizeDistribution {
848 Monodisperse,
850 Bidisperse,
852 LogNormal,
854 Gaussian,
856 PowerLaw,
858}
859#[derive(Debug, Clone)]
861pub struct OptimizerConfig {
862 pub max_iterations: usize,
864 pub time_step: f64,
866 pub tolerance: f64,
868 pub damping: f64,
870 pub stiffness: f64,
872}
873#[derive(Debug, Clone, Copy, PartialEq)]
875pub enum LatticeType {
876 Fcc,
878 Hcp,
880 Bcc,
882 SimpleCubic,
884}
885pub struct PackingOptimizer {
890 pub config: OptimizerConfig,
892 pub spheres: Vec<PackingSphere>,
894 pub domain: [f64; 3],
896 pub convergence: Vec<f64>,
898 pub converged: bool,
900}
901impl PackingOptimizer {
902 pub fn new(spheres: Vec<PackingSphere>, domain: [f64; 3], config: OptimizerConfig) -> Self {
904 Self {
905 config,
906 spheres,
907 domain,
908 convergence: Vec::new(),
909 converged: false,
910 }
911 }
912 fn pair_force(&self, i: usize, j: usize) -> [f64; 3] {
916 let si = &self.spheres[i];
917 let sj = &self.spheres[j];
918 let dx = si.center[0] - sj.center[0];
919 let dy = si.center[1] - sj.center[1];
920 let dz = si.center[2] - sj.center[2];
921 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
922 let min_dist = si.radius + sj.radius;
923 if dist < min_dist && dist > 1e-30 {
924 let overlap = min_dist - dist;
925 let k = self.config.stiffness;
926 let f = k * overlap / dist;
927 [f * dx, f * dy, f * dz]
928 } else {
929 [0.0, 0.0, 0.0]
930 }
931 }
932 fn wall_force(&self, i: usize) -> [f64; 3] {
934 let s = &self.spheres[i];
935 let k = self.config.stiffness;
936 let [lx, ly, lz] = self.domain;
937 let mut f = [0.0f64; 3];
938 let dims = [
939 (s.center[0], 0.0, lx),
940 (s.center[1], 0.0, ly),
941 (s.center[2], 0.0, lz),
942 ];
943 for (d, (c, lo, hi)) in dims.iter().enumerate() {
944 if c - s.radius < *lo {
945 f[d] += k * (lo + s.radius - c);
946 }
947 if c + s.radius > *hi {
948 f[d] -= k * (c + s.radius - hi);
949 }
950 }
951 f
952 }
953 pub fn step(&mut self) -> f64 {
957 let n = self.spheres.len();
958 let mut forces = vec![[0.0f64; 3]; n];
959 for i in 0..n {
960 for j in (i + 1)..n {
961 let f = self.pair_force(i, j);
962 for d in 0..3 {
963 forces[i][d] += f[d];
964 forces[j][d] -= f[d];
965 }
966 }
967 let wf = self.wall_force(i);
968 for d in 0..3 {
969 forces[i][d] += wf[d];
970 }
971 }
972 let dt = self.config.time_step;
973 let damp = self.config.damping;
974 for (i, s) in self.spheres.iter_mut().enumerate() {
975 for d in 0..3 {
976 s.center[d] += dt * (1.0 - damp) * forces[i][d];
977 }
978 }
979 let mut max_overlap = 0.0f64;
980 for i in 0..n {
981 for j in (i + 1)..n {
982 let ov = self.spheres[i].overlap_with(&self.spheres[j]);
983 max_overlap = max_overlap.max(ov);
984 }
985 }
986 max_overlap
987 }
988 pub fn run(&mut self) {
990 self.convergence.clear();
991 for _ in 0..self.config.max_iterations {
992 let max_ov = self.step();
993 self.convergence.push(max_ov);
994 if max_ov < self.config.tolerance {
995 self.converged = true;
996 break;
997 }
998 }
999 }
1000 pub fn potential_energy(&self) -> f64 {
1002 let n = self.spheres.len();
1003 let k = self.config.stiffness;
1004 let mut energy = 0.0;
1005 for i in 0..n {
1006 for j in (i + 1)..n {
1007 let ov = self.spheres[i].overlap_with(&self.spheres[j]);
1008 if ov > 0.0 {
1009 energy += 0.5 * k * ov * ov;
1010 }
1011 }
1012 }
1013 energy
1014 }
1015 pub fn packing_fraction(&self) -> f64 {
1017 let [lx, ly, lz] = self.domain;
1018 let vol = lx * ly * lz;
1019 let sv: f64 = self.spheres.iter().map(|s| s.volume()).sum();
1020 sv / vol
1021 }
1022 pub fn max_overlap(&self) -> f64 {
1024 let n = self.spheres.len();
1025 let mut max_ov = 0.0f64;
1026 for i in 0..n {
1027 for j in (i + 1)..n {
1028 let ov = self.spheres[i].overlap_with(&self.spheres[j]);
1029 max_ov = max_ov.max(ov);
1030 }
1031 }
1032 max_ov
1033 }
1034}
1035#[derive(Debug, Clone, Copy, PartialEq)]
1037pub enum ConfinementShape {
1038 Box,
1040 Cylinder,
1042 SphericalContainer,
1044}
1045#[derive(Debug, Clone, PartialEq)]
1047pub struct PackingSphere {
1048 pub center: [f64; 3],
1050 pub radius: f64,
1052 pub id: usize,
1054}
1055impl PackingSphere {
1056 pub fn new(center: [f64; 3], radius: f64, id: usize) -> Self {
1058 Self { center, radius, id }
1059 }
1060 pub fn volume(&self) -> f64 {
1062 4.0 / 3.0 * std::f64::consts::PI * self.radius.powi(3)
1063 }
1064 pub fn overlaps(&self, other: &PackingSphere) -> bool {
1066 let min_dist = self.radius + other.radius;
1067 let dx = self.center[0] - other.center[0];
1068 let dy = self.center[1] - other.center[1];
1069 let dz = self.center[2] - other.center[2];
1070 dx * dx + dy * dy + dz * dz < min_dist * min_dist
1071 }
1072 pub fn distance_to(&self, other: &PackingSphere) -> f64 {
1074 let dx = self.center[0] - other.center[0];
1075 let dy = self.center[1] - other.center[1];
1076 let dz = self.center[2] - other.center[2];
1077 (dx * dx + dy * dy + dz * dz).sqrt()
1078 }
1079 pub fn overlap_with(&self, other: &PackingSphere) -> f64 {
1083 let min_dist = self.radius + other.radius;
1084 let dist = self.distance_to(other);
1085 min_dist - dist
1086 }
1087}