1use crate::{CifValue, DataBlock, Document};
4use molframe_core::structure::UnitCell;
5use std::collections::BTreeMap;
6
7#[derive(Clone, Debug, PartialEq)]
9pub struct SmallCifAtom {
10 pub label: Box<str>,
12 pub type_symbol: Box<str>,
14 pub fractional: Option<[f64; 3]>,
16 pub cartesian: Option<[f64; 3]>,
18 pub occupancy: Option<f64>,
20 pub u_iso: Option<f64>,
22 pub charge: Option<Box<str>>,
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct SmallCifBond {
29 pub first: usize,
31 pub second: usize,
33 pub distance: Option<f64>,
35 pub site_symmetry: Option<Box<str>>,
37 pub bond_type: Option<Box<str>>,
39}
40
41#[derive(Clone, Debug, PartialEq)]
43pub struct SmallCifStructure {
44 pub name: Box<str>,
46 pub cell: Option<UnitCell>,
48 pub space_group_name: Option<Box<str>>,
50 pub space_group_number: Option<i32>,
52 pub symmetry_operations: Vec<Box<str>>,
54 pub atoms: Vec<SmallCifAtom>,
56 pub bonds: Vec<SmallCifBond>,
58}
59
60#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum SmallCifError {
64 #[error("small-molecule CIF has no atom-site table")]
66 MissingAtoms,
67 #[error("small-molecule CIF atom labels are absent or duplicated")]
69 AtomLabels,
70 #[error("small-molecule CIF coordinates are incomplete or malformed")]
72 Coordinates,
73 #[error("small-molecule CIF unit cell is incomplete or invalid")]
75 Cell,
76 #[error("small-molecule CIF bond references an absent atom")]
78 BondAtom,
79 #[error("small-molecule CIF atom type symbol is absent")]
81 AtomTypeSymbol,
82}
83
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86pub enum SmallCifDialect {
87 #[default]
89 Ddl2,
90 Ddl1,
92}
93
94#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96pub struct SmallCifOptions {
97 dialect: SmallCifDialect,
98}
99
100impl SmallCifOptions {
101 #[must_use]
103 pub const fn ddl2() -> Self {
104 Self {
105 dialect: SmallCifDialect::Ddl2,
106 }
107 }
108
109 #[must_use]
111 pub const fn ddl1() -> Self {
112 Self {
113 dialect: SmallCifDialect::Ddl1,
114 }
115 }
116}
117
118pub fn lower_small_cif(document: &Document) -> Result<SmallCifStructure, SmallCifError> {
127 lower_small_cif_with_options(document, SmallCifOptions::ddl2())
128}
129
130pub fn lower_small_cif_with_options(
137 document: &Document,
138 options: SmallCifOptions,
139) -> Result<SmallCifStructure, SmallCifError> {
140 let block = document.first_block().ok_or(SmallCifError::MissingAtoms)?;
141 let source = Source {
142 block,
143 dialect: options.dialect,
144 };
145 let labels = source
146 .column("atom_site", "label", "atom_site_label")
147 .ok_or(SmallCifError::MissingAtoms)?;
148 if labels.is_empty() {
149 return Err(SmallCifError::MissingAtoms);
150 }
151 let mut atoms = Vec::with_capacity(labels.len());
152 let mut label_indices = BTreeMap::new();
153 for row in 0..labels.len() {
154 let label = labels
155 .get(row)
156 .and_then(CifValue::as_identifier)
157 .ok_or(SmallCifError::AtomLabels)?
158 .into_owned();
159 if label_indices.insert(label.clone(), row).is_some() {
160 return Err(SmallCifError::AtomLabels);
161 }
162 let type_symbol = source
163 .identifier("atom_site", "type_symbol", "atom_site_type_symbol", row)
164 .ok_or(SmallCifError::AtomTypeSymbol)?
165 .into();
166 let fractional = coordinate(&source, "fract", row)?;
167 let cartesian = coordinate(&source, "Cartn", row)?;
168 if fractional.is_none() && cartesian.is_none() {
169 return Err(SmallCifError::Coordinates);
170 }
171 atoms.push(SmallCifAtom {
172 label: label.into(),
173 type_symbol,
174 fractional,
175 cartesian,
176 occupancy: source.number("atom_site", "occupancy", "atom_site_occupancy", row),
177 u_iso: source.number(
178 "atom_site",
179 "U_iso_or_equiv",
180 "atom_site_U_iso_or_equiv",
181 row,
182 ),
183 charge: source
184 .identifier("atom_site", "charge", "atom_site_charge", row)
185 .map(Into::into),
186 });
187 }
188 let cell = lower_cell(&source)?;
189 let bonds = lower_bonds(&source, &label_indices)?;
190 Ok(SmallCifStructure {
191 name: block.name().into(),
192 cell,
193 space_group_name: match options.dialect {
194 SmallCifDialect::Ddl2 => {
195 source.identifier("space_group", "name_H-M_alt", "space_group_name_H-M_alt", 0)
196 }
197 SmallCifDialect::Ddl1 => source.identifier(
198 "symmetry",
199 "space_group_name_H-M",
200 "symmetry_space_group_name_H-M",
201 0,
202 ),
203 }
204 .map(Into::into),
205 space_group_number: match options.dialect {
206 SmallCifDialect::Ddl2 => {
207 source.integer("space_group", "IT_number", "space_group_IT_number", 0)
208 }
209 SmallCifDialect::Ddl1 => source.integer(
210 "symmetry",
211 "Int_Tables_number",
212 "symmetry_Int_Tables_number",
213 0,
214 ),
215 },
216 symmetry_operations: symmetry_operations(&source),
217 atoms,
218 bonds,
219 })
220}
221
222fn coordinate(
223 source: &Source<'_>,
224 namespace: &str,
225 row: usize,
226) -> Result<Option<[f64; 3]>, SmallCifError> {
227 let values = ["x", "y", "z"].map(|axis| {
228 let item = format!("{namespace}_{axis}");
229 let tag = format!("atom_site_{namespace}_{axis}");
230 source.number("atom_site", &item, &tag, row)
231 });
232 if values.iter().all(Option::is_none) {
233 return Ok(None);
234 }
235 let [Some(x), Some(y), Some(z)] = values else {
236 return Err(SmallCifError::Coordinates);
237 };
238 if [x, y, z].iter().any(|value| !value.is_finite()) {
239 return Err(SmallCifError::Coordinates);
240 }
241 Ok(Some([x, y, z]))
242}
243
244fn lower_cell(source: &Source<'_>) -> Result<Option<UnitCell>, SmallCifError> {
245 let values = [
246 ("length_a", "cell_length_a"),
247 ("length_b", "cell_length_b"),
248 ("length_c", "cell_length_c"),
249 ("angle_alpha", "cell_angle_alpha"),
250 ("angle_beta", "cell_angle_beta"),
251 ("angle_gamma", "cell_angle_gamma"),
252 ]
253 .map(|(item, tag)| source.number("cell", item, tag, 0));
254 if values.iter().all(Option::is_none) {
255 return Ok(None);
256 }
257 let [
258 Some(a),
259 Some(b),
260 Some(c),
261 Some(alpha),
262 Some(beta),
263 Some(gamma),
264 ] = values
265 else {
266 return Err(SmallCifError::Cell);
267 };
268 if [a, b, c]
269 .iter()
270 .any(|value| !value.is_finite() || *value <= 0.0)
271 || [alpha, beta, gamma]
272 .iter()
273 .any(|value| !value.is_finite() || !(0.0..180.0).contains(value))
274 {
275 return Err(SmallCifError::Cell);
276 }
277 Ok(Some(UnitCell {
278 lengths: [a, b, c],
279 angles: [alpha, beta, gamma],
280 }))
281}
282
283fn lower_bonds(
284 source: &Source<'_>,
285 indices: &BTreeMap<String, usize>,
286) -> Result<Vec<SmallCifBond>, SmallCifError> {
287 let Some(first_column) = source.column(
288 "geom_bond",
289 "atom_site_label_1",
290 "geom_bond_atom_site_label_1",
291 ) else {
292 return Ok(Vec::new());
293 };
294 let mut bonds = Vec::with_capacity(first_column.len());
295 for row in 0..first_column.len() {
296 let first_label = first_column
297 .get(row)
298 .and_then(CifValue::as_identifier)
299 .ok_or(SmallCifError::BondAtom)?;
300 let second_label = source
301 .identifier(
302 "geom_bond",
303 "atom_site_label_2",
304 "geom_bond_atom_site_label_2",
305 row,
306 )
307 .ok_or(SmallCifError::BondAtom)?;
308 bonds.push(SmallCifBond {
309 first: *indices
310 .get(first_label.as_ref())
311 .ok_or(SmallCifError::BondAtom)?,
312 second: *indices
313 .get(second_label.as_ref())
314 .ok_or(SmallCifError::BondAtom)?,
315 distance: source.number("geom_bond", "distance", "geom_bond_distance", row),
316 site_symmetry: source
317 .identifier(
318 "geom_bond",
319 "site_symmetry_2",
320 "geom_bond_site_symmetry_2",
321 row,
322 )
323 .map(Into::into),
324 bond_type: source
325 .identifier("geom_bond", "type", "geom_bond_type", row)
326 .map(Into::into),
327 });
328 }
329 Ok(bonds)
330}
331
332fn symmetry_operations(source: &Source<'_>) -> Vec<Box<str>> {
333 let column = match source.dialect {
334 SmallCifDialect::Ddl2 => source.column(
335 "space_group_symop",
336 "operation_xyz",
337 "space_group_symop_operation_xyz",
338 ),
339 SmallCifDialect::Ddl1 => {
340 source.column("symmetry_equiv", "pos_as_xyz", "symmetry_equiv_pos_as_xyz")
341 }
342 };
343 column
344 .into_iter()
345 .flat_map(crate::Column::iter)
346 .filter_map(CifValue::as_str)
347 .map(Into::into)
348 .collect()
349}
350struct Source<'a> {
351 block: &'a DataBlock,
352 dialect: SmallCifDialect,
353}
354
355impl<'a> Source<'a> {
356 fn column(&self, category: &str, item: &str, ddl1_tag: &str) -> Option<&'a crate::Column> {
357 match self.dialect {
358 SmallCifDialect::Ddl2 => self.block.category(category)?.column(item),
359 SmallCifDialect::Ddl1 => self.block.category(ddl1_tag)?.column(ddl1_tag),
360 }
361 }
362
363 fn number(&self, category: &str, item: &str, ddl1_tag: &str, row: usize) -> Option<f64> {
364 self.column(category, item, ddl1_tag)?.get(row)?.as_float()
365 }
366
367 fn integer(&self, category: &str, item: &str, ddl1_tag: &str, row: usize) -> Option<i32> {
368 i32::try_from(
369 self.column(category, item, ddl1_tag)?
370 .get(row)?
371 .as_integer()?,
372 )
373 .ok()
374 }
375
376 fn identifier(
377 &self,
378 category: &str,
379 item: &str,
380 ddl1_tag: &str,
381 row: usize,
382 ) -> Option<std::borrow::Cow<'a, str>> {
383 self.column(category, item, ddl1_tag)?
384 .get(row)?
385 .as_identifier()
386 }
387}
388
389#[cfg(test)]
390#[path = "model_tests.rs"]
391mod tests;