Skip to main content

molframe_core/io/format/
options.rs

1//! Shared parsing options and reader/writer traits.
2
3use super::super::input::{InputBuffer, Limits};
4use super::detect::Format;
5use crate::diagnostic::Diagnostic;
6use crate::diagnostic::{Diagnostics, Strictness};
7use crate::structure::Structure;
8
9/// How much irregularity a read tolerates, and what it does about it.
10#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
11#[non_exhaustive]
12pub enum ParseMode {
13    /// Anything violating the specification fails the read.
14    Strict,
15    /// Produce a structure and report what was wrong with it.
16    #[default]
17    Permissive,
18    /// Continue past local errors, marking the regions they affected.
19    Recover,
20}
21
22impl ParseMode {
23    /// The strictness this mode implies.
24    #[must_use]
25    pub const fn strictness(self) -> Strictness {
26        match self {
27            Self::Strict => Strictness::Strict,
28            Self::Permissive => Strictness::Medium,
29            Self::Recover => Strictness::Loose,
30        }
31    }
32}
33
34/// Whether a read infers bonds, components and secondary structure afterwards.
35#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
36pub enum Perception {
37    /// Run the perception pass: inferred bonds, standard components, DSSP.
38    #[default]
39    Infer,
40    /// Return the structure as the file states it.
41    Skip,
42}
43
44/// Whether a read records a digest of the bytes it parsed.
45#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
46pub enum InputDigest {
47    /// Record nothing: hashing is work on every read, and the read path is measured on
48    /// throughput.
49    #[default]
50    Skip,
51    /// Record the SHA-256 of the bytes parsed, after decompression.
52    Sha256,
53}
54
55/// Policy for atom rows whose format-specific element field is absent or invalid.
56#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
57pub enum MissingElementPolicy {
58    /// Preserve the missing value as [`Element::UNKNOWN`](crate::Element::UNKNOWN).
59    #[default]
60    PreserveUnknown,
61    /// Explicitly apply the naming convention of the selected input format.
62    InferFromAtomName,
63}
64
65/// Policy for atoms whose recorded occupancy lies outside zero to one.
66///
67/// Every policy reports the condition once per read as `E3009` (or `W3014`
68/// when clamped), with the atom count, the first atom and the first value. A
69/// `Strict` parse mode rejects the default policy's finding.
70#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
71pub enum OccupancyPolicy {
72    /// Keep the deposited values and report the condition.
73    #[default]
74    Keep,
75    /// Clamp recorded occupancies into zero to one and report the change.
76    Clamp,
77    /// Fail the read, in any parse mode.
78    Reject,
79}
80
81/// Policy for adjacent mmCIF atom groups whose residue identifiers are identical.
82#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
83pub enum AmbiguousResidueBoundaryPolicy {
84    /// Refuse to invent a residue boundary that the deposited identifiers do not define.
85    #[default]
86    Reject,
87    /// Explicitly split when an atom name repeats in the same alternate location.
88    InferFromFileOrder,
89}
90
91/// mmCIF/BinaryCIF categories every structure read needs, whatever the filter.
92pub const REQUIRED_CIF_CATEGORIES: [&str; 5] = [
93    "atom_site",
94    "entity",
95    "entity_poly",
96    "entity_poly_seq",
97    "struct_asym",
98];
99
100/// PDB records every structure read needs, whatever the filter.
101pub const REQUIRED_PDB_RECORDS: [&str; 6] = ["ATOM", "HETATM", "MODEL", "ENDMDL", "TER", "END"];
102
103/// Categories a retained `struct_conn` needs to be interpreted rather than guessed.
104///
105/// A connection may name a partner on a neighbouring crystal image. Without the
106/// operator table there is nothing to check the named operator against, so the
107/// reader would have to assume the local identity — the guess this list exists to
108/// make unnecessary.
109pub const SYMMETRY_DEPENDENCIES: [&str; 2] = ["space_group_symop", "symmetry_equiv"];
110
111/// Which optional categories (mmCIF/BinaryCIF) or records (PDB) a read keeps.
112///
113/// Names are compared ASCII case-insensitively. Required names (see
114/// [`REQUIRED_CIF_CATEGORIES`], [`REQUIRED_PDB_RECORDS`]) are kept by
115/// [`CategoryFilter::keeps_category`] and [`CategoryFilter::keeps_record`]
116/// regardless of the filter.
117#[derive(Clone, Debug, Default, PartialEq, Eq)]
118pub enum CategoryFilter {
119    /// Keep everything.
120    #[default]
121    All,
122    /// Keep only the named names, plus the required ones.
123    Only(Vec<Box<str>>),
124    /// Keep everything except the named names, unless required.
125    Except(Vec<Box<str>>),
126}
127
128impl CategoryFilter {
129    /// Keeps only `names` (and the required names).
130    #[must_use]
131    pub fn only<I, S>(names: I) -> Self
132    where
133        I: IntoIterator<Item = S>,
134        S: Into<Box<str>>,
135    {
136        Self::Only(names.into_iter().map(Into::into).collect())
137    }
138
139    /// Drops `names`, except those that are required.
140    #[must_use]
141    pub fn except<I, S>(names: I) -> Self
142    where
143        I: IntoIterator<Item = S>,
144        S: Into<Box<str>>,
145    {
146        Self::Except(names.into_iter().map(Into::into).collect())
147    }
148
149    /// Whether the filter itself admits `name`, ignoring what is required.
150    #[must_use]
151    pub fn admits(&self, name: &str) -> bool {
152        let listed = |names: &[Box<str>]| names.iter().any(|item| item.eq_ignore_ascii_case(name));
153        match self {
154            Self::All => true,
155            Self::Only(names) => listed(names),
156            Self::Except(names) => !listed(names),
157        }
158    }
159
160    /// Whether a CIF category must be read: required, admitted, or a dependency
161    /// of one that is.
162    #[must_use]
163    pub fn keeps_category(&self, name: &str) -> bool {
164        self.admits(name)
165            || REQUIRED_CIF_CATEGORIES
166                .iter()
167                .any(|required| required.eq_ignore_ascii_case(name))
168            || self.dependency_admitted(name)
169    }
170
171    /// Whether a retained category depends on this one.
172    ///
173    /// A retained `struct_conn` names its partners through symmetry operators, and
174    /// an operator table the filter dropped cannot be shown to be the identity.
175    /// Keeping the connections therefore keeps the tables with them: otherwise a
176    /// filter would silently turn an explicit nonidentity operator into an implicit
177    /// local bond.
178    fn dependency_admitted(&self, name: &str) -> bool {
179        self.admits("struct_conn")
180            && SYMMETRY_DEPENDENCIES
181                .iter()
182                .any(|dependency| dependency.eq_ignore_ascii_case(name))
183    }
184
185    /// Whether a PDB record must be read: required, or admitted.
186    #[must_use]
187    pub fn keeps_record(&self, name: &str) -> bool {
188        self.admits(name)
189            || REQUIRED_PDB_RECORDS
190                .iter()
191                .any(|required| required.eq_ignore_ascii_case(name))
192    }
193}
194
195/// What a read should and should not bother doing.
196#[derive(Clone, Debug, Default)]
197pub struct ReadOptions {
198    /// Which format, or automatic detection.
199    pub format: Format,
200    /// How much irregularity to tolerate.
201    pub mode: ParseMode,
202    /// Read only the first model.
203    pub only_first_model: bool,
204    /// Read coordinates and skip everything else.
205    pub only_atomic_coords: bool,
206    /// Skip the perception pass that follows a read: inferred bonds, standard
207    /// component annotation and secondary-structure assignment. The parse and
208    /// everything the file itself records are unaffected; only what a reader
209    /// would otherwise compute from the coordinates is left out.
210    pub perception: Perception,
211    /// Drop hydrogens while reading.
212    pub discard_hydrogens: bool,
213    /// What to do when a row does not declare a valid element.
214    pub missing_element_policy: MissingElementPolicy,
215    /// What to do when a recorded occupancy lies outside zero to one.
216    pub occupancy_policy: OccupancyPolicy,
217    /// What to do when mmCIF residue identifiers do not define a boundary.
218    pub ambiguous_residue_boundary_policy: AmbiguousResidueBoundaryPolicy,
219    /// Ceilings that apply while reading.
220    pub limits: Limits,
221    /// Which optional categories or records to read.
222    pub categories: CategoryFilter,
223    /// Whether to record the SHA-256 of the bytes parsed in the structure's entry
224    /// metadata; only a result certified as computed from exactly these bytes needs it.
225    pub input_digest: InputDigest,
226}
227
228impl ReadOptions {
229    /// Options that read everything, tolerating what the archive contains.
230    #[must_use]
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Sets the format.
236    #[must_use]
237    pub const fn format(mut self, format: Format) -> Self {
238        self.format = format;
239        self
240    }
241
242    /// Sets how much irregularity to tolerate.
243    #[must_use]
244    pub const fn mode(mut self, mode: ParseMode) -> Self {
245        self.mode = mode;
246        self
247    }
248
249    /// Reads only the first model.
250    #[must_use]
251    pub const fn only_first_model(mut self, only: bool) -> Self {
252        self.only_first_model = only;
253        self
254    }
255
256    /// Reads coordinates and skips everything else.
257    #[must_use]
258    pub const fn only_atomic_coords(mut self, only: bool) -> Self {
259        self.only_atomic_coords = only;
260        self
261    }
262
263    /// Chooses whether the perception pass runs after the parse (it does by default).
264    ///
265    /// Passing `false` returns the structure as the file states it: file bonds and
266    /// deposited secondary structure are kept, nothing is inferred from geometry.
267    #[must_use]
268    pub const fn perceive(mut self, perceive: bool) -> Self {
269        self.perception = if perceive {
270            Perception::Infer
271        } else {
272            Perception::Skip
273        };
274        self
275    }
276
277    /// Selects an explicit policy for missing or invalid element fields.
278    #[must_use]
279    pub const fn missing_element_policy(mut self, policy: MissingElementPolicy) -> Self {
280        self.missing_element_policy = policy;
281        self
282    }
283
284    /// Selects the policy for occupancies outside zero to one.
285    #[must_use]
286    pub const fn occupancy_policy(mut self, policy: OccupancyPolicy) -> Self {
287        self.occupancy_policy = policy;
288        self
289    }
290
291    /// Selects an explicit policy for ambiguous mmCIF residue boundaries.
292    #[must_use]
293    pub const fn ambiguous_residue_boundary_policy(
294        mut self,
295        policy: AmbiguousResidueBoundaryPolicy,
296    ) -> Self {
297        self.ambiguous_residue_boundary_policy = policy;
298        self
299    }
300
301    /// Drops hydrogens while reading.
302    #[must_use]
303    pub const fn discard_hydrogens(mut self, discard: bool) -> Self {
304        self.discard_hydrogens = discard;
305        self
306    }
307
308    /// Restricts which optional categories (mmCIF/BinaryCIF) or records (PDB)
309    /// are read. Names the structure cannot be built without are always read.
310    #[must_use]
311    pub fn categories(mut self, categories: CategoryFilter) -> Self {
312        self.categories = categories;
313        self
314    }
315
316    /// Records the SHA-256 of the bytes parsed, so a result can be tied to them.
317    #[must_use]
318    pub const fn digest_input(mut self, digest: bool) -> Self {
319        self.input_digest = if digest {
320            InputDigest::Sha256
321        } else {
322            InputDigest::Skip
323        };
324        self
325    }
326
327    /// Sets the ceilings that apply while reading.
328    #[must_use]
329    pub const fn limits(mut self, limits: Limits) -> Self {
330        self.limits = limits;
331        self
332    }
333
334    /// Finishes a read under this option set.
335    ///
336    /// # Errors
337    ///
338    /// Returns the ordered findings when at least one finding is an error under
339    /// the selected parse mode.
340    pub fn finish(
341        &self,
342        structure: Structure,
343        findings: impl IntoIterator<Item = Diagnostic>,
344    ) -> ReadResult {
345        let mut ordered = Diagnostics::new();
346        ordered.extend(findings);
347        let (structure, reject) =
348            super::occupancy::apply(structure, self.occupancy_policy, &mut ordered);
349        let is_error = reject || ordered.has_error(self.mode.strictness());
350        let findings = ordered.finish();
351        if is_error {
352            Err(findings)
353        } else {
354            Ok((structure, findings))
355        }
356    }
357}
358
359/// A structure and everything that was wrong with the file it came from.
360pub type ReadResult = Result<(Structure, Vec<Diagnostic>), Vec<Diagnostic>>;
361
362/// A reader for one format.
363pub trait Reader {
364    /// The format this reads.
365    const FORMAT: Format;
366
367    /// Reads a structure.
368    ///
369    /// # Errors
370    ///
371    /// Returns the findings that stopped the read.
372    fn read(input: &InputBuffer, options: &ReadOptions) -> ReadResult;
373}
374
375/// Which parts of a structure a write should include.
376pub trait Select {
377    /// Whether to write this model.
378    fn accept_model(&self, _model: crate::index::ModelIndex) -> bool {
379        true
380    }
381
382    /// Whether to write this chain.
383    fn accept_chain(&self, _chain: crate::index::ChainIndex) -> bool {
384        true
385    }
386
387    /// Whether to write this residue.
388    fn accept_residue(&self, _residue: crate::index::ResidueIndex) -> bool {
389        true
390    }
391
392    /// Whether to write this atom.
393    fn accept_atom(&self, _atom: crate::index::AtomIndex) -> bool {
394        true
395    }
396}
397
398/// A filter that writes everything.
399#[derive(Clone, Copy, Debug, Default)]
400pub struct SelectAll;
401
402impl Select for SelectAll {}
403
404#[cfg(test)]
405#[path = "options_tests.rs"]
406mod tests;