spherical_cow/
lib.rs

1//! **Spherical Cow**: *A high volume fraction sphere packing library*.
2//!
3//! # Usage
4//!
5//! First, add `spherical-cow` to the dependencies in your project's `Cargo.toml`.
6//!
7//! ```toml
8//! [dependencies]
9//! spherical-cow = "0.1"
10//! ```
11//!
12//! If you'd like to enable serialization through `serde` add this line instead to turn on that feature
13//!
14//! ```toml
15//! spherical-cow = { version = "0.1", features = ["serde-1"] }
16//! ```
17//!
18//! To calculate the `volume_fraction` of a spherical container with radius 2 filled with spheres of radii between 0.05 and 0.1 is straightforward:
19//!
20//! ```rust,no_run
21//! use spherical_cow::shapes::Sphere;
22//! use spherical_cow::PackedVolume;
23//! use rand::distributions::Uniform;
24//! use nalgebra::{Matrix, Point3};
25//!
26//! fn main() {
27//!     // Pack spheres with radii between 0.05 and 0.1 into a spherical container of radius 2,
28//!     // output quantitative analysis data.
29//!     let boundary = Sphere::new(Point3::origin(), 2.0).unwrap();
30//!     let mut sizes = Uniform::new(0.05, 0.1);
31//!
32//!     let packed = PackedVolume::new(boundary, &mut sizes).unwrap();
33//!
34//!     println!("Volume Fraction: {:.2}%", packed.volume_fraction() * 100.);
35//! }
36//! ```
37//!
38//! A full list of examples can be found in the [examples](https://github.com/Libbum/spherical-cow/tree/master/examples) directory.
39//!
40//! # Research
41//!
42//! The method implemented herein is an advancing front algorithm from
43//! Valera *et al.*, [Computational Particle Mechanics 2, 161 (2015)](https://doi.org/10.1007/s40571-015-0045-8).
44
45#![warn(missing_docs)]
46#![allow(clippy::needless_doctest_main)]
47
48pub mod errors;
49#[cfg(feature = "serde-1")]
50mod serialization;
51pub mod shapes;
52pub mod util;
53
54use crate::errors::SphericalCowError as Error;
55use crate::shapes::Sphere;
56use float_cmp::ApproxEqRatio;
57use itertools::Itertools;
58use nalgebra::core::{Matrix, Matrix3};
59use nalgebra::Point3;
60use rand::distributions::Distribution;
61use rand::prelude::SliceRandom;
62
63/// The `Container` trait must be implemented for all shapes you wish to pack spheres into.
64/// Standard shapes such as spheres and cuboids already derrive this trait. More complicated
65/// shapes such as a triangular mesh are also straightforward to implement, examples
66/// of such can be seen in the
67/// [`show_in_emerald`](https://github.com/Libbum/spherical-cow/blob/master/examples/show_in_emerald.rs)
68/// and [`show_in_cow`](https://github.com/Libbum/spherical-cow/blob/master/examples/show_in_cow.rs) files.
69pub trait Container {
70    /// Checks if a sphere exists inside some bounding geometry.
71    fn contains(&self, sphere: &Sphere) -> bool;
72    /// Calculates the volume of this container in normalised units.
73    fn volume(&self) -> f32;
74}
75
76/// To obtain quantitative values of your packing effectiveness, `PackedVolume` provides
77/// a number of useful indicators of such.
78#[derive(Debug)]
79pub struct PackedVolume<C> {
80    /// A set of spheres generated by a call to [pack_spheres](fn.pack_spheres.html).
81    pub spheres: Vec<Sphere>,
82    /// The container in which spheres have been packed.
83    pub container: C,
84}
85
86impl<C: Container> PackedVolume<C> {
87    /// Creates a new `PackedVolume` by calling [pack_spheres](fn.pack_spheres.html) with a given distribution of sphere sizes
88    /// and a `container` to pack into.
89    pub fn new<D: Distribution<f64>>(
90        container: C,
91        mut size_distribution: &mut D,
92    ) -> Result<PackedVolume<C>, Error> {
93        let spheres = pack_spheres::<C, D>(&container, &mut size_distribution)?;
94        Ok(PackedVolume::<C> { spheres, container })
95    }
96
97    /// Creates a `PackedVolume` from a pre calculated cluster of `spheres`. Useful for gathering statistics from
98    /// packings generated elsewhere for comparison to the current algorithm. Also used for deserialization.
99    /// This method is currently unchecked, so use with caution.
100    pub fn from_vec(spheres: Vec<Sphere>, container: C) -> PackedVolume<C> {
101        PackedVolume::<C> { spheres, container }
102    }
103
104    /// Calculates the volume fraction ν = Vs/V: the volume of all spheres packed into a container
105    /// divided by the volume of said container.
106    ///
107    /// The [Kepler Conjecture](http://mathworld.wolfram.com/KeplerConjecture.html) suggests that the
108    /// densest possible volume fraction for equal sized spheres is ~74.05%. However, the higest possible
109    /// volume fraction for any random radii distribution is currently unknown to
110    /// mathematicians. The algorithm implemented herin obtains 59.29% in a cube with side lengths of
111    /// 90 with sphere radii between 0.01 and 0.02, compared with a long standing and well known
112    /// [geometric compression algorithm](10.1016/j.powtec.2005.04.055) which achieved 52.89%.
113    pub fn volume_fraction(&self) -> f32 {
114        let vol_spheres: f32 = self.spheres.iter().map(|sphere| sphere.volume()).sum();
115        vol_spheres / self.container.volume()
116    }
117
118    /// Calculates the void ratio e = Vv/Vs: the volume of all void space divided by the volume of
119    /// solids in the container. Here we take 'solids' to mean volumes of all packed spheres.
120    pub fn void_ratio(&self) -> f32 {
121        let vol_spheres: f32 = self.spheres.iter().map(|sphere| sphere.volume()).sum();
122        let vol_total = self.container.volume();
123        (vol_total - vol_spheres) / vol_spheres
124    }
125
126    /// The coordination number indicates the connectivity of the packing.
127    /// For any given sphere in the packing, its coordination number is defined as
128    /// the number of spheres it is in contact with. This function returns the
129    /// arethmetic mean of all coordination numbers in the packing, yielding a
130    /// overall coordination number of the system.
131    pub fn coordination_number(&self) -> f32 {
132        let num_particles = self.spheres.len() as f32;
133        let mut coordinations = 0;
134        for idx in 0..self.spheres.len() {
135            coordinations += self.sphere_contacts_count(idx);
136        }
137        coordinations as f32 / num_particles
138    }
139
140    /// Generates the fabric tensor of the packing. The sum of all eigenvalues phi_i,j will always equal 1.
141    /// Perfectly isotropic packing should see the diagonals of this matrix = 1/3. Deviations from this value
142    /// indicates the amount of anisotropy in the system.
143    pub fn fabric_tensor(&self) -> Matrix3<f32> {
144        let phi = |i: usize, j: usize| {
145            let mut sum_all = 0.;
146            for idx in 0..self.spheres.len() {
147                let center = self.spheres[idx].center.coords;
148                // The set of all spheres in contact with the current sphere
149                let p_c = self.sphere_contacts(idx);
150                // Number of spheres in contact with the current sphere
151                let m_p = p_c.len() as f32;
152                let mut sum_vec = 0.;
153                for c in &p_c {
154                    let vec_n_pc = Matrix::cross(&center, &c.center.coords);
155                    // The unit vector pointing from the center of the current sphere to
156                    // the center of a connecting sphere
157                    let n_pc = vec_n_pc / Matrix::norm(&vec_n_pc);
158                    sum_vec += n_pc[i] * n_pc[j];
159                }
160                sum_all += sum_vec / m_p;
161            }
162            // phiᵢⱼ
163            1. / self.spheres.len() as f32 * sum_all
164        };
165        Matrix3::from_fn(phi)
166    }
167
168    /// Returns a set of spheres connected to the sphere at a chosen index.
169    fn sphere_contacts(&self, sphere_idx: usize) -> Vec<Sphere> {
170        let center = self.spheres[sphere_idx].center;
171        let radius = self.spheres[sphere_idx].radius;
172        self.spheres
173            .iter()
174            .cloned()
175            .filter(|sphere| {
176                nalgebra::distance(&center, &sphere.center)
177                    .approx_eq_ratio(&(radius + sphere.radius), 0.0001)
178            })
179            .collect()
180    }
181
182    /// Calculates the number of contacts a sphere has with the rest of the packed set.
183    fn sphere_contacts_count(&self, sphere_idx: usize) -> usize {
184        let center = self.spheres[sphere_idx].center;
185        let radius = self.spheres[sphere_idx].radius;
186        self.spheres
187            .iter()
188            .filter(|sphere| {
189                nalgebra::distance(&center, &sphere.center)
190                    .approx_eq_ratio(&(radius + sphere.radius), 0.0001)
191            })
192            .count()
193    }
194}
195
196/// Packs all habitat spheres to be as dense as possible.
197/// Requires a `containter` and a distribution of radii sizes.
198///
199/// Generally, a uniform distribution is chosen, although this library
200/// accepts any distribution implementing `rand`s `Distribution` trait.
201/// This [example](https://github.com/Libbum/spherical-cow/blob/master/examples/count_sphere_normal.rs)
202/// uses a normally distributed radii range. Note that the packing is sub optimal in this case, and
203/// attention must be paid when using such distributions that radii values do not become negagive.
204pub fn pack_spheres<C: Container, D: Distribution<f64>>(
205    container: &C,
206    size_distribution: &mut D,
207) -> Result<Vec<Sphere>, Error> {
208    // Distribution is already derrived for all distributions in `rand` with f64,
209    // so we just downsample here instead of implementing traits on f32 for everything.
210    let mut rng = rand::thread_rng();
211
212    // Radii of three initial spheres, taken from the input distribution
213    let init_radii: [f32; 3] = [
214        size_distribution.sample(&mut rng) as f32,
215        size_distribution.sample(&mut rng) as f32,
216        size_distribution.sample(&mut rng) as f32,
217    ];
218
219    // S := {s₁, s₂, s₃}
220    let mut spheres = init_spheres(&init_radii, container)?;
221
222    // F := {s₁, s₂, s₃}
223    let mut front = spheres.clone();
224
225    // Radius of new sphere to be added to the current front, taken from the input distribution
226    let mut new_radius = size_distribution.sample(&mut rng) as f32;
227
228    let mut set_v = Vec::new();
229    let mut set_f = Vec::new();
230    'outer: while !front.is_empty() {
231        // s₀ := s(c₀, r₀) picked at random from F
232        let curr_sphere = front.choose(&mut rng).ok_or(Error::NoneFront)?.clone();
233        // V := {s(c', r') ∈ S : d(c₀, c') ≤ r₀ + r' + 2r}
234        set_v.clear();
235        set_v = spheres
236            .iter()
237            .cloned()
238            .filter(|s_dash| {
239                s_dash != &curr_sphere
240                    && nalgebra::distance(&curr_sphere.center, &s_dash.center)
241                        <= curr_sphere.radius + s_dash.radius + 2. * new_radius
242            })
243            .collect::<Vec<_>>();
244
245        for (s_i, s_j) in set_v.iter().tuple_combinations::<(&Sphere, &Sphere)>() {
246            set_f.clear();
247            identify_f(
248                &mut set_f,
249                &curr_sphere,
250                s_i,
251                s_j,
252                container,
253                &set_v,
254                new_radius,
255            )?;
256            if !set_f.is_empty() {
257                // Found at least one position to place the sphere,
258                // choose one and move on
259                let s_new = set_f.choose(&mut rng).ok_or(Error::NoneSetF)?;
260                front.push(s_new.clone());
261                spheres.push(s_new.clone());
262                new_radius = size_distribution.sample(&mut rng) as f32;
263                continue 'outer;
264            }
265        }
266        if let Some(i) = front.iter().position(|s| s == &curr_sphere) {
267            front.remove(i);
268        }
269    }
270    Ok(spheres)
271}
272
273/// Creates three initial spheres that are tangent pairwise. The incenter of the triangle formed
274/// by verticies located at the centers of each sphere is aligned at the origin.
275fn init_spheres<C: Container>(radii: &[f32; 3], container: &C) -> Result<Vec<Sphere>, Error> {
276    let mut init = Vec::new();
277
278    //            C (x,y)
279    //            ^
280    //           / \
281    //        b /   \ a
282    //         /     \
283    //        /       \
284    // A (0,0)--------- B (c,0)
285    //            c
286
287    // Sphere A can sit at the origin, sphere B extends outward along the x axis
288    // sphere C extends outward along the y axis and complete the triangle
289    let radius_a = radii[0];
290    let radius_b = radii[1];
291    let radius_c = radii[2];
292    let distance_c = radius_a + radius_b;
293    let distance_b = radius_a + radius_c;
294    let distance_a = radius_c + radius_b;
295
296    let b_2 = distance_b.powi(2);
297    let x = (b_2 + distance_c.powi(2) - distance_a.powi(2)) / (2. * distance_c);
298    let y = (b_2 - x.powi(2)).sqrt();
299
300    // Find incenter
301    let perimeter = distance_a + distance_b + distance_c;
302    let incenter_x = (distance_b * distance_c + distance_c * x) / perimeter;
303    let incenter_y = (distance_c * y) / perimeter;
304
305    // Create spheres at positions shown in the diagram above, but offset such
306    // that the incenter is now the origin. This offset attempts to minimise
307    // bounding box issues in the sense that c may be close to or over the
308    // bb boundary already
309    let s_1 = Sphere::new(Point3::new(-incenter_x, -incenter_y, 0.), radius_a)?;
310    if container.contains(&s_1) {
311        init.push(s_1);
312    } else {
313        return Err(Error::Uncontained);
314    }
315    let s_2 = Sphere::new(
316        Point3::new(distance_c - incenter_x, -incenter_y, 0.),
317        radius_b,
318    )?;
319    if container.contains(&s_2) {
320        init.push(s_2);
321    } else {
322        return Err(Error::Uncontained);
323    }
324    let s_3 = Sphere::new(Point3::new(x - incenter_x, y - incenter_y, 0.), radius_c)?;
325    if container.contains(&s_3) {
326        init.push(s_3);
327    } else {
328        return Err(Error::Uncontained);
329    }
330
331    Ok(init)
332}
333
334/// $f$ is as a set of spheres (or the empty set) such that they have a known `radius`,
335/// are in outer contact with `s_1`, `s_2` and `s_3` simultaneously, are completely
336/// contained in `container` and do not overlap with any element of `set_v`.
337/// The set f has at most two elements, because there exist at most two spheres with
338/// `radius` in outer contact with `s_1`, `s_2` and `s_3` simultaneously.
339fn identify_f<C: Container>(
340    set_f: &mut Vec<Sphere>,
341    s_1: &Sphere,
342    s_2: &Sphere,
343    s_3: &Sphere,
344    container: &C,
345    set_v: &[Sphere],
346    radius: f32,
347) -> Result<(), Error> {
348    //The center points of s_1, s_2, s_3 are verticies of a tetrahedron,
349    //and the distances d_1, d_2, d_3 can be defined as the distances from these points to
350    //a fourth vertex s_4, whose coordinates x,y,z must be found. This satisfies the equations
351    // (x-x_1)^2+(y-y_1)^2+(z-z_1)^2=d_1^2 (1)
352    // (x-x_2)^2+(y-y_2)^2+(z-z_2)^2=d_2^2 (2)
353    // (x-x_3)^2+(y-y_3)^2+(z-z_3)^2=d_3^2 (3)
354
355    //To solve this system, we subtract (1) from (2) & (3), to obtain the (linear) equations of two planes.
356    //Coupling these planes to (1) we yield a quadratic system which takes the form
357    // \vec u\cdot\vec r=a
358    // \vec v\cdot\vec r=b
359    // \vec r\cdot\vec r+\vec w\cdot\vec r=c
360
361    // With a little bit of magic following https://axiomatic.neophilus.net/posts/2018-01-16-clustering-tangent-spheres.html
362    // we can solve this system to identify r in the form
363    // \vec r=\alpha\vec u+\beta\vec v+\gamma\vec t
364    // Where \gamma has a quadratic solution identifying our two solutions.
365
366    let distance_14 = s_1.radius + radius;
367    let distance_24 = s_2.radius + radius;
368    let distance_34 = s_3.radius + radius;
369
370    let vector_u = s_1.center - s_2.center;
371    let unitvector_u = vector_u / Matrix::norm(&vector_u);
372    let vector_v = s_1.center - s_3.center;
373    let unitvector_v = vector_v / Matrix::norm(&vector_v);
374    let cross_uv = Matrix::cross(&vector_u, &vector_v);
375    let unitvector_t = cross_uv / Matrix::norm(&cross_uv);
376    let vector_w = -2. * s_1.center.coords;
377
378    let distance_c =
379        distance_14.powi(2) - s_1.center.x.powi(2) - s_1.center.y.powi(2) - s_1.center.z.powi(2);
380    let distance_a = (distance_24.powi(2)
381        - distance_c
382        - s_2.center.x.powi(2)
383        - s_2.center.y.powi(2)
384        - s_2.center.z.powi(2))
385        / (2. * Matrix::norm(&vector_u));
386    let distance_b = (distance_34.powi(2)
387        - distance_c
388        - s_3.center.x.powi(2)
389        - s_3.center.y.powi(2)
390        - s_3.center.z.powi(2))
391        / (2. * Matrix::norm(&vector_v));
392
393    let dot_uv = Matrix::dot(&unitvector_u, &unitvector_v);
394    let dot_wt = Matrix::dot(&vector_w, &unitvector_t);
395    let dot_uw = Matrix::dot(&unitvector_u, &vector_w);
396    let dot_vw = Matrix::dot(&unitvector_v, &vector_w);
397
398    let denominator = 1. - dot_uv.powi(2);
399    let alpha = (distance_a - distance_b * dot_uv) / denominator;
400    let beta = (distance_b - distance_a * dot_uv) / denominator;
401    let value_d =
402        alpha.powi(2) + beta.powi(2) + 2. * alpha * beta * dot_uv + alpha * dot_uw + beta * dot_vw
403            - distance_c;
404
405    // There is a possiblity of obtaining imaginary solutions in gamma,
406    // so we must check this comparison. TODO: Would be nice to have
407    // some quick way of verifying this configuration and deny it early.
408    let dot_wt_2 = dot_wt.powi(2);
409    let value_4d = 4. * value_d;
410    if dot_wt_2 > value_4d {
411        let gamma_pos = 0.5 * (-dot_wt + (dot_wt_2 - value_4d).sqrt());
412        let gamma_neg = 0.5 * (-dot_wt - (dot_wt_2 - value_4d).sqrt());
413
414        let s_4_positive = Sphere::new(
415            Point3::from(alpha * unitvector_u + beta * unitvector_v + gamma_pos * unitvector_t),
416            radius,
417        )?;
418        let s_4_negative = Sphere::new(
419            Point3::from(alpha * unitvector_u + beta * unitvector_v + gamma_neg * unitvector_t),
420            radius,
421        )?;
422
423        // Make sure the spheres are bounded by the containing geometry and do not overlap any spheres in V
424        if container.contains(&s_4_positive) && !set_v.iter().any(|v| v.overlaps(&s_4_positive)) {
425            set_f.push(s_4_positive);
426        }
427        if container.contains(&s_4_negative) && !set_v.iter().any(|v| v.overlaps(&s_4_negative)) {
428            set_f.push(s_4_negative);
429        }
430    }
431    Ok(())
432}
433
434#[test]
435fn init_spheres_err() {
436    let container = Sphere::new(Point3::origin(), 0.1).unwrap();
437    assert!(init_spheres(&[10., 15., 20.], &container).is_err());
438}
439
440#[test]
441fn identify_f_known() {
442    let one = Sphere::new(Point3::new(0.5, -0.28112677, 0.0), 0.5).unwrap();
443    let two = Sphere::new(Point3::new(0.058333218, 0.44511732, 0.0), 0.35).unwrap();
444    let three = Sphere::new(Point3::new(-0.70000005, -0.28112677, 0.0), 0.7).unwrap();
445    let container = Sphere::new(Point3::origin(), 20.0).unwrap();
446
447    let four_p = Sphere::new(Point3::new(0.06666666, 0.12316025, 0.6773287), 0.4).unwrap();
448    let four_n = Sphere::new(Point3::new(0.06666666, 0.12316025, -0.6773287), 0.4).unwrap();
449
450    let mut found = Vec::new();
451    identify_f::<Sphere>(&mut found, &one, &two, &three, &container, &Vec::new(), 0.4).unwrap();
452
453    println!("{:?}", found);
454    assert!(found.contains(&four_p));
455    assert!(found.contains(&four_n));
456}