1use serde::{Deserialize, Serialize};
4
5use crate::Error;
6use crate::Lattice;
7use crate::Structure;
8use crate::spatial::{FractionalGrid, perpendicular_widths};
9use crate::symop::SymOp;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct P1Atom {
14 pub element: String,
16 pub label: String,
18 pub frac: [f64; 3],
20 pub occupancy: f64,
22 pub site_idx: usize,
24 pub op_idx: usize,
26 pub tau: [i32; 3],
28 #[serde(default)]
30 pub disorder_group: Option<u32>,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct P1Cell {
36 pub lattice: Lattice,
38 pub atoms: Vec<P1Atom>,
40}
41
42impl P1Cell {
43 pub fn expand(structure: &Structure, tolerance: f64) -> Result<Self, Error> {
50 if !tolerance.is_finite() || tolerance < 0.0 {
51 return Err(Error::InvalidInput(
52 "P1 expansion tolerance must be non-negative and finite".to_owned(),
53 ));
54 }
55 let identity = [SymOp::identity()];
56 let operations = if structure.operations().is_empty() {
57 identity.as_slice()
58 } else {
59 structure.operations()
60 };
61 let widths = perpendicular_widths(structure.lattice())?;
62 let mut atoms: Vec<P1Atom> = Vec::new();
63 for (site_idx, site) in structure.sites().iter().enumerate() {
64 let mut grid = FractionalGrid::new(widths, tolerance);
67 for (op_idx, operation) in operations.iter().enumerate() {
68 let base = operation.apply(site.frac());
69 let [bx, by, bz] = base;
70 let unit = |value: f64| {
74 let floor = value.floor();
75 let frac = value - floor;
76 if frac >= 1.0 {
77 (0.0, floor + 1.0)
78 } else {
79 (frac, floor)
80 }
81 };
82 let (fx, floor_x) = unit(bx);
83 let (fy, floor_y) = unit(by);
84 let (fz, floor_z) = unit(bz);
85 let frac = [fx, fy, fz];
86 let tau = [-floor_x as i32, -floor_y as i32, -floor_z as i32];
87 let mut duplicate = false;
88 grid.for_each_candidate(site.element(), frac, |existing_index| {
89 let Some(existing) = atoms.get(existing_index) else {
90 return true;
91 };
92 if structure.lattice().distance(existing.frac, frac) <= tolerance {
93 duplicate = true;
94 return false;
95 }
96 true
97 });
98 if duplicate {
99 continue;
100 }
101 let atom_index = atoms.len();
102 atoms.push(P1Atom {
103 element: site.element().to_owned(),
104 label: site.label().to_owned(),
105 frac,
106 occupancy: site.occupancy(),
107 site_idx,
108 op_idx,
109 tau,
110 disorder_group: site.disorder_group(),
111 });
112 grid.insert(site.element(), frac, atom_index);
113 }
114 }
115 Ok(Self {
116 lattice: *structure.lattice(),
117 atoms,
118 })
119 }
120
121 #[must_use]
123 pub fn n_atoms(&self) -> usize {
124 self.atoms.len()
125 }
126
127 #[must_use]
129 pub fn elements(&self) -> Vec<String> {
130 self.atoms.iter().map(|atom| atom.element.clone()).collect()
131 }
132
133 #[must_use]
135 pub fn frac(&self) -> Vec<[f64; 3]> {
136 self.atoms.iter().map(|atom| atom.frac).collect()
137 }
138
139 #[must_use]
141 pub fn occupancy(&self) -> Vec<f64> {
142 self.atoms.iter().map(|atom| atom.occupancy).collect()
143 }
144
145 #[must_use]
147 pub fn images_of(&self, site_idx: usize) -> Vec<&P1Atom> {
148 self.atoms
149 .iter()
150 .filter(|atom| atom.site_idx == site_idx)
151 .collect()
152 }
153}
154
155impl Structure {
156 pub fn as_p1(&self) -> Result<Self, Error> {
169 if self.is_p1() {
170 return Ok(self.clone());
171 }
172 let expanded = self.p1()?;
173 let mut builder = crate::StructureBuilder::new(self.name(), *self.lattice()).hall("P 1");
174 for atom in &expanded.atoms {
175 let site = self.sites().get(atom.site_idx).ok_or_else(|| {
176 Error::InvalidInput("the P1 expansion references a missing site".to_owned())
177 })?;
178 builder = builder.site(site.clone().with_frac(atom.frac)?);
179 }
180 for (key, value) in self.metadata() {
181 builder = builder.metadata(key.clone(), value.clone());
182 }
183 builder.build()
184 }
185}
186
187impl Structure {
188 pub fn p1(&self) -> Result<P1Cell, Error> {
194 P1Cell::expand(self, 0.05)
195 }
196}