use bon::Builder;
use castep_cell_fmt::{
CResult, Cell, CellValue, Error, ToCell, ToCellFile,
parse::{FromBlock, FromCellFile},
query::find_block,
};
use crate::cell::{
constraints_params::ConstraintsParams,
dynamics_params::DynamicsParams,
external_field_params::ExternalFieldParams,
kpoints_params::KpointsParams,
lattice_param::{LatticeABC, LatticeCart},
optics_magres_params::OpticsMagresParams,
phonon_params::PhononParams,
phonon_fine_params::PhononFineParams,
positions::{PositionsAbs, PositionsFrac},
species_params::SpeciesParams,
spectral_params::SpectralParams,
symmetry_params::SymmetryParams,
};
use cell_document_builder::IsComplete;
#[derive(Debug, Clone)]
pub enum Lattice {
Cart(LatticeCart),
Abc(LatticeABC),
}
impl ToCell for Lattice {
fn to_cell(&self) -> Cell<'_> {
match self {
Lattice::Cart(cart) => cart.to_cell(),
Lattice::Abc(abc) => abc.to_cell(),
}
}
}
impl From<LatticeCart> for Lattice {
fn from(v: LatticeCart) -> Self {
Lattice::Cart(v)
}
}
impl From<LatticeABC> for Lattice {
fn from(v: LatticeABC) -> Self {
Lattice::Abc(v)
}
}
#[derive(Debug, Clone)]
pub enum Positions {
Frac(PositionsFrac),
Abs(PositionsAbs),
}
impl ToCell for Positions {
fn to_cell(&self) -> Cell<'_> {
match self {
Positions::Frac(frac) => frac.to_cell(),
Positions::Abs(abs) => abs.to_cell(),
}
}
}
#[allow(clippy::duplicated_attributes)]
#[derive(Debug, Clone, Builder)]
#[builder(on(Lattice, into), on(Positions, into), finish_fn(vis = "", name = build_internal))]
pub struct CellDocument {
pub lattice: Lattice,
pub positions: Positions,
#[builder(default)]
pub kpoints: KpointsParams,
#[builder(default)]
pub spectral: SpectralParams,
#[builder(default)]
pub optics_magres: OpticsMagresParams,
#[builder(default)]
pub symmetry: SymmetryParams,
#[builder(default)]
pub constraints: ConstraintsParams,
#[builder(default)]
pub external_fields: ExternalFieldParams,
#[builder(default)]
pub species: SpeciesParams,
#[builder(default)]
pub phonon: PhononParams,
#[builder(default)]
pub phonon_fine: PhononFineParams,
#[builder(default)]
pub dynamics: DynamicsParams,
}
impl<S: cell_document_builder::IsComplete> CellDocumentBuilder<S> {
pub fn build(self) -> CResult<CellDocument> {
let mut doc = self.build_internal();
doc.kpoints = doc.kpoints.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.spectral = doc.spectral.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.symmetry = doc.symmetry.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.constraints = doc.constraints.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.phonon = doc.phonon.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.phonon_fine = doc.phonon_fine.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.optics_magres = doc.optics_magres.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.external_fields = doc.external_fields.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.species = doc.species.validate().map_err(|e| Error::Message(e.to_string()))?;
doc.dynamics = doc.dynamics.validate().map_err(|e| Error::Message(e.to_string()))?;
Ok(doc)
}
}
impl FromCellFile for CellDocument {
fn from_cell_file(cells: &[Cell<'_>]) -> CResult<Self> {
let has_lattice_cart = find_block(cells, "LATTICE_CART").is_ok();
let has_lattice_abc = find_block(cells, "LATTICE_ABC").is_ok();
if has_lattice_cart && has_lattice_abc {
return Err(Error::Message(
"Both LATTICE_CART and LATTICE_ABC are specified. Only one lattice specification is allowed."
.into(),
));
}
let lattice = if has_lattice_cart {
Lattice::Cart(LatticeCart::from_block_rows(find_block(cells, "LATTICE_CART")?)?)
} else {
Lattice::Abc(LatticeABC::from_block_rows(find_block(cells, "LATTICE_ABC")?)?)
};
let positions = if find_block(cells, "POSITIONS_FRAC").is_ok() {
Positions::Frac(PositionsFrac::from_block_rows(find_block(cells, "POSITIONS_FRAC")?)?)
} else {
Positions::Abs(PositionsAbs::from_block_rows(find_block(cells, "POSITIONS_ABS")?)?)
};
Self::builder()
.lattice(lattice)
.positions(positions)
.kpoints(KpointsParams::from_cell_file(cells)?)
.spectral(SpectralParams::from_cell_file(cells)?)
.optics_magres(OpticsMagresParams::from_cell_file(cells)?)
.symmetry(SymmetryParams::from_cell_file(cells)?)
.constraints(ConstraintsParams::from_cell_file(cells)?)
.external_fields(ExternalFieldParams::from_cell_file(cells)?)
.species(SpeciesParams::from_cell_file(cells)?)
.phonon(PhononParams::from_cell_file(cells)?)
.phonon_fine(PhononFineParams::from_cell_file(cells)?)
.dynamics(DynamicsParams::from_cell_file(cells)?)
.build()
}
}
impl ToCellFile for CellDocument {
fn to_cell_file(&self) -> Vec<Cell<'_>> {
let mut cells = vec![self.lattice.to_cell(), self.positions.to_cell()];
cells.extend(self.kpoints.to_cell_file());
cells.extend(self.spectral.to_cell_file());
cells.extend(self.optics_magres.to_cell_file());
cells.extend(self.symmetry.to_cell_file());
cells.extend(self.constraints.to_cell_file());
cells.extend(self.external_fields.to_cell_file());
cells.extend(self.species.to_cell_file());
cells.extend(self.phonon.to_cell_file());
cells.extend(self.phonon_fine.to_cell_file());
cells.extend(self.dynamics.to_cell_file());
cells
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cell::bz_sampling_kpoints::{
BsKpointPath, BsKpointPathEntry, Kpoint, KpointsList, KpointsMpGrid, KpointsMpOffset,
KpointsMpSpacing, SpectralKpointPath, SpectralKpointPathEntry, SpectralKpointsMpGrid,
SpectralKpointsMpOffset,
};
use crate::cell::phonon::{PhononKpointList, PhononKpointListEntry, PhononKpointPath, PhononKpointPathEntry};
use crate::cell::positions::PositionFracEntry;
use crate::cell::species::Species;
use crate::cell::symmetry::{SymmetryGenerate, SymmetryOp, SymmetryOps};
#[test]
fn test_parse_mg2sio4_forsterite_cell() {
let input = std::fs::read_to_string("tests/fixtures/Mg2SiO4_Cr_1.cell").unwrap();
let doc = castep_cell_fmt::parse::<CellDocument>(&input).expect("Failed to parse Mg2SiO4_Cr_1.cell");
assert!(matches!(doc.lattice, Lattice::Cart(ref c)
if (c.a[0] - 10.183).abs() < 0.001
&& (c.b[1] - 5.970).abs() < 0.001
&& (c.c[2] - 4.751).abs() < 0.001));
assert!(doc.kpoints.kpoints_list.is_some());
assert_eq!(doc.kpoints.kpoints_list.as_ref().unwrap().kpts.len(), 3);
assert!(doc.symmetry.symmetry_ops.is_some());
assert_eq!(doc.symmetry.symmetry_ops.as_ref().unwrap().ops.len(), 2);
assert!(doc.constraints.fix_com.is_some());
assert_eq!(doc.constraints.fix_com.as_ref().unwrap().0, false);
assert!(doc.species.species_mass.is_some());
assert_eq!(doc.species.species_mass.as_ref().unwrap().masses.len(), 4);
}
#[test]
fn test_parse_fe2o3_cell() {
let input = std::fs::read_to_string("tests/fixtures/Fe2O3.cell").unwrap();
let doc = castep_cell_fmt::parse::<CellDocument>(&input).expect("Failed to parse Fe2O3.cell");
assert!(matches!(doc.lattice, Lattice::Cart(ref c)
if (c.a[0] - 4.360).abs() < 0.001
&& (c.b[1] - 5.035).abs() < 0.001
&& (c.c[2] - 13.72).abs() < 0.01));
assert!(doc.kpoints.kpoints_list.is_some());
assert_eq!(doc.kpoints.kpoints_list.as_ref().unwrap().kpts.len(), 5);
assert!(doc.constraints.fix_all_cell.is_some());
assert_eq!(doc.constraints.fix_all_cell.as_ref().unwrap().0, true);
assert!(doc.external_fields.external_pressure.is_some());
assert!(doc.species.hubbard_u.is_some());
assert_eq!(doc.species.hubbard_u.as_ref().unwrap().atom_u_values.len(), 12);
assert!(doc.species.species_mass.is_some());
assert_eq!(doc.species.species_mass.as_ref().unwrap().masses.len(), 2);
}
#[test]
fn test_parse_zno_lr_cell() {
let input = std::fs::read_to_string("tests/fixtures/ZnO_LR.cell").unwrap();
let doc = castep_cell_fmt::parse::<CellDocument>(&input).expect("Failed to parse ZnO_LR.cell");
assert!(matches!(doc.lattice, Lattice::Cart(_)));
assert!(doc.kpoints.kpoints_list.is_some());
assert_eq!(doc.kpoints.kpoints_list.as_ref().unwrap().kpts.len(), 10);
assert!(doc.symmetry.symmetry_ops.is_some());
assert_eq!(doc.symmetry.symmetry_ops.as_ref().unwrap().ops.len(), 12);
assert!(doc.constraints.cell_constraints.is_some());
let cc = doc.constraints.cell_constraints.as_ref().unwrap();
assert_eq!(cc.lengths, [1, 1, 3]);
assert_eq!(cc.angles, [0, 0, 0]);
assert!(matches!(doc.positions, Positions::Frac(_)));
if let Positions::Frac(ref pos) = doc.positions {
assert_eq!(pos.positions.len(), 4);
}
}
fn minimal_lattice() -> Lattice {
Lattice::Cart(LatticeCart {
unit: None,
a: [10.0, 0.0, 0.0],
b: [0.0, 10.0, 0.0],
c: [0.0, 0.0, 10.0],
})
}
fn minimal_positions() -> Positions {
Positions::Frac(PositionsFrac {
positions: vec![PositionFracEntry {
species: Species::Symbol("Si".to_string()),
coord: [0.0, 0.0, 0.0],
spin: None,
mixture: None,
}],
})
}
#[test]
fn build_rejects_multiple_kpoint_specs() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.kpoints(KpointsParams {
kpoints_list: Some(KpointsList::builder()
.kpts(vec![Kpoint::builder().coord([0.0, 0.0, 0.0]).weight(1.0).build()])
.build()),
kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
..Default::default()
})
.build();
assert!(result.is_err());
}
#[test]
fn build_rejects_spectral_and_bs_duplication() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.spectral(SpectralParams {
spectral_kpoint_path: Some(SpectralKpointPath::builder()
.points(vec![SpectralKpointPathEntry { coord: [0.0, 0.0, 0.0] }])
.build()),
bs_kpoint_path: Some(BsKpointPath::builder()
.points(vec![BsKpointPathEntry { coord: [0.0, 0.0, 0.0] }])
.build()),
..Default::default()
})
.build();
assert!(result.is_err());
}
#[test]
fn build_rejects_multiple_phonon_specs() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.phonon(PhononParams {
phonon_kpoint_path: Some(PhononKpointPath {
points: vec![PhononKpointPathEntry { coord: [0.0, 0.0, 0.0] }],
}),
phonon_kpoint_list: Some(PhononKpointList::builder()
.kpoints(vec![PhononKpointListEntry { coord: [0.0, 0.0, 0.0], weight: 1.0 }])
.build()),
..Default::default()
})
.build();
assert!(result.is_err());
}
#[test]
fn build_rejects_symmetry_generate_and_ops() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.symmetry(SymmetryParams {
symmetry_generate: Some(SymmetryGenerate),
symmetry_ops: Some(SymmetryOps::builder()
.ops(vec![SymmetryOp::builder()
.rotation([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
.translation([0.0, 0.0, 0.0])
.build()])
.build()),
..Default::default()
})
.build();
assert!(result.is_err());
}
#[test]
fn build_allows_mp_offset_with_grid() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.kpoints(KpointsParams {
kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
kpoints_mp_offset: Some(KpointsMpOffset([0.0, 0.0, 0.0])),
..Default::default()
})
.build();
assert!(result.is_ok());
}
#[test]
fn build_allows_spectral_mp_offset_with_grid() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.spectral(SpectralParams {
spectral_kpoints_mp_grid: Some(SpectralKpointsMpGrid([2, 2, 2])),
spectral_kpoints_mp_offset: Some(SpectralKpointsMpOffset([0.0, 0.0, 0.0])),
..Default::default()
})
.build();
assert!(result.is_ok());
}
#[test]
fn build_allows_single_spec_each_category() {
let r1 = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.kpoints(KpointsParams {
kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
..Default::default()
})
.build();
assert!(r1.is_ok());
let r2 = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.spectral(SpectralParams {
spectral_kpoint_path: Some(SpectralKpointPath::builder()
.points(vec![SpectralKpointPathEntry { coord: [0.0, 0.0, 0.0] }])
.build()),
..Default::default()
})
.build();
assert!(r2.is_ok());
let r3 = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.phonon(PhononParams {
phonon_kpoint_path: Some(PhononKpointPath {
points: vec![PhononKpointPathEntry { coord: [0.0, 0.0, 0.0] }],
}),
..Default::default()
})
.build();
assert!(r3.is_ok());
let r4 = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.symmetry(SymmetryParams {
symmetry_ops: Some(SymmetryOps::builder()
.ops(vec![SymmetryOp::builder()
.rotation([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
.translation([0.0, 0.0, 0.0])
.build()])
.build()),
..Default::default()
})
.build();
assert!(r4.is_ok());
}
#[test]
fn build_rejects_all_three_kpoint_specs() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.kpoints(KpointsParams {
kpoints_list: Some(KpointsList::builder()
.kpts(vec![Kpoint::builder().coord([0.0, 0.0, 0.0]).weight(1.0).build()])
.build()),
kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
kpoints_mp_spacing: Some(KpointsMpSpacing { value: 0.05, unit: None }),
..Default::default()
})
.build();
assert!(result.is_err());
}
#[test]
fn build_allows_empty_document() {
let result = CellDocument::builder()
.lattice(minimal_lattice())
.positions(minimal_positions())
.build();
assert!(result.is_ok());
}
}