use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::Error;
use crate::Lattice;
use crate::bonding::{ExplicitBond, TopologyBond};
use crate::elements::element_by_symbol;
use crate::site::Site;
use crate::symop::SymOp;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Structure {
name: String,
lattice: Lattice,
hall: String,
sites: Vec<Site>,
#[serde(default)]
operations: Vec<SymOp>,
#[serde(default)]
explicit_bonds: Vec<ExplicitBond>,
#[serde(default)]
topology_bonds: Vec<TopologyBond>,
metadata: BTreeMap<String, Value>,
}
mod properties;
impl Structure {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn lattice(&self) -> &Lattice {
&self.lattice
}
#[must_use]
pub fn hall(&self) -> &str {
&self.hall
}
#[must_use]
pub fn sites(&self) -> &[Site] {
&self.sites
}
#[must_use]
pub fn operations(&self) -> &[SymOp] {
&self.operations
}
#[must_use]
pub fn explicit_bonds(&self) -> &[ExplicitBond] {
&self.explicit_bonds
}
#[must_use]
pub fn topology_bonds(&self) -> &[TopologyBond] {
&self.topology_bonds
}
#[must_use]
pub fn metadata(&self) -> &BTreeMap<String, Value> {
&self.metadata
}
#[must_use]
pub fn is_p1(&self) -> bool {
let hall = self.hall.trim();
(hall.is_empty() || hall == "P 1" || hall == "P1")
&& self.operations.iter().all(SymOp::is_identity)
}
#[must_use]
pub fn cart_sites(&self) -> Vec<[f64; 3]> {
self.sites
.iter()
.map(|site| self.lattice.to_cart(site.frac()))
.collect()
}
pub fn summary(&self) -> Result<StructureSummary, Error> {
Ok(StructureSummary {
name: self.name.clone(),
formula: self.formula(),
hall: self.hall.clone(),
n_sites: self.sites.len(),
volume: self.lattice.volume(),
density: self.density()?,
total_charge: self.total_charge(),
is_p1: self.is_p1(),
})
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
#[must_use]
pub fn with_lattice(mut self, lattice: Lattice) -> Self {
self.lattice = lattice;
self
}
#[must_use]
pub fn with_site(mut self, site: Site) -> Self {
self.sites.push(site);
self
}
pub fn molecule(
name: impl Into<String>,
points: &[(String, [f64; 3])],
padding: f64,
) -> Result<Self, Error> {
if points.is_empty() {
return Err(Error::InvalidInput(
"a molecule needs at least one atom".to_owned(),
));
}
if !padding.is_finite() || padding <= 0.0 {
return Err(Error::InvalidInput(
"molecule padding must be positive and finite".to_owned(),
));
}
let [mut min_x, mut min_y, mut min_z] = [f64::INFINITY; 3];
let [mut max_x, mut max_y, mut max_z] = [f64::NEG_INFINITY; 3];
for (_, point) in points {
let [x, y, z] = *point;
min_x = min_x.min(x);
min_y = min_y.min(y);
min_z = min_z.min(z);
max_x = max_x.max(x);
max_y = max_y.max(y);
max_z = max_z.max(z);
}
let [length_x, length_y, length_z] = [
max_x - min_x + 2.0 * padding,
max_y - min_y + 2.0 * padding,
max_z - min_z + 2.0 * padding,
];
let lattice = Lattice::orthorhombic(length_x, length_y, length_z)?;
let [origin_x, origin_y, origin_z] = [min_x - padding, min_y - padding, min_z - padding];
let mut builder = StructureBuilder::new(name, lattice);
for (index, (element, point)) in points.iter().enumerate() {
let [x, y, z] = *point;
let frac = [
(x - origin_x) / length_x,
(y - origin_y) / length_y,
(z - origin_z) / length_z,
];
builder = builder.site(Site::new(
format!("{}{}", element, index + 1),
element,
frac,
)?);
}
builder.build()
}
}
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StructureDocument {
pub schema_version: u32,
pub structure: Structure,
}
impl StructureDocument {
#[must_use]
pub fn new(structure: Structure) -> Self {
Self {
schema_version: SCHEMA_VERSION,
structure,
}
}
pub fn to_json(&self) -> Result<String, Error> {
serde_json::to_string(self)
.map_err(|error| Error::InvalidInput(format!("could not serialize structure: {error}")))
}
pub fn from_json(text: &str) -> Result<Self, Error> {
let document: Self = serde_json::from_str(text)
.map_err(|error| Error::InvalidInput(format!("invalid structure JSON: {error}")))?;
if document.schema_version != SCHEMA_VERSION {
return Err(Error::InvalidInput(format!(
"unsupported structure schema_version {}, expected {SCHEMA_VERSION}",
document.schema_version
)));
}
Ok(document)
}
}
impl Structure {
#[must_use]
pub fn to_document(&self) -> StructureDocument {
StructureDocument::new(self.clone())
}
}
pub fn structure_info(text: &str) -> Result<StructureSummary, Error> {
let structure: Structure = if text.contains("\"schema_version\"") {
StructureDocument::from_json(text)?.structure
} else {
serde_json::from_str(text)
.map_err(|error| Error::InvalidInput(format!("invalid structure JSON: {error}")))?
};
structure.summary()
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct StructureSummary {
pub name: String,
pub formula: String,
pub hall: String,
pub n_sites: usize,
pub volume: f64,
pub density: f64,
pub total_charge: f64,
pub is_p1: bool,
}
#[derive(Debug, Clone)]
pub struct StructureBuilder {
name: String,
lattice: Lattice,
hall: String,
sites: Vec<Site>,
operations: Vec<SymOp>,
explicit_bonds: Vec<ExplicitBond>,
topology_bonds: Vec<TopologyBond>,
metadata: BTreeMap<String, Value>,
}
impl StructureBuilder {
#[must_use]
pub fn new(name: impl Into<String>, lattice: Lattice) -> Self {
Self {
name: name.into(),
lattice,
hall: "P 1".to_owned(),
sites: Vec::new(),
operations: Vec::new(),
explicit_bonds: Vec::new(),
topology_bonds: Vec::new(),
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn hall(mut self, hall: impl Into<String>) -> Self {
self.hall = hall.into();
self
}
#[must_use]
pub fn site(mut self, site: Site) -> Self {
self.sites.push(site);
self
}
#[must_use]
pub fn operation(mut self, operation: SymOp) -> Self {
self.operations.push(operation);
self
}
#[must_use]
pub fn explicit_bond(mut self, bond: ExplicitBond) -> Self {
self.explicit_bonds.push(bond);
self
}
#[must_use]
pub fn topology_bond(mut self, bond: TopologyBond) -> Self {
self.topology_bonds.push(bond);
self
}
#[must_use]
pub fn metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn build(self) -> Result<Structure, Error> {
if self.hall.trim().is_empty() {
return Err(Error::InvalidInput(
"space-group Hall symbol must not be empty; use P 1".to_owned(),
));
}
Ok(Structure {
name: self.name,
lattice: self.lattice,
hall: self.hall,
sites: self.sites,
operations: self.operations,
explicit_bonds: self.explicit_bonds,
topology_bonds: self.topology_bonds,
metadata: self.metadata,
})
}
}