molframe 0.5.1

A batteries-included structural bioinformatics engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
//! Reading and writing, dispatched to whichever format crate is linked.
//!
//! The dispatch is the only thing here. Deciding which format a file is belongs
//! to the core, reading it belongs to the format crate, and this is the seam
//! between them.

#[cfg(feature = "mmcif")]
use formats::{cif_write_findings, write_mmcif_to_with_options};
use molframe_core::ExecutionContext;
use molframe_core::diagnostic::{Code, Diagnostic, Findings};
use molframe_core::io::{Format, InputBuffer, OutputOptions, OutputSink, ReadOptions};
use molframe_core::structure::Structure as CoreStructure;
use std::io::Write;
use std::path::Path;

use crate::structure::Structure;

mod formats;

pub use read_in::{read_buffer_in, read_bytes_in, read_with_options_in};

#[cfg(feature = "chemistry")]
pub use formats::read_component_dictionary;
#[cfg(feature = "geometry")]
pub use formats::transform;
#[cfg(feature = "pdb")]
pub use formats::write_pdb;
#[cfg(feature = "mmcif")]
pub use formats::{read_document, write_mmcif, write_mmcif_with_options};
#[cfg(feature = "bcif")]
pub use formats::{write_bcif, write_bcif_with_options};

#[cfg(feature = "mmcif")]
mod cif_family;
mod dispatch;
#[cfg(feature = "mmcif")]
mod extensions;
#[cfg(feature = "pdb")]
mod pdb_symmetry;
mod read_in;
#[cfg(feature = "chemistry")]
mod small_molecule;
// The enum holds one variant per format crate, so with none of them linked it
// would be an empty type whose `next_batch` match has no arms to reach. The
// module is gated by the same four features that gate its variants, rather
// than publishing an enum nothing can construct.
#[cfg(any(
    feature = "mmcif",
    feature = "pdb",
    feature = "bcif",
    feature = "modelcif"
))]
mod structure_batches;

#[cfg(any(
    feature = "mmcif",
    feature = "pdb",
    feature = "bcif",
    feature = "modelcif"
))]
pub use structure_batches::{StructureBatchReader, open_structure_batches};

#[cfg(feature = "mmcif")]
use dispatch::unsupported;
use dispatch::{dispatch_read, unsupported_writer};

/// Reads a structure, discarding what was wrong with the file.
///
/// Convenient, and less honest than [`read_with_diagnostics`]: a file that
/// parses is not the same as a file that is right.
///
/// # Errors
///
/// Returns the findings that stopped the read.
pub fn read(path: impl AsRef<Path>) -> Result<Structure, Findings> {
    read_with_diagnostics(path).map(|(structure, _)| structure)
}

/// Reads a structure and everything that was wrong with the file it came from.
///
/// The findings that came with a structure that parsed are the pair's second
/// element. The findings that stopped a read are the error.
///
/// # Errors
///
/// Returns the findings that stopped the read.
pub fn read_with_diagnostics(
    path: impl AsRef<Path>,
) -> Result<(Structure, Vec<Diagnostic>), Findings> {
    read_with_options(path, &ReadOptions::new())
}

/// Reads a structure under explicit options.
///
/// # Errors
///
/// Returns the findings that stopped the read.
pub fn read_with_options(
    path: impl AsRef<Path>,
    options: &ReadOptions,
) -> Result<(Structure, Vec<Diagnostic>), Findings> {
    let path = path.as_ref();
    let input = InputBuffer::open(path, options.limits).map_err(Findings::from)?;
    let name = path.file_name().and_then(|name| name.to_str());
    read_buffer(&input, name, options)
}

/// Reads a structure from bytes already in hand.
///
/// `name` is only used to settle the format when the content does not.
///
/// # Errors
///
/// Returns the findings that stopped the read.
pub fn read_bytes(
    bytes: Vec<u8>,
    name: Option<&str>,
    options: &ReadOptions,
) -> Result<(Structure, Vec<Diagnostic>), Findings> {
    let input = InputBuffer::from_bytes(bytes);
    read_buffer(&input, name, options)
}

/// Writes a structure in the format named by the destination.
///
/// A `.gz` or `.zst` suffix applies deterministic compression after the format
/// suffix has selected the writer.
///
/// # Errors
///
/// Returns a diagnostic when the name does not select a linked writer, the
/// structure cannot be represented, compression is unavailable, or the
/// destination cannot be written.
pub fn write(path: impl AsRef<Path>, structure: &Structure) -> Result<(), Findings> {
    write_with_options(path, structure, &WriteOptions::canonical())
}

/// Streams a structure under explicit projection and memory decisions.
///
/// The output policy defaults to 100 MB and no policy above 500 MB is
/// accepted. The file sink itself retains at most 64 KiB; format writers
/// receive rows or columns incrementally and never construct a complete file
/// buffer.
///
/// # Errors
///
/// Returns format, projection, memory-policy, compression, or destination
/// diagnostics.
pub fn write_with_options(
    path: impl AsRef<Path>,
    structure: &Structure,
    options: &WriteOptions,
) -> Result<(), Findings> {
    let path = path.as_ref();
    let name = path.file_name().and_then(|name| name.to_str());
    let Some(format) = name.and_then(Format::from_name) else {
        return Err(Findings::from(
            Diagnostic::new(Code::E1001).with_context("name", path.display().to_string()),
        ));
    };
    write_as(path, structure, format, options)
}

/// As [`write_with_options`], in the stated format instead of the one the
/// destination's name selects. A `.gz` or `.zst` suffix still applies its
/// compression.
///
/// # Errors
///
/// Returns the same diagnostics as [`write_with_options`]; a format that has no
/// linked writer is refused.
pub fn write_as(
    path: impl AsRef<Path>,
    structure: &Structure,
    format: Format,
    options: &WriteOptions,
) -> Result<(), Findings> {
    let path = path.as_ref();
    let mut output = OutputSink::create(path, options.output).map_err(Findings::from)?;
    write_stream(&mut output, structure, format, options)?;
    output.finish().map_err(Findings::from)
}

/// The decisions a write makes: which identifiers the projection invents, per
/// format family, plus the output working-memory policy.
///
/// [`WriteOptions::canonical`] is the default projection; the `with_*`
/// builders hand the per-family decision types to callers that need explicit
/// identifiers.
#[derive(Clone, Debug)]
pub struct WriteOptions {
    output: OutputOptions,
    #[cfg(feature = "mmcif")]
    cif: molframe_cif::CifWriteOptions,
    #[cfg(feature = "pdb")]
    pdb: molframe_pdb::PdbOptions,
}

impl WriteOptions {
    /// Canonical projection with the default 100 MB output-workspace ceiling.
    #[must_use]
    pub fn canonical() -> Self {
        Self {
            output: OutputOptions::default(),
            #[cfg(feature = "mmcif")]
            cif: molframe_cif::CifWriteOptions::new(),
            #[cfg(feature = "pdb")]
            pdb: molframe_pdb::PdbOptions::new(),
        }
    }

    /// Replaces the output working-memory policy.
    #[must_use]
    pub fn with_output(mut self, output: OutputOptions) -> Self {
        self.output = output;
        self
    }

    /// Replaces the CIF-family projection decisions.
    #[cfg(feature = "mmcif")]
    #[must_use]
    pub fn with_cif(mut self, cif: molframe_cif::CifWriteOptions) -> Self {
        self.cif = cif;
        self
    }

    /// Replaces the PDB-family projection decisions.
    #[cfg(feature = "pdb")]
    #[must_use]
    pub fn with_pdb(mut self, pdb: molframe_pdb::PdbOptions) -> Self {
        self.pdb = pdb;
        self
    }

    /// The output working-memory policy.
    #[must_use]
    pub const fn output(&self) -> &OutputOptions {
        &self.output
    }

    /// The CIF-family projection decisions.
    #[cfg(feature = "mmcif")]
    #[must_use]
    pub const fn cif(&self) -> &molframe_cif::CifWriteOptions {
        &self.cif
    }

    /// The PDB-family projection decisions.
    #[cfg(feature = "pdb")]
    #[must_use]
    pub const fn pdb(&self) -> &molframe_pdb::PdbOptions {
        &self.pdb
    }
}

fn write_stream<W: Write>(
    output: &mut W,
    structure: &Structure,
    format: Format,
    options: &WriteOptions,
) -> Result<(), Findings> {
    #[cfg(not(any(feature = "mmcif", feature = "bcif", feature = "pdb")))]
    let _ = options;
    #[cfg(not(any(
        feature = "mmcif",
        feature = "bcif",
        feature = "pdb",
        feature = "chemistry"
    )))]
    let _ = (output, structure);
    match format {
        #[cfg(feature = "mmcif")]
        Format::Mmcif => write_mmcif_to_with_options(structure, options.cif(), output)
            .map_err(|error| cif_write_findings(&error)),
        #[cfg(feature = "bcif")]
        Format::BinaryCif => molframe_bcif::write_structure_to_with_memory_limit(
            structure.engine(),
            options.cif(),
            options.output().memory_limit_bytes,
            output,
        )
        .map_err(Findings::from),
        #[cfg(feature = "pdb")]
        Format::Pdb => molframe_pdb::write_to(structure.engine(), options.pdb(), output)
            .map_err(Findings::from),
        #[cfg(feature = "pdb")]
        Format::Mmtf => {
            molframe_pdb::write_mmtf_to(structure.engine(), output).map_err(Findings::from)
        }
        #[cfg(feature = "pdb")]
        Format::Pqr => molframe_pdb::write_pqr_to(structure.engine(), options.pdb(), output)
            .map_err(Findings::from),
        #[cfg(feature = "pdb")]
        Format::Pdbqt => molframe_pdb::write_pdbqt_to(structure.engine(), options.pdb(), output)
            .map_err(Findings::from),
        #[cfg(feature = "chemistry")]
        Format::Sdf => small_molecule::write_sdf_to(structure.engine(), output),
        other => Err(unsupported_writer(other).into()),
    }
}

/// Reads a structure from an already-acquired buffer, without a further copy.
///
/// This is the zero-copy entry point `read_bytes` builds on: `InputBuffer` is
/// already `Arc`-backed and cheap to clone, so a caller holding one (an FFI
/// boundary, a memory-mapped file) can read through it directly instead of
/// paying `read_bytes`'s `Vec<u8>` copy.
///
/// `name` is only used to settle the format when the content does not.
///
/// # Errors
///
/// Returns the findings that stopped the read.
pub fn read_buffer(
    input: &InputBuffer,
    name: Option<&str>,
    options: &ReadOptions,
) -> Result<(Structure, Vec<Diagnostic>), Findings> {
    read_buffer_in(input, name, options, &ExecutionContext::default())
}

/// Applies the default chemistry perception pass to a caller-created structure.
///
/// File reads perform this step automatically unless [`ReadOptions::only_atomic_coords`]
/// is set. Structures assembled by a custom reader or provider deliberately do
/// not run perception behind the caller's back; call this function when the
/// same file-like bond and secondary-structure guarantees are desired. The
/// returned structure shares the existing coordinate storage.
///
/// # Errors
///
/// Returns findings only if the supplied structure cannot be used as a valid
/// perception input.
#[cfg(all(feature = "chemistry", feature = "spatial"))]
pub fn perceive(structure: &Structure) -> Result<(Structure, Vec<Diagnostic>), Findings> {
    perceive_in(structure, &ExecutionContext::default())
}

/// [`perceive`] on the worker budget of `context`.
///
/// # Errors
///
/// Returns findings only if the supplied structure cannot be used as a valid
/// perception input.
#[cfg(all(feature = "chemistry", feature = "spatial"))]
pub fn perceive_in(
    structure: &Structure,
    context: &ExecutionContext,
) -> Result<(Structure, Vec<Diagnostic>), Findings> {
    enrich_read(
        Ok((structure.engine().clone(), Vec::new())),
        &ReadOptions::new(),
        context,
    )
    .map(|(structure, diagnostics)| (structure.into(), diagnostics))
}

#[cfg(all(feature = "chemistry", feature = "spatial"))]
fn enrich_read(
    result: Result<(CoreStructure, Vec<Diagnostic>), Findings>,
    options: &ReadOptions,
    context: &ExecutionContext,
) -> Result<(CoreStructure, Vec<Diagnostic>), Findings> {
    let (structure, mut findings) = result?;
    if options.only_atomic_coords {
        return Ok((structure, findings));
    }
    let structure = match molframe_chem::perceive_bonds_in(&structure, context) {
        Ok(structure) => structure,
        // A cancellation or an exhausted budget is the caller's own limit
        // taking effect, not a defect of the file: the read stops.
        Err(finding) if matches!(finding.code(), Code::E1902 | Code::E1904 | Code::E7001) => {
            return Err(Findings::from(finding));
        }
        Err(finding) => {
            findings.push(finding);
            structure
        }
    };
    let structure = match molframe_chem::annotate_standard_components(&structure) {
        Ok(structure) => structure,
        Err(finding) => {
            findings.push(finding);
            structure
        }
    };
    let (states, sources) = merge_secondary(
        structure.data(),
        molframe_chem::assign_secondary_structure(&structure),
    );
    let mut data = structure.data().clone();
    data.secondary_structure = states.into();
    data.secondary_source = sources.into();
    Ok((CoreStructure::new(data), findings))
}

/// Keeps, per residue, whichever assignment has the higher source rank, so a
/// deposited state always survives and analysis fills only what the file left
/// unassigned.
#[cfg(all(feature = "chemistry", feature = "spatial"))]
fn merge_secondary(
    data: &molframe_core::StructureData,
    inferred: Vec<molframe_core::SecondaryAssignment>,
) -> (
    Vec<molframe_core::SecondaryStructure>,
    Vec<molframe_core::SecondarySource>,
) {
    inferred
        .into_iter()
        .enumerate()
        .map(|(residue, inferred)| {
            let current = match (
                data.secondary_structure.get(residue),
                data.secondary_source.get(residue),
            ) {
                (Some(state), Some(source)) => molframe_core::SecondaryAssignment {
                    state: *state,
                    source: *source,
                },
                _ => molframe_core::SecondaryAssignment::default(),
            };
            let kept = if inferred.source.rank() > current.source.rank() {
                inferred
            } else {
                current
            };
            (kept.state, kept.source)
        })
        .unzip()
}

#[cfg(not(all(feature = "chemistry", feature = "spatial")))]
fn enrich_read(
    result: Result<(CoreStructure, Vec<Diagnostic>), Findings>,
    _options: &ReadOptions,
    _context: &ExecutionContext,
) -> Result<(CoreStructure, Vec<Diagnostic>), Findings> {
    result
}

#[cfg(all(test, feature = "pdb"))]
#[path = "facade/input_digest_tests.rs"]
mod input_digest_tests;
#[cfg(all(test, feature = "pdb"))]
#[path = "facade/pdb_symmetry_tests.rs"]
mod pdb_symmetry_tests;
#[cfg(all(test, feature = "chemistry", feature = "mmcif", feature = "pdb"))]
#[path = "facade/small_molecule_tests.rs"]
mod small_molecule_tests;
#[cfg(test)]
#[path = "facade/facade_tests.rs"]
mod tests;
#[cfg(all(test, feature = "pdb"))]
#[path = "facade/write_as_tests.rs"]
mod write_as_tests;