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;