// Construct a supercell large enough to contain all atoms that interact with an atom
// in the unit cell with coeffs [0, 0, 0]
//
// FIXME: even for how retardedly conservative this idea is for large unit cells,
//        it's still VERY HARD to actually implement this function correctly!
//        (Just as with all things PBC, general lattices continue to be a nightmare
//         when it comes to finding "sufficiently large search volumes")
//
//        We took the easy way out for nothing!
Construct a supercell large enough to contain all atoms that interact with an atom
in the unit cell with coeffs [0, 0, 0]

even for how retardedly conservative this idea is for large unit cells,
it's still VERY HARD to actually implement this function correctly.
(Just as with all things PBC, general lattices continue to be a nightmare
when it comes to finding "sufficiently large search volumes")

We took the easy way out for nothing!
Construct a supercell large enough to contain all atoms that interact with an atom in the unit cell with coeffs [ 0 0 0 ] even for how retardedly conservative this idea is for large unit cells it ' s still VERY HARD to actually implement this function correctly . ( Just as with all things PBC general lattices continue to be a nightmare when it comes to finding " sufficiently large search volumes " ) We took the easy way out for nothing!
fn sufficiently_large_centered_supercell(
    lattice: &Lattice,
    mut interaction_range: f64,
) -> Result<supercell::Builder, Error> {
    if interaction_range == 0.0 {
        return Ok(supercell::diagonal([1, 1, 1]));
    }
    // Search for a slightly larger range to account for numerical fuzz.
    interaction_range *= 1.0 + 1e-4;
    let get_scaled_lattice = |coeffs: [u32; 3]| {
        let mut vectors = lattice.vectors().clone();
        for k in 0..3 {
            vectors[k] *= coeffs[k] as f64;
        }
        Lattice::from_vectors(&vectors)
    };
    //-------------
    // Bail out on skewed structures, because it's unclear how to handle them.
    //
    // Specifically, this implementation is limited to cells with the following property
    // (henceforth the 'small-skew' property):
    //
    // * Take any two of the lattice vectors; call them `a` and `b`.
    //   Draw a ray from the center of the unit cell which is perpendicular to both
    //   `a` and `b`. When this ray exits the unit cell, it will touch the face of the
    //   periodic boundary spanned by `a` and `b`. (i.e. the one it is normal to)
    //
    // The key advantage of such a cell is that these six "normal ray" intersection points must
    // necessarily be the only points on the parallelepiped where distance to the origin is
    // a local minimum. (hence, the global minimum must be among them)
    //
    // (TODO: can it be shown that a Delaunay or Niggli cell meets this criteria?)
    let check_plane_distances = |lattice: &Lattice| {
        // Recognizing that the plane normals are reciprocal lattice vectors, it is easy to prove
        // the following: The requirement that the ray drawn from the origin along reciprocal basis
        // vector `b_i` does not cross the plane of a face normal to `b_k` before crossing the plane
        // of a face normal to `b_i` can be written as:
        //
        //                         abs(b_i dot b_k) <= b_i dot b_i
        //
        // This condition is `!Lattice::is_large_skew()`.
        //
        // The fuzz is to allow matrices like this, where two faces tie.
        // (i.e. one of the rays exits through an edge of the parallelepiped):
        //
        //                     [[1 1 0], [0 1 0], [0 0 1]]
        //
        // (which is a useful lattice for test cases since it is unimodular)
        //                                                                   - ML
        ensure!(!lattice.is_large_skew(1e-4), "cell is too skewed for bond search");
        // Return the plane distances from the origin.
        Ok(V3::from_fn(|axis| 0.5 * lattice.plane_spacing(V3::axis_unit(axis))))
    };
    let distances = check_plane_distances(lattice)?;
    // If we expand the cell large enough that all of these normal intersection points
    // are at a distance `>= interaction_range` from the center, then we have guaranteed
    // that the cell includes all points in a sphere of interaction radius around the
    // cell center.
    // The small-skew property makes it safe to do this analysis per-axis and take a
    // diagonal supercell.  We just pick large enough multiples along each axis so that rays normal
    // to the cell faces will exit the sphere centered at the origin before it exits its
    // corresponding face.
    let mut coeffs = distances.map(|dist: f64| f64::ceil(interaction_range / dist) as u32).0;
    // ...just one thing.  In theory, by picking different multiples for each cell vector, we may
    // have accidentally destroyed the small-skew property.  (meaning that, while each ray
    // definitely intersects its corresponding face at a distance beyond `interaction_range`, they
    // may theoretically intersect another face before that point)
    //
    // ...I think.  Better safe than sorry, anyways.
    let distances = match check_plane_distances(&get_scaled_lattice(coeffs)) {
        Ok(distances) => {
            trace!("bond graph: intermediate supercell: {:?}, r = {}", coeffs, interaction_range);
            distances
        }
        Err(_) => {
            // Pick a larger cell with uniform scaling.
            coeffs = [*coeffs.iter().max().unwrap(); 3];
            trace!("bond graph: taking uniform intermediate supercell: {:?}, r = {}", coeffs, interaction_range);
            check_plane_distances(&get_scaled_lattice(coeffs))
                .expect("(bug) uniform supercell does not satisfy the property!?")
        }
    };
    // The supercell for 'coeffs' should now accommodate a sphere around the center.
    assert!(distances.iter().all(|&dist| interaction_range <= dist * (1.0 + 1e-4)));
    // But hold on!
    //
    // What we *really* need is to be able to fit a sphere around ANY point in the
    // central cell; not just the center.  Calling the supercell we just chose `S1`,
    // we can pick an even larger supercell `S2` which is guaranteed to include a
    // region shaped like `S1` around any point in the original unit cell.
    //
    // `(2 * c + 1)` cells (where `c` was the old number) should be enough.
    // Lucky for us, we automatically get this by building a centered supercell.
    trace!("bond graph: true supercell: centered_diagonal({:?})", coeffs);
    Ok(supercell::centered_diagonal(coeffs))
}
fn sufficiently_large_centered_supercell ( lattice : & Lattice , mut interaction_range : f64 , ) -> Result < supercell :: Builder , Error > { if interaction_range == 0.0 { return Ok ( supercell :: diagonal ( [ 1 , 1 , 1 ] ) ) ; } // Search for a slightly larger range to account for numerical fuzz. interaction_range *= 1.0 + 1e-4 ; let get_scaled_lattice = | coeffs : [ u32 ; 3 ] | { let mut vectors = lattice . vectors ( ) . clone ( ) ; for k in 0 .. 3 { vectors [ k ] *= coeffs [ k ] as f64 ; } Lattice :: from_vectors ( & vectors ) } ; //------------- // Bail out on skewed structures, because it's unclear how to handle them. // // Specifically, this implementation is limited to cells with the following property // (henceforth the 'small-skew' property): // // * Take any two of the lattice vectors; call them `a` and `b`. //   Draw a ray from the center of the unit cell which is perpendicular to both //   `a` and `b`. When this ray exits the unit cell, it will touch the face of the //   periodic boundary spanned by `a` and `b`. (i.e. the one it is normal to) // // The key advantage of such a cell is that these six "normal ray" intersection points must // necessarily be the only points on the parallelepiped where distance to the origin is // a local minimum. (hence, the global minimum must be among them) // // (TODO: can it be shown that a Delaunay or Niggli cell meets this criteria?) let check_plane_distances = | lattice : & Lattice | { // Recognizing that the plane normals are reciprocal lattice vectors, it is easy to prove // the following: The requirement that the ray drawn from the origin along reciprocal basis // vector `b_i` does not cross the plane of a face normal to `b_k` before crossing the plane // of a face normal to `b_i` can be written as: // //                         abs(b_i dot b_k) <= b_i dot b_i // // This condition is `!Lattice::is_large_skew()`. // // The fuzz is to allow matrices like this, where two faces tie. // (i.e. one of the rays exits through an edge of the parallelepiped): // //                     [[1 1 0], [0 1 0], [0 0 1]] // // (which is a useful lattice for test cases since it is unimodular) //                                                                   - ML ensure ! ( lattice is_large_skew ( 1e-4 ) " " ) ; // Return the plane distances from the origin. Ok ( V3 :: from_fn ( | axis | 0.5 * lattice . plane_spacing ( V3 :: axis_unit ( axis ) ) ) ) } ; let distances = check_plane_distances ( lattice ) ? ; // If we expand the cell large enough that all of these normal intersection points // are at a distance `>= interaction_range` from the center, then we have guaranteed // that the cell includes all points in a sphere of interaction radius around the // cell center. // The small-skew property makes it safe to do this analysis per-axis and take a // diagonal supercell.  We just pick large enough multiples along each axis so that rays normal // to the cell faces will exit the sphere centered at the origin before it exits its // corresponding face. let mut coeffs = distances . map ( | dist : f64 | f64 :: ceil ( interaction_range / dist ) as u32 ) . 0 ; // ...just one thing.  In theory, by picking different multiples for each cell vector, we may // have accidentally destroyed the small-skew property.  (meaning that, while each ray // definitely intersects its corresponding face at a distance beyond `interaction_range`, they // may theoretically intersect another face before that point) // // ...I think.  Better safe than sorry, anyways. let distances = match check_plane_distances ( & get_scaled_lattice ( coeffs ) ) { Ok ( distances ) => { trace ! ( " " coeffs interaction_range ) ; distances } Err ( _ ) => { // Pick a larger cell with uniform scaling. coeffs = [ * coeffs . iter ( ) . max ( ) . unwrap ( ) ; 3 ] ; trace ! ( " " coeffs interaction_range ) ; check_plane_distances ( & get_scaled_lattice ( coeffs ) ) . expect ( " " ) } } ; // The supercell for 'coeffs' should now accommodate a sphere around the center. assert ! ( distances iter ( ) all ( dist interaction_range dist ( 1.0 1e-4 ) ) ) ; // But hold on! // // What we *really* need is to be able to fit a sphere around ANY point in the // central cell; not just the center.  Calling the supercell we just chose `S1`, // we can pick an even larger supercell `S2` which is guaranteed to include a // region shaped like `S1` around any point in the original unit cell. // // `(2 * c + 1)` cells (where `c` was the old number) should be enough. // Lucky for us, we automatically get this by building a centered supercell. trace ! ( " " coeffs ) ; Ok ( supercell :: centered_diagonal ( coeffs ) ) }
Construct a supercell large enough to contain all atoms that interact with an atom
in the unit cell with coeffs [0, 0, 0]
Construct a supercell large enough to contain all atoms that interact with an atom in the unit cell with coeffs [ 0 0 0 ]
lattice
&Lattice
interaction_range
f64