1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::Error;
9use crate::Lattice;
10use crate::bonding::{ExplicitBond, TopologyBond};
11use crate::elements::element_by_symbol;
12use crate::site::Site;
13use crate::symop::SymOp;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct Structure {
18 name: String,
19 lattice: Lattice,
20 hall: String,
21 sites: Vec<Site>,
22 #[serde(default)]
23 operations: Vec<SymOp>,
24 #[serde(default)]
25 explicit_bonds: Vec<ExplicitBond>,
26 #[serde(default)]
27 topology_bonds: Vec<TopologyBond>,
28 metadata: BTreeMap<String, Value>,
29}
30
31mod properties;
32
33impl Structure {
34 #[must_use]
36 pub fn name(&self) -> &str {
37 &self.name
38 }
39
40 #[must_use]
42 pub fn lattice(&self) -> &Lattice {
43 &self.lattice
44 }
45
46 #[must_use]
48 pub fn hall(&self) -> &str {
49 &self.hall
50 }
51
52 #[must_use]
54 pub fn sites(&self) -> &[Site] {
55 &self.sites
56 }
57
58 #[must_use]
60 pub fn operations(&self) -> &[SymOp] {
61 &self.operations
62 }
63
64 #[must_use]
66 pub fn explicit_bonds(&self) -> &[ExplicitBond] {
67 &self.explicit_bonds
68 }
69
70 #[must_use]
72 pub fn topology_bonds(&self) -> &[TopologyBond] {
73 &self.topology_bonds
74 }
75
76 #[must_use]
78 pub fn metadata(&self) -> &BTreeMap<String, Value> {
79 &self.metadata
80 }
81
82 #[must_use]
87 pub fn is_p1(&self) -> bool {
88 let hall = self.hall.trim();
89 (hall.is_empty() || hall == "P 1" || hall == "P1")
90 && self.operations.iter().all(SymOp::is_identity)
91 }
92
93 #[must_use]
95 pub fn cart_sites(&self) -> Vec<[f64; 3]> {
96 self.sites
97 .iter()
98 .map(|site| self.lattice.to_cart(site.frac()))
99 .collect()
100 }
101
102 #[must_use]
104 pub fn warnings(&self) -> Vec<StructureWarning> {
105 let mut warnings = Vec::new();
106 for (key, value) in &self.metadata {
107 let Some(message) = value.as_str() else {
108 continue;
109 };
110 let code = key
111 .strip_suffix("_warning")
112 .unwrap_or(key.as_str())
113 .to_owned();
114 let hint = if code.contains("symmetry") {
115 Some("use --spacegroup SYMBOL to override the setting".to_owned())
116 } else if code.contains("coincident") {
117 Some("remove one of the overlapping atoms or lower --symprec".to_owned())
118 } else {
119 None
120 };
121 warnings.push(StructureWarning {
122 code,
123 message: message.to_owned(),
124 atoms: Vec::new(),
125 fields: vec![key.clone()],
126 hint,
127 });
128 }
129 warnings
130 }
131
132 pub fn summary(&self) -> Result<StructureSummary, Error> {
138 Ok(StructureSummary {
139 name: self.name.clone(),
140 formula: self.formula(),
141 hall: self.hall.clone(),
142 n_sites: self.sites.len(),
143 volume: self.lattice.volume(),
144 density: self.density()?,
145 total_charge: self.total_charge(),
146 is_p1: self.is_p1(),
147 warnings: self.warnings(),
148 })
149 }
150
151 #[must_use]
153 pub fn with_name(mut self, name: impl Into<String>) -> Self {
154 self.name = name.into();
155 self
156 }
157
158 #[must_use]
160 pub fn with_lattice(mut self, lattice: Lattice) -> Self {
161 self.lattice = lattice;
162 self
163 }
164
165 #[must_use]
167 pub fn with_site(mut self, site: Site) -> Self {
168 self.sites.push(site);
169 self
170 }
171
172 pub fn molecule(
179 name: impl Into<String>,
180 points: &[(String, [f64; 3])],
181 padding: f64,
182 ) -> Result<Self, Error> {
183 if points.is_empty() {
184 return Err(Error::InvalidInput(
185 "a molecule needs at least one atom".to_owned(),
186 ));
187 }
188 if !padding.is_finite() || padding <= 0.0 {
189 return Err(Error::InvalidInput(
190 "molecule padding must be positive and finite".to_owned(),
191 ));
192 }
193 let [mut min_x, mut min_y, mut min_z] = [f64::INFINITY; 3];
194 let [mut max_x, mut max_y, mut max_z] = [f64::NEG_INFINITY; 3];
195 for (_, point) in points {
196 let [x, y, z] = *point;
197 min_x = min_x.min(x);
198 min_y = min_y.min(y);
199 min_z = min_z.min(z);
200 max_x = max_x.max(x);
201 max_y = max_y.max(y);
202 max_z = max_z.max(z);
203 }
204 let [length_x, length_y, length_z] = [
205 max_x - min_x + 2.0 * padding,
206 max_y - min_y + 2.0 * padding,
207 max_z - min_z + 2.0 * padding,
208 ];
209 let lattice = Lattice::orthorhombic(length_x, length_y, length_z)?;
210 let [origin_x, origin_y, origin_z] = [min_x - padding, min_y - padding, min_z - padding];
211 let mut builder = StructureBuilder::new(name, lattice);
212 for (index, (element, point)) in points.iter().enumerate() {
213 let [x, y, z] = *point;
214 let frac = [
215 (x - origin_x) / length_x,
216 (y - origin_y) / length_y,
217 (z - origin_z) / length_z,
218 ];
219 builder = builder.site(Site::new(
220 format!("{}{}", element, index + 1),
221 element,
222 frac,
223 )?);
224 }
225 builder.build()
226 }
227}
228
229pub const SCHEMA_VERSION: u32 = 1;
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct StructureDocument {
236 pub schema_version: u32,
238 pub structure: Structure,
240}
241
242impl StructureDocument {
243 #[must_use]
245 pub fn new(structure: Structure) -> Self {
246 Self {
247 schema_version: SCHEMA_VERSION,
248 structure,
249 }
250 }
251
252 pub fn to_json(&self) -> Result<String, Error> {
258 serde_json::to_string(self)
259 .map_err(|error| Error::InvalidInput(format!("could not serialize structure: {error}")))
260 }
261
262 pub fn from_json(text: &str) -> Result<Self, Error> {
269 let document: Self = serde_json::from_str(text)
270 .map_err(|error| Error::InvalidInput(format!("invalid structure JSON: {error}")))?;
271 if document.schema_version != SCHEMA_VERSION {
272 return Err(Error::InvalidInput(format!(
273 "unsupported structure schema_version {}, expected {SCHEMA_VERSION}",
274 document.schema_version
275 )));
276 }
277 Ok(document)
278 }
279}
280
281impl Structure {
282 #[must_use]
284 pub fn to_document(&self) -> StructureDocument {
285 StructureDocument::new(self.clone())
286 }
287}
288
289pub fn structure_info(text: &str) -> Result<StructureSummary, Error> {
300 let structure: Structure = if text.contains("\"schema_version\"") {
301 StructureDocument::from_json(text)?.structure
302 } else {
303 serde_json::from_str(text)
304 .map_err(|error| Error::InvalidInput(format!("invalid structure JSON: {error}")))?
305 };
306 structure.summary()
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct StructureWarning {
312 pub code: String,
314 pub message: String,
316 pub atoms: Vec<usize>,
318 pub fields: Vec<String>,
320 pub hint: Option<String>,
322}
323
324#[derive(Debug, Clone, PartialEq, Serialize)]
326pub struct StructureSummary {
327 pub name: String,
329 pub formula: String,
331 pub hall: String,
333 pub n_sites: usize,
335 pub volume: f64,
337 pub density: f64,
339 pub total_charge: f64,
341 pub is_p1: bool,
343 pub warnings: Vec<StructureWarning>,
345}
346
347#[derive(Debug, Clone)]
349pub struct StructureBuilder {
350 name: String,
351 lattice: Lattice,
352 hall: String,
353 sites: Vec<Site>,
354 operations: Vec<SymOp>,
355 explicit_bonds: Vec<ExplicitBond>,
356 topology_bonds: Vec<TopologyBond>,
357 metadata: BTreeMap<String, Value>,
358}
359
360impl StructureBuilder {
361 #[must_use]
363 pub fn new(name: impl Into<String>, lattice: Lattice) -> Self {
364 Self {
365 name: name.into(),
366 lattice,
367 hall: "P 1".to_owned(),
368 sites: Vec::new(),
369 operations: Vec::new(),
370 explicit_bonds: Vec::new(),
371 topology_bonds: Vec::new(),
372 metadata: BTreeMap::new(),
373 }
374 }
375
376 #[must_use]
378 pub fn hall(mut self, hall: impl Into<String>) -> Self {
379 self.hall = hall.into();
380 self
381 }
382
383 #[must_use]
385 pub fn site(mut self, site: Site) -> Self {
386 self.sites.push(site);
387 self
388 }
389
390 #[must_use]
392 pub fn operation(mut self, operation: SymOp) -> Self {
393 self.operations.push(operation);
394 self
395 }
396
397 #[must_use]
399 pub fn explicit_bond(mut self, bond: ExplicitBond) -> Self {
400 self.explicit_bonds.push(bond);
401 self
402 }
403
404 #[must_use]
406 pub fn topology_bond(mut self, bond: TopologyBond) -> Self {
407 self.topology_bonds.push(bond);
408 self
409 }
410
411 #[must_use]
413 pub fn metadata(mut self, key: impl Into<String>, value: Value) -> Self {
414 self.metadata.insert(key.into(), value);
415 self
416 }
417
418 pub fn build(self) -> Result<Structure, Error> {
425 if self.hall.trim().is_empty() {
426 return Err(Error::InvalidInput(
427 "space-group Hall symbol must not be empty; use P 1".to_owned(),
428 ));
429 }
430 Ok(Structure {
431 name: self.name,
432 lattice: self.lattice,
433 hall: self.hall,
434 sites: self.sites,
435 operations: self.operations,
436 explicit_bonds: self.explicit_bonds,
437 topology_bonds: self.topology_bonds,
438 metadata: self.metadata,
439 })
440 }
441}