use chematic_crystal::PeriodicStructure;
use mikiwame::{OwnedStructure, Site};
pub fn to_mikiwame_structure(structure: &PeriodicStructure) -> OwnedStructure {
let mut lattice = structure.lattice().matrix();
let inverted = determinant(&lattice) < 0.0;
if inverted {
lattice.swap(1, 2); }
let mut sites = Vec::new();
for site in structure.sites() {
let mut fractional = site.fractional.0;
if inverted {
fractional.swap(1, 2); }
let mut per_element: Vec<(String, f64)> = Vec::new();
for species in &site.species {
let symbol = species.element.symbol().to_string();
let occupancy = species.occupancy.value();
match per_element
.iter_mut()
.find(|(element, _)| *element == symbol)
{
Some((_, total)) => *total += occupancy,
None => per_element.push((symbol, occupancy)),
}
}
for (element, occupancy) in per_element {
if occupancy == 0.0 {
continue; }
sites.push(Site {
element,
fractional,
occupancy,
});
}
}
OwnedStructure::new(lattice, sites)
}
fn determinant(m: &[[f64; 3]; 3]) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_core::Element;
use chematic_crystal::{FractionalCoord, Lattice, Occupancy, PeriodicSite, SiteSpecies};
use mikiwame::{AnalysisConfig, FindingCode, PeriodicStructureView, Verdict};
fn structure(lattice: Lattice, sites: Vec<PeriodicSite>) -> PeriodicStructure {
PeriodicStructure::new(lattice, sites).unwrap()
}
#[test]
fn single_element_full_occupancy_produces_one_site() {
let s = structure(
Lattice::cubic(4.0).unwrap(),
vec![
PeriodicSite::new(
vec![SiteSpecies::full(Element::FE)],
FractionalCoord::new([0.1, 0.2, 0.3]),
None,
)
.unwrap(),
],
);
let converted = to_mikiwame_structure(&s);
let sites = converted.sites();
assert_eq!(sites.len(), 1);
assert_eq!(sites[0].element, "Fe");
assert_eq!(sites[0].occupancy, 1.0);
assert_eq!(sites[0].fractional, [0.1, 0.2, 0.3]);
}
#[test]
fn mixed_occupancy_site_expands_into_two_mikiwame_sites() {
let s = structure(
Lattice::cubic(4.0).unwrap(),
vec![
PeriodicSite::new(
vec![
SiteSpecies {
element: Element::FE,
occupancy: Occupancy::new(0.6).unwrap(),
},
SiteSpecies {
element: Element::NI,
occupancy: Occupancy::new(0.4).unwrap(),
},
],
FractionalCoord::new([0.25, 0.25, 0.25]),
None,
)
.unwrap(),
],
);
let converted = to_mikiwame_structure(&s);
let sites = converted.sites();
assert_eq!(sites.len(), 2);
for site in sites {
assert_eq!(site.fractional, [0.25, 0.25, 0.25]);
}
let fe = sites.iter().find(|s| s.element == "Fe").unwrap();
let ni = sites.iter().find(|s| s.element == "Ni").unwrap();
assert_eq!(fe.occupancy, 0.6);
assert_eq!(ni.occupancy, 0.4);
}
#[test]
fn same_element_within_one_site_is_consolidated() {
let s = structure(
Lattice::cubic(4.0).unwrap(),
vec![
PeriodicSite::new(
vec![
SiteSpecies {
element: Element::FE,
occupancy: Occupancy::new(0.3).unwrap(),
},
SiteSpecies {
element: Element::FE,
occupancy: Occupancy::new(0.3).unwrap(),
},
],
FractionalCoord::new([0.1, 0.1, 0.1]),
None,
)
.unwrap(),
],
);
let converted = to_mikiwame_structure(&s);
let sites = converted.sites();
assert_eq!(sites.len(), 1);
assert_eq!(sites[0].element, "Fe");
assert!((sites[0].occupancy - 0.6).abs() < 1e-12);
}
#[test]
fn consolidation_can_produce_an_occupancy_slightly_above_one() {
let s = structure(
Lattice::cubic(4.0).unwrap(),
vec![
PeriodicSite::new(
vec![
SiteSpecies {
element: Element::FE,
occupancy: Occupancy::new(0.5).unwrap(),
},
SiteSpecies {
element: Element::FE,
occupancy: Occupancy::new(0.5000005).unwrap(),
},
],
FractionalCoord::new([0.1, 0.1, 0.1]),
None,
)
.unwrap(),
],
);
let converted = to_mikiwame_structure(&s);
let sites = converted.sites();
assert_eq!(sites.len(), 1);
assert!(
sites[0].occupancy > 1.0,
"expected an occupancy above 1.0, got {}",
sites[0].occupancy
);
let report = mikiwame::analyze(&converted, &AnalysisConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.code == FindingCode::InputInvalidOccupancy),
"mikiwame should flag the consolidated occupancy itself: {:?}",
report.findings
);
}
#[test]
fn zero_occupancy_after_consolidation_is_excluded() {
let zero_site = PeriodicSite::new(
vec![
SiteSpecies {
element: Element::FE,
occupancy: Occupancy::new(0.0).unwrap(),
},
SiteSpecies {
element: Element::FE,
occupancy: Occupancy::new(0.0).unwrap(),
},
],
FractionalCoord::new([0.1, 0.1, 0.1]),
None,
)
.unwrap();
let normal_site = PeriodicSite::new(
vec![SiteSpecies::full(Element::NA)],
FractionalCoord::new([0.5, 0.5, 0.5]),
None,
)
.unwrap();
let s = structure(Lattice::cubic(4.0).unwrap(), vec![zero_site, normal_site]);
let converted = to_mikiwame_structure(&s);
let sites = converted.sites();
assert_eq!(sites.len(), 1);
assert_eq!(sites[0].element, "Na");
}
#[test]
fn two_periodic_sites_at_the_same_element_and_position_are_kept_distinct_and_mikiwame_still_flags_them()
{
let a = PeriodicSite::new(
vec![SiteSpecies::full(Element::FE)],
FractionalCoord::new([0.25, 0.25, 0.25]),
Some("Fe-A".to_string()),
)
.unwrap();
let b = PeriodicSite::new(
vec![SiteSpecies::full(Element::FE)],
FractionalCoord::new([0.25, 0.25, 0.25]),
Some("Fe-B".to_string()),
)
.unwrap();
let s = structure(Lattice::cubic(5.0).unwrap(), vec![a, b]);
let converted = to_mikiwame_structure(&s);
assert_eq!(
converted.sites().len(),
2,
"distinct sites must not be merged"
);
let report = mikiwame::analyze(&converted, &AnalysisConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.code == FindingCode::SiteDuplicate),
"a genuine cross-site duplicate must still be caught: {:?}",
report.findings
);
}
#[test]
fn non_orthogonal_lattice_with_asymmetric_rows_is_not_transposed() {
let matrix = [[1.0, 0.0, 0.0], [0.5, 2.0, 0.0], [0.3, 0.4, 3.0]];
let s = structure(
Lattice::from_matrix(matrix).unwrap(),
vec![
PeriodicSite::new(
vec![SiteSpecies::full(Element::FE)],
FractionalCoord::new([0.2, 0.3, 0.4]),
None,
)
.unwrap(),
],
);
let converted = to_mikiwame_structure(&s);
assert_eq!(*converted.lattice(), matrix);
assert_eq!(converted.sites()[0].fractional, [0.2, 0.3, 0.4]);
}
#[test]
fn left_handed_lattice_is_corrected_without_moving_any_site() {
let matrix = [[2.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.5, 0.5, -4.0]];
assert!(determinant(&matrix) < 0.0, "fixture must be left-handed");
let original_fractional = [0.3, 0.4, 0.5];
let s = structure(
Lattice::from_matrix(matrix).unwrap(),
vec![
PeriodicSite::new(
vec![SiteSpecies::full(Element::FE)],
FractionalCoord::new(original_fractional),
None,
)
.unwrap(),
],
);
let converted = to_mikiwame_structure(&s);
let converted_lattice = *converted.lattice();
assert!(
determinant(&converted_lattice) > 0.0,
"handedness must be corrected"
);
fn cart(frac: [f64; 3], lattice: [[f64; 3]; 3]) -> [f64; 3] {
[
frac[0] * lattice[0][0] + frac[1] * lattice[1][0] + frac[2] * lattice[2][0],
frac[0] * lattice[0][1] + frac[1] * lattice[1][1] + frac[2] * lattice[2][1],
frac[0] * lattice[0][2] + frac[1] * lattice[1][2] + frac[2] * lattice[2][2],
]
}
let before = cart(original_fractional, matrix);
let after = cart(converted.sites()[0].fractional, converted_lattice);
for i in 0..3 {
assert!(
(before[i] - after[i]).abs() < 1e-9,
"axis {i}: {before:?} vs {after:?}"
);
}
}
#[test]
fn converted_structure_runs_through_real_mikiwame_analyze() {
let s = structure(
Lattice::cubic(5.64).unwrap(),
vec![
PeriodicSite::new(
vec![SiteSpecies::full(Element::NA)],
FractionalCoord::new([0.0, 0.0, 0.0]),
None,
)
.unwrap(),
PeriodicSite::new(
vec![SiteSpecies::full(Element::CL)],
FractionalCoord::new([0.5, 0.5, 0.5]),
None,
)
.unwrap(),
],
);
let converted = to_mikiwame_structure(&s);
let report = mikiwame::analyze(&converted, &AnalysisConfig::default());
assert_eq!(
report.overall.verdict,
Verdict::StructurallyConsistent,
"unexpected findings: {:?}",
report.findings
);
}
}