1use super::options::{CifWriteError, CifWriteOptions, valid_block_id};
8use molframe_core::element::Element;
9use molframe_core::index::ModelIndex;
10use molframe_core::structure::{
11 AtomRef, ChainRef, ResidueRef, SEQUENCE_REFERENCES_EXTENSION, SequenceReferences, Structure,
12};
13
14#[derive(Clone, Copy, Debug, PartialEq)]
16pub enum CanonicalValue<T> {
17 Present(T),
19 Inapplicable,
21 Unknown,
23}
24
25#[derive(Clone, Copy, Debug)]
27pub struct CanonicalProjection<'a> {
28 structure: &'a Structure,
29 options: &'a CifWriteOptions,
30 block_id: &'a str,
31}
32
33pub fn canonical_projection<'a>(
40 structure: &'a Structure,
41 options: &'a CifWriteOptions,
42) -> Result<CanonicalProjection<'a>, CifWriteError> {
43 let Some(block_id) = options.block_id().or(structure.data().entry.id.as_deref()) else {
44 return Err(CifWriteError::MissingBlockId);
45 };
46 if !valid_block_id(block_id) {
47 return Err(CifWriteError::InvalidBlockId(block_id.to_owned()));
48 }
49 let projection = CanonicalProjection {
50 structure,
51 options,
52 block_id,
53 };
54 projection.atom_site_row_count()?;
55 projection.visit_atom_rows(|_| {})?;
56 super::bonds::preflight(structure, options)?;
57 super::secondary::preflight(structure)?;
58 Ok(projection)
59}
60
61impl<'a> CanonicalProjection<'a> {
62 #[must_use]
64 pub const fn block_id(self) -> &'a str {
65 self.block_id
66 }
67
68 #[must_use]
70 pub const fn structure(self) -> &'a Structure {
71 self.structure
72 }
73
74 #[must_use]
76 pub const fn options(self) -> &'a CifWriteOptions {
77 self.options
78 }
79
80 #[must_use]
82 pub fn sequence_references(self) -> Option<&'a SequenceReferences> {
83 self.structure
84 .extensions()
85 .get::<SequenceReferences>(SEQUENCE_REFERENCES_EXTENSION)
86 }
87
88 pub fn atom_site_row_count(self) -> Result<usize, CifWriteError> {
95 if let Some(models) = self.structure.ragged_models() {
96 return models.iter().try_fold(0usize, |total, model| {
97 let rows = usize::try_from(model.atom_count())
98 .map_err(|_| CifWriteError::AtomRowCountOverflow)?;
99 total
100 .checked_add(rows)
101 .ok_or(CifWriteError::AtomRowCountOverflow)
102 });
103 }
104 let atoms = usize::try_from(self.structure.atom_count())
105 .map_err(|_| CifWriteError::AtomRowCountOverflow)?;
106 atoms
107 .checked_mul(self.structure.model_count())
108 .ok_or(CifWriteError::AtomRowCountOverflow)
109 }
110
111 #[must_use]
113 pub fn carries_formal_charge(self) -> bool {
114 let mut found = false;
115 let _ = self.visit_atom_rows(|row| {
118 found |= matches!(row.formal_charge(), CanonicalValue::Present(_));
119 });
120 found
121 }
122
123 pub fn visit_atom_rows(
134 self,
135 mut visit: impl for<'row> FnMut(CanonicalAtomRow<'row>),
136 ) -> Result<(), CifWriteError> {
137 for position in 0..self.structure.model_count() {
138 let model = model_index(position)?;
139 let Some((snapshot, local_model)) = self.structure.model_snapshot(model) else {
140 return Err(CifWriteError::MissingModelNumber { model: model.get() });
141 };
142 let Some(model_number) = snapshot
143 .model(local_model)
144 .and_then(molframe_core::structure::ModelRef::number)
145 else {
146 return Err(CifWriteError::MissingModelNumber { model: model.get() });
147 };
148 visit_model(&snapshot, local_model, model_number, &mut visit)?;
149 }
150 Ok(())
151 }
152}
153
154fn visit_model(
155 structure: &Structure,
156 model: ModelIndex,
157 model_number: i32,
158 visit: &mut impl for<'row> FnMut(CanonicalAtomRow<'row>),
159) -> Result<(), CifWriteError> {
160 for chain in structure.data().chains() {
161 let Some(chain_label) = chain.label().filter(|text| !text.is_empty()) else {
162 let atom = chain
163 .residues()
164 .next()
165 .and_then(|residue| residue.atoms().next());
166 let position = atom.map_or(0, |value| value.index().get());
167 return Err(CifWriteError::MissingAtomField {
168 atom: position,
169 field: "label_asym_id",
170 });
171 };
172 let positions = structure.model_positions(model);
173 for residue in chain.residues() {
174 for atom in residue.atoms() {
175 let row = atom_row(
176 structure,
177 atom,
178 residue,
179 chain,
180 chain_label,
181 positions,
182 model_number,
183 )?;
184 visit(row);
185 }
186 }
187 }
188 Ok(())
189}
190
191fn atom_row<'a>(
192 structure: &'a Structure,
193 atom: AtomRef<'a>,
194 residue: ResidueRef<'a>,
195 chain: ChainRef<'a>,
196 chain_label: &'a str,
197 positions: Option<&'a [[f32; 3]]>,
198 model_number: i32,
199) -> Result<CanonicalAtomRow<'a>, CifWriteError> {
200 let atom_index = atom.index().get();
201 let Some(atom_site_id) = atom.atom_site_id().filter(|value| *value != 0) else {
202 return Err(CifWriteError::MissingAtomSiteId { atom: atom_index });
203 };
204 let Some(atom_name) = atom.name().filter(|text| !text.is_empty()) else {
205 return Err(CifWriteError::MissingAtomField {
206 atom: atom_index,
207 field: "label_atom_id",
208 });
209 };
210 let Some(component_name) = atom.component_name().filter(|text| !text.is_empty()) else {
211 return Err(CifWriteError::MissingAtomField {
212 atom: atom_index,
213 field: "label_comp_id",
214 });
215 };
216 let position = positions.and_then(|values| values.get(atom.index().as_usize()).copied());
217 Ok(CanonicalAtomRow {
218 structure,
219 atom,
220 residue,
221 chain,
222 atom_site_id,
223 atom_name,
224 component_name,
225 chain_label,
226 position,
227 model_number,
228 })
229}
230
231fn model_index(position: usize) -> Result<ModelIndex, CifWriteError> {
232 u32::try_from(position)
233 .map(ModelIndex::new)
234 .map_err(|_| CifWriteError::ModelIndexOverflow { model: position })
235}
236
237#[derive(Clone, Copy, Debug)]
239pub struct CanonicalAtomRow<'a> {
240 structure: &'a Structure,
241 atom: AtomRef<'a>,
242 residue: ResidueRef<'a>,
243 chain: ChainRef<'a>,
244 atom_site_id: u32,
245 atom_name: &'a str,
246 component_name: &'a str,
247 chain_label: &'a str,
248 position: Option<[f32; 3]>,
249 model_number: i32,
250}
251
252impl<'a> CanonicalAtomRow<'a> {
253 #[must_use]
255 pub fn group(self) -> &'static str {
256 if self.residue.is_het() {
257 "HETATM"
258 } else {
259 "ATOM"
260 }
261 }
262
263 #[must_use]
265 pub const fn atom_site_id(self) -> i64 {
266 self.atom_site_id as i64
267 }
268
269 #[must_use]
271 pub fn element(self) -> CanonicalValue<Element> {
272 match self.atom.element() {
273 Some(value) => CanonicalValue::Present(value),
274 None => CanonicalValue::Unknown,
275 }
276 }
277
278 #[must_use]
280 pub const fn label_atom_id(self) -> &'a str {
281 self.atom_name
282 }
283
284 #[must_use]
286 pub fn label_alt_id(self) -> CanonicalValue<&'a str> {
287 inapplicable_text(self.atom.alt_label())
288 }
289
290 #[must_use]
292 pub const fn label_comp_id(self) -> &'a str {
293 self.component_name
294 }
295
296 #[must_use]
298 pub const fn label_asym_id(self) -> &'a str {
299 self.chain_label
300 }
301
302 #[must_use]
304 pub fn label_entity_id(self) -> CanonicalValue<&'a str> {
305 let value = self
306 .chain
307 .entity()
308 .and_then(|entity| self.structure.data().topology.entities.id(entity))
309 .and_then(|symbol| self.structure.resolve(symbol));
310 inapplicable_text(value)
311 }
312
313 #[must_use]
315 pub fn label_seq_id(self) -> CanonicalValue<i64> {
316 inapplicable_integer(self.residue.label_seq_id())
317 }
318
319 #[must_use]
321 pub fn insertion_code(self) -> CanonicalValue<&'a str> {
322 unknown_text(self.residue.ins_code())
323 }
324
325 #[must_use]
327 pub fn coordinate(self, axis: usize) -> CanonicalValue<f64> {
328 match self.position.and_then(|point| point.get(axis).copied()) {
329 Some(value) => CanonicalValue::Present(f64::from(value)),
330 None => CanonicalValue::Unknown,
331 }
332 }
333
334 #[must_use]
336 pub fn occupancy(self) -> CanonicalValue<f64> {
337 optional_measurement(self.atom.occupancy())
338 }
339
340 #[must_use]
342 pub fn b_factor(self) -> CanonicalValue<f64> {
343 optional_measurement(self.atom.b_factor())
344 }
345
346 #[must_use]
348 pub fn formal_charge(self) -> CanonicalValue<i64> {
349 match self.atom.formal_charge() {
350 Some(charge) => CanonicalValue::Present(i64::from(charge)),
351 None => CanonicalValue::Unknown,
352 }
353 }
354
355 #[must_use]
357 pub fn auth_seq_id(self) -> CanonicalValue<i64> {
358 inapplicable_integer(self.residue.auth_seq_id())
359 }
360
361 #[must_use]
363 pub fn auth_comp_id(self) -> CanonicalValue<&'a str> {
364 unknown_text(self.residue.auth_name())
365 }
366
367 #[must_use]
369 pub fn auth_asym_id(self) -> CanonicalValue<&'a str> {
370 match self.chain.auth_label() {
371 Some(label) => CanonicalValue::Present(label),
372 None => CanonicalValue::Unknown,
373 }
374 }
375
376 #[must_use]
378 pub fn auth_atom_id(self) -> CanonicalValue<&'a str> {
379 unknown_text(self.atom.auth_name())
380 }
381
382 #[must_use]
384 pub const fn model_number(self) -> i64 {
385 self.model_number as i64
386 }
387}
388
389fn inapplicable_text(value: Option<&str>) -> CanonicalValue<&str> {
390 match value.filter(|text| !text.is_empty()) {
391 Some(text) => CanonicalValue::Present(text),
392 None => CanonicalValue::Inapplicable,
393 }
394}
395
396fn unknown_text(value: Option<&str>) -> CanonicalValue<&str> {
397 match value.filter(|text| !text.is_empty()) {
398 Some(text) => CanonicalValue::Present(text),
399 None => CanonicalValue::Unknown,
400 }
401}
402
403fn inapplicable_integer(value: Option<i32>) -> CanonicalValue<i64> {
404 match value {
405 Some(number) => CanonicalValue::Present(i64::from(number)),
406 None => CanonicalValue::Inapplicable,
407 }
408}
409
410fn optional_measurement(value: Option<f32>) -> CanonicalValue<f64> {
411 match value {
412 Some(number) => CanonicalValue::Present(f64::from(number)),
413 None => CanonicalValue::Unknown,
414 }
415}