use crystallographic_group::HallSymbolNotation;
use crystallographic_group::database::{DEFAULT_SPACE_GROUP_SYMBOLS, LookUpSpaceGroup};
use crate::feffinp::{Crystal, Error, Site};
fn wrap01(x: f64) -> f64 {
let v = x - x.floor();
if v >= 1.0 { 0.0 } else { v }
}
fn frac_close(a: [f64; 3], b: [f64; 3]) -> bool {
a.iter().zip(b.iter()).all(|(&p, &q)| {
let mut d = p - q;
d -= d.round();
d.abs() < 1.0e-4
})
}
pub fn expand_sites(space_group: u32, sites: &[Site]) -> Result<Vec<Site>, Error> {
if !(1..=230).contains(&space_group) {
return Err(Error::SpaceGroup(space_group));
}
let hall = DEFAULT_SPACE_GROUP_SYMBOLS
.get_hall_symbol((space_group - 1) as usize)
.ok_or(Error::SpaceGroup(space_group))?;
let notation = HallSymbolNotation::try_from_str(hall)
.map_err(|_| Error::Parse(format!("could not parse Hall symbol `{hall}`")))?;
let ops = notation.general_positions().derive_full_sets();
let mut out: Vec<Site> = Vec::new();
for s in sites {
let f = s.frac;
for set in &ops {
for op in set {
let m = op.to_f64_mat();
let np = [
wrap01(m[(0, 0)] * f[0] + m[(0, 1)] * f[1] + m[(0, 2)] * f[2] + m[(0, 3)]),
wrap01(m[(1, 0)] * f[0] + m[(1, 1)] * f[1] + m[(1, 2)] * f[2] + m[(1, 3)]),
wrap01(m[(2, 0)] * f[0] + m[(2, 1)] * f[1] + m[(2, 2)] * f[2] + m[(2, 3)]),
];
if !out
.iter()
.any(|o| o.element == s.element && frac_close(o.frac, np))
{
out.push(Site {
element: s.element.clone(),
frac: np,
});
}
}
}
}
Ok(out)
}
impl Crystal {
pub fn expand(&self, space_group: u32) -> Result<Crystal, Error> {
Ok(Crystal {
lattice: self.lattice,
sites: expand_sites(space_group, &self.sites)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::feffinp::{Edge, Lattice};
fn has_site(sites: &[Site], elem: &str, frac: [f64; 3]) -> bool {
sites
.iter()
.any(|s| s.element == elem && frac_close(s.frac, frac))
}
#[test]
fn p1_is_identity() {
let s = vec![Site::new("Cu", 0.1, 0.2, 0.3)];
let out = expand_sites(1, &s).expect("expand");
assert_eq!(out.len(), 1);
assert!(frac_close(out[0].frac, [0.1, 0.2, 0.3]));
}
#[test]
fn fm3m_expands_single_atom_to_fcc() {
let out = expand_sites(225, &[Site::new("Cu", 0.0, 0.0, 0.0)]).expect("expand");
assert_eq!(out.len(), 4, "fcc has 4 atoms in the conventional cell");
for frac in [
[0.0, 0.0, 0.0],
[0.5, 0.5, 0.0],
[0.5, 0.0, 0.5],
[0.0, 0.5, 0.5],
] {
assert!(has_site(&out, "Cu", frac), "missing fcc site {frac:?}");
}
}
#[test]
fn im3m_expands_single_atom_to_bcc() {
let out = expand_sites(229, &[Site::new("Fe", 0.0, 0.0, 0.0)]).expect("expand");
assert_eq!(out.len(), 2);
assert!(has_site(&out, "Fe", [0.0, 0.0, 0.0]));
assert!(has_site(&out, "Fe", [0.5, 0.5, 0.5]));
}
#[test]
fn fm3m_rocksalt_two_sublattices() {
let out = expand_sites(
225,
&[
Site::new("Na", 0.0, 0.0, 0.0),
Site::new("Cl", 0.5, 0.5, 0.5),
],
)
.expect("expand");
assert_eq!(out.iter().filter(|s| s.element == "Na").count(), 4);
assert_eq!(out.iter().filter(|s| s.element == "Cl").count(), 4);
}
#[test]
fn expanded_fcc_cluster_matches_manual_full_cell() {
let asym = Crystal {
lattice: Lattice::cubic(3.61),
sites: vec![Site::new("Cu", 0.0, 0.0, 0.0)],
};
let full = asym.expand(225).expect("expand");
assert_eq!(full.sites.len(), 4);
let c = full.cluster(0, 3.0, Edge::K).expect("cluster");
let first = c
.atoms
.iter()
.filter(|a| a.ipot != 0 && a.distance < 2.6)
.count();
assert_eq!(first, 12, "fcc first shell is 12 neighbours");
}
#[test]
fn out_of_range() {
assert!(matches!(expand_sites(0, &[]), Err(Error::SpaceGroup(0))));
assert!(matches!(
expand_sites(231, &[]),
Err(Error::SpaceGroup(231))
));
}
}