Skip to main content

cosmolkit_core/bio/
mod.rs

1//! Biomolecular structure primitives.
2//!
3//! `BioStructure` is a flat-row, hierarchy-indexed representation for proteins,
4//! DNA, RNA, and complexes. It is NOT a giant `Molecule`; it is a hierarchy +
5//! coordinate + assembly object. See `dev/bio_structure_operation_contract_design.md`.
6//!
7//! Gemmi marker convention is defined in `dev/source_reproduction_protocol.md`.
8
9use std::collections::HashMap;
10use std::marker::PhantomData;
11
12pub mod invariants;
13pub mod ops;
14pub mod protein;
15pub mod resinfo;
16
17// ---------------------------------------------------------------------------
18// Stable row IDs
19// ---------------------------------------------------------------------------
20
21macro_rules! row_id {
22    ($name:ident) => {
23        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24        pub struct $name(u32);
25
26        impl $name {
27            #[must_use]
28            pub const fn new(index: u32) -> Self {
29                Self(index)
30            }
31
32            #[must_use]
33            pub const fn index(self) -> u32 {
34                self.0
35            }
36        }
37    };
38}
39
40row_id!(AtomId);
41row_id!(ResidueId);
42row_id!(ChainId);
43row_id!(EntityId);
44row_id!(ModelId);
45row_id!(BondId);
46row_id!(AssemblyId);
47row_id!(AltLocGroupId);
48
49// ---------------------------------------------------------------------------
50// RowSpan — contiguous child range within a parent block
51// ---------------------------------------------------------------------------
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct RowSpan<T> {
55    pub start: u32,
56    pub len: u32,
57    _marker: PhantomData<T>,
58}
59
60impl<T> RowSpan<T> {
61    #[must_use]
62    pub const fn new(start: u32, len: u32) -> Self {
63        Self {
64            start,
65            len,
66            _marker: PhantomData,
67        }
68    }
69
70    #[must_use]
71    pub const fn end(self) -> u32 {
72        self.start + self.len
73    }
74
75    #[must_use]
76    pub const fn is_empty(self) -> bool {
77        self.len == 0
78    }
79}
80
81// ---------------------------------------------------------------------------
82// Polymer classification enums
83// ---------------------------------------------------------------------------
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum ResidueKind {
87    AminoAcid,
88    DNA,
89    RNA,
90    Saccharide,
91    Water,
92    Ligand,
93    Ion,
94    Cofactor,
95    Unknown,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub enum PolymerKind {
100    Peptide,
101    DNA,
102    RNA,
103    PeptideLike,
104    NucleicAcidHybrid,
105    Saccharide,
106    NonPolymer,
107    Water,
108    Unknown,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub enum EntityKind {
113    Polymer,
114    NonPolymer,
115    Branched,
116    Water,
117    Unknown,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub enum ChainKind {
122    Protein,
123    DNA,
124    RNA,
125    ProteinDNAComplex,
126    ProteinRNAComplex,
127    LigandOnly,
128    WaterOnly,
129    Mixed,
130    Unknown,
131}
132
133// ---------------------------------------------------------------------------
134// Source identifier types (PDB/mmCIF provenance, NOT row ids)
135// ---------------------------------------------------------------------------
136
137/// PDB atom serial number (source provenance only).
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct PdbAtomSerial(pub i32);
140
141/// PDB/mmCIF chain identifier string (up to 4 chars).
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143pub struct PdbChainId(pub [u8; 4], pub u8);
144
145impl PdbChainId {
146    #[must_use]
147    pub fn as_str(&self) -> &str {
148        std::str::from_utf8(&self.0[..self.1 as usize]).unwrap_or("")
149    }
150}
151
152/// PDB residue sequence number + insertion code.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
154pub struct PdbSeqId {
155    pub seq_num: i32,
156    pub ins_code: Option<u8>,
157}
158
159// ---------------------------------------------------------------------------
160// Atom / residue / chain / model name types
161// ---------------------------------------------------------------------------
162
163/// Up to 4-char atom name (e.g. " CA ", " N  ").
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct AtomName(pub [u8; 4]);
166
167/// Up to 3-char residue name (e.g. "ALA", "GLY").
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub struct ResidueName(pub [u8; 4], pub u8);
170
171impl ResidueName {
172    #[must_use]
173    pub fn as_str(&self) -> &str {
174        std::str::from_utf8(&self.0[..self.1 as usize]).unwrap_or("")
175    }
176}
177
178#[must_use]
179pub fn classify_residue_name(name: ResidueName) -> ResidueKind {
180    let info = resinfo::find_tabulated_residue(name.as_str());
181    if info.is_amino_acid() {
182        ResidueKind::AminoAcid
183    } else if info.is_water() {
184        ResidueKind::Water
185    } else {
186        ResidueKind::Unknown
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    fn residue_name(value: &str) -> ResidueName {
195        let mut bytes = [0; 4];
196        bytes[..value.len()].copy_from_slice(value.as_bytes());
197        ResidueName(bytes, value.len() as u8)
198    }
199
200    #[test]
201    fn classifies_complete_gemmi_amino_acid_vocabulary() {
202        let amino_acids = resinfo::RESIDUE_INFO_TABLE
203            .iter()
204            .filter(|info| info.is_amino_acid())
205            .collect::<Vec<_>>();
206        assert_eq!(amino_acids.len(), 128);
207        for info in amino_acids {
208            assert_eq!(
209                classify_residue_name(residue_name(info.name)),
210                ResidueKind::AminoAcid
211            );
212        }
213    }
214
215    #[test]
216    fn classifies_gemmi_water_names_without_guessing_other_residues() {
217        for name in ["HOH", "DOD", "WAT", "H2O"] {
218            assert_eq!(
219                classify_residue_name(residue_name(name)),
220                ResidueKind::Water
221            );
222        }
223        assert_eq!(
224            classify_residue_name(residue_name("XYZ")),
225            ResidueKind::Unknown
226        );
227    }
228}
229
230/// Single-char altloc label (e.g. b'A', b'B').
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub struct AltLocLabel(pub u8);
233
234// ---------------------------------------------------------------------------
235// Source identifier bundles (per row)
236// ---------------------------------------------------------------------------
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub struct AtomSourceIds {
240    pub serial: Option<PdbAtomSerial>,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
244pub enum BioCalcFlag {
245    #[default]
246    NotSet,
247    NoHydrogen,
248    Determined,
249    Calculated,
250    Dummy,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub struct ResidueSourceIds {
255    pub seq_id: Option<PdbSeqId>,
256    pub label_seq_id: Option<i32>,
257    pub segment_id: Option<[u8; 4]>,
258    pub subchain_id: Option<PdbChainId>,
259    pub label_entity_id: Option<EntityId>,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub struct ChainSourceIds {
264    pub auth_chain_id: Option<PdbChainId>,
265    pub label_asym_id: Option<PdbChainId>,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct EntitySourceIds {
270    pub source_entity_id: String,
271}
272
273// ---------------------------------------------------------------------------
274// Flat row types
275// ---------------------------------------------------------------------------
276
277#[derive(Debug, Clone, PartialEq)]
278pub struct AtomRow {
279    pub residue_id: ResidueId,
280    pub name: AtomName,
281    pub element: crate::Element,
282    pub altloc: Option<AltLocLabel>,
283    pub occupancy: Option<f32>,
284    pub b_iso: Option<f32>,
285    pub formal_charge: Option<i8>,
286    pub anisou: Option<[f32; 6]>,
287    pub calc_flag: BioCalcFlag,
288    pub tls_group_id: Option<i16>,
289    pub fraction: Option<f32>,
290    pub source: AtomSourceIds,
291}
292
293#[derive(Debug, Clone, PartialEq)]
294pub struct ResidueRow {
295    pub chain_id: ChainId,
296    pub atom_span: RowSpan<AtomId>,
297    pub name: ResidueName,
298    pub kind: ResidueKind,
299    pub entity_kind: EntityKind,
300    pub het_flag: Option<char>,
301    pub source: ResidueSourceIds,
302    pub sifts_unp: Option<BioSiftsUnpResidue>,
303}
304
305#[derive(Debug, Clone, PartialEq)]
306pub struct ChainRow {
307    pub model_id: ModelId,
308    pub entity_id: Option<EntityId>,
309    pub residue_span: RowSpan<ResidueId>,
310    pub kind: ChainKind,
311    pub source: ChainSourceIds,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct EntityRow {
316    pub kind: EntityKind,
317    pub polymer_kind: PolymerKind,
318    pub reflects_microhetero: bool,
319    pub sequence: Vec<String>,
320    pub dbrefs: Vec<BioEntityDbRef>,
321    pub sifts_unp_acc: Vec<String>,
322    pub subchains: Vec<PdbChainId>,
323    pub source: EntitySourceIds,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Default)]
327pub struct BioEntityDbRef {
328    pub db_name: String,
329    pub accession_code: String,
330    pub id_code: String,
331    pub isoform: String,
332    // Gemmi✔️✔️: SeqId seq_begin, seq_end;
333    // Gemmi✔️✔️: SeqId db_begin, db_end;
334    // Gemmi✔️✔️: OptionalNum num;   // sequence number
335    pub seq_begin: Option<PdbSeqId>,
336    pub seq_end: Option<PdbSeqId>,
337    pub db_begin: Option<PdbSeqId>,
338    pub db_end: Option<PdbSeqId>,
339    pub label_seq_begin: Option<i32>,
340    pub label_seq_end: Option<i32>,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
344pub struct BioSiftsUnpResidue {
345    pub res: Option<char>,
346    pub acc_index: u8,
347    pub num: u16,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Default)]
351pub struct BioModRes {
352    pub chain_name: String,
353    pub res_id: PdbSeqId,
354    pub residue_name: String,
355    pub parent_comp_id: String,
356    pub mod_id: String,
357    pub details: String,
358}
359
360#[derive(Debug, Clone, PartialEq)]
361pub struct ModelRow {
362    pub chain_span: RowSpan<ChainId>,
363    pub source_model_number: Option<i32>,
364}
365
366#[derive(Debug, Clone, PartialEq, Default)]
367pub struct BioMetadata {
368    pub entry_id: Option<String>,
369    pub title: Option<String>,
370    pub pdbx_keywords: Option<String>,
371    pub keywords: Option<String>,
372    pub experimental_method: Option<String>,
373    pub received_initial_deposition_date: Option<String>,
374    pub authors: Vec<String>,
375    pub software: Vec<BioSoftwareItem>,
376    pub refinement: Vec<BioRefinementInfo>,
377    pub experiments: Vec<BioExperimentInfo>,
378    pub experiment_crystals: Vec<BioExperimentCrystalInfo>,
379    pub solved_by: Option<String>,
380    pub starting_model: Option<String>,
381    pub remark_300_detail: Option<String>,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
385pub enum BioSoftwareClassification {
386    DataCollection,
387    DataExtraction,
388    DataProcessing,
389    DataReduction,
390    DataScaling,
391    ModelBuilding,
392    Phasing,
393    Refinement,
394    #[default]
395    Unspecified,
396}
397
398#[derive(Debug, Clone, PartialEq, Default)]
399pub struct BioSoftwareItem {
400    pub name: String,
401    pub version: String,
402    pub date: String,
403    pub description: String,
404    pub contact_author: String,
405    pub contact_author_email: String,
406    pub classification: BioSoftwareClassification,
407}
408
409#[derive(Debug, Clone, PartialEq, Default)]
410pub struct BioRefinementBin {
411    pub resolution_high: Option<f64>,
412    pub resolution_low: Option<f64>,
413    pub completeness: Option<f64>,
414    pub reflection_count: Option<i32>,
415    pub work_set_count: Option<i32>,
416    pub rfree_set_count: Option<i32>,
417    pub r_all: Option<f64>,
418    pub r_work: Option<f64>,
419    pub r_free: Option<f64>,
420    pub cc_fo_fc_work: Option<f64>,
421    pub cc_fo_fc_free: Option<f64>,
422    pub fsc_work: Option<f64>,
423    pub fsc_free: Option<f64>,
424    pub cc_intensity_work: Option<f64>,
425    pub cc_intensity_free: Option<f64>,
426}
427
428#[derive(Debug, Clone, PartialEq, Default)]
429pub struct BioRefinementRestraint {
430    pub name: String,
431    pub count: Option<i32>,
432    pub weight: Option<f64>,
433    pub function: String,
434    pub dev_ideal: Option<f64>,
435}
436
437#[derive(Debug, Clone, PartialEq, Default)]
438pub struct BioTlsSelection {
439    pub chain: String,
440    pub res_begin: Option<PdbSeqId>,
441    pub res_end: Option<PdbSeqId>,
442    pub details: String,
443}
444
445#[derive(Debug, Clone, PartialEq)]
446pub struct BioTlsGroup {
447    pub num_id: Option<i16>,
448    pub id: String,
449    pub selections: Vec<BioTlsSelection>,
450    pub origin: [f64; 3],
451    pub t: [[f64; 3]; 3],
452    pub l: [[f64; 3]; 3],
453    pub s: [[f64; 3]; 3],
454}
455
456impl Default for BioTlsGroup {
457    fn default() -> Self {
458        Self {
459            num_id: None,
460            id: String::new(),
461            selections: Vec::new(),
462            origin: [f64::NAN; 3],
463            t: [[f64::NAN; 3]; 3],
464            l: [[f64::NAN; 3]; 3],
465            s: [[f64::NAN; 3]; 3],
466        }
467    }
468}
469
470#[derive(Debug, Clone, PartialEq)]
471pub struct BioAnisotropicB {
472    pub u11: f64,
473    pub u22: f64,
474    pub u33: f64,
475    pub u12: f64,
476    pub u13: f64,
477    pub u23: f64,
478}
479
480impl Default for BioAnisotropicB {
481    fn default() -> Self {
482        Self {
483            u11: f64::NAN,
484            u22: f64::NAN,
485            u33: f64::NAN,
486            u12: f64::NAN,
487            u13: f64::NAN,
488            u23: f64::NAN,
489        }
490    }
491}
492
493#[derive(Debug, Clone, PartialEq, Default)]
494pub struct BioRefinementInfo {
495    pub id: String,
496    pub resolution_high: Option<f64>,
497    pub resolution_low: Option<f64>,
498    pub completeness: Option<f64>,
499    pub reflection_count: Option<i32>,
500    pub work_set_count: Option<i32>,
501    pub rfree_set_count: Option<i32>,
502    pub r_all: Option<f64>,
503    pub r_work: Option<f64>,
504    pub r_free: Option<f64>,
505    pub cross_validation_method: String,
506    pub rfree_selection_method: String,
507    pub bin_count: Option<i32>,
508    pub bins: Vec<BioRefinementBin>,
509    pub mean_b: Option<f64>,
510    pub aniso_b: BioAnisotropicB,
511    pub luzzati_error: Option<f64>,
512    pub dpi_blow_r: Option<f64>,
513    pub dpi_blow_rfree: Option<f64>,
514    pub dpi_cruickshank_r: Option<f64>,
515    pub dpi_cruickshank_rfree: Option<f64>,
516    pub cc_fo_fc_work: Option<f64>,
517    pub cc_fo_fc_free: Option<f64>,
518    pub fsc_work: Option<f64>,
519    pub fsc_free: Option<f64>,
520    pub cc_intensity_work: Option<f64>,
521    pub cc_intensity_free: Option<f64>,
522    pub restr_stats: Vec<BioRefinementRestraint>,
523    pub tls_groups: Vec<BioTlsGroup>,
524    pub remarks: String,
525}
526
527#[derive(Debug, Clone, PartialEq, Default)]
528pub struct BioReflectionsInfo {
529    pub resolution_high: Option<f64>,
530    pub resolution_low: Option<f64>,
531    pub completeness: Option<f64>,
532    pub redundancy: Option<f64>,
533    pub r_merge: Option<f64>,
534    pub r_sym: Option<f64>,
535    pub mean_i_over_sigma: Option<f64>,
536}
537
538#[derive(Debug, Clone, PartialEq, Default)]
539pub struct BioExperimentInfo {
540    pub method: String,
541    pub number_of_crystals: Option<i32>,
542    pub unique_reflections: Option<i32>,
543    pub diffraction_ids: Vec<String>,
544    pub reflections: BioReflectionsInfo,
545    pub b_wilson: Option<f64>,
546    pub shells: Vec<BioReflectionsInfo>,
547}
548
549#[derive(Debug, Clone, PartialEq, Default)]
550pub struct BioDiffractionInfo {
551    pub id: String,
552    pub collection_date: String,
553    pub temperature: Option<f64>,
554    pub source: String,
555    pub source_type: String,
556    pub synchrotron: String,
557    pub beamline: String,
558    pub wavelengths: String,
559    pub scattering_type: String,
560    pub monochromator: String,
561    pub optics: String,
562    pub detector: String,
563    pub detector_make: String,
564    pub mono_or_laue: Option<char>,
565}
566
567#[derive(Debug, Clone, PartialEq, Default)]
568pub struct BioExperimentCrystalInfo {
569    pub id: String,
570    pub description: String,
571    pub ph: Option<f64>,
572    pub ph_range: String,
573    pub diffractions: Vec<BioDiffractionInfo>,
574}
575
576#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
577pub enum BioAsu {
578    #[default]
579    Any,
580    Same,
581    Different,
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Default)]
585pub struct BioAtomAddress {
586    pub chain_name: String,
587    pub seq_id: Option<PdbSeqId>,
588    pub residue_name: String,
589    pub atom_name: String,
590    pub altloc: Option<AltLocLabel>,
591}
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
594pub enum BioConnectionType {
595    Covale,
596    Disulf,
597    Hydrog,
598    MetalC,
599    #[default]
600    Unknown,
601}
602
603#[derive(Debug, Clone, PartialEq, Default)]
604pub struct BioConnection {
605    pub name: String,
606    pub type_: BioConnectionType,
607    pub partner1: BioAtomAddress,
608    pub partner2: BioAtomAddress,
609    pub asu: BioAsu,
610    pub reported_sym: [i16; 4],
611    pub reported_distance: Option<f64>,
612    pub link_id: String,
613}
614
615#[derive(Debug, Clone, PartialEq, Default)]
616pub struct BioCisPep {
617    pub partner_c: BioAtomAddress,
618    pub partner_n: BioAtomAddress,
619    pub model_num: i32,
620    pub only_altloc: Option<AltLocLabel>,
621    pub reported_angle: Option<f64>,
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
625pub enum BioHelixClass {
626    #[default]
627    UnknownHelix,
628    RAlpha,
629    ROmega,
630    RPi,
631    RGamma,
632    R310,
633    LAlpha,
634    LOmega,
635    LGamma,
636    Helix27,
637    HelixPolyProlineNone,
638}
639
640#[derive(Debug, Clone, PartialEq)]
641pub struct BioHelix {
642    pub start: BioAtomAddress,
643    pub end: BioAtomAddress,
644    pub helix_class: BioHelixClass,
645    pub length: i32,
646}
647
648impl Default for BioHelix {
649    fn default() -> Self {
650        // Gemmi✔️✔️: struct Helix {
651        // Gemmi✔️✔️:   AtomAddress start, end;
652        // Gemmi✔️✔️:   HelixClass pdb_helix_class = UnknownHelix;
653        // Gemmi✔️✔️:   int length = -1;
654        Self {
655            start: BioAtomAddress::default(),
656            end: BioAtomAddress::default(),
657            helix_class: BioHelixClass::UnknownHelix,
658            length: -1,
659        }
660    }
661}
662
663impl BioHelix {
664    pub fn set_helix_class_as_int(&mut self, n: i32) {
665        self.helix_class = match n {
666            1 => BioHelixClass::RAlpha,
667            2 => BioHelixClass::ROmega,
668            3 => BioHelixClass::RPi,
669            4 => BioHelixClass::RGamma,
670            5 => BioHelixClass::R310,
671            6 => BioHelixClass::LAlpha,
672            7 => BioHelixClass::LOmega,
673            8 => BioHelixClass::LGamma,
674            9 => BioHelixClass::Helix27,
675            10 => BioHelixClass::HelixPolyProlineNone,
676            _ => BioHelixClass::UnknownHelix,
677        };
678    }
679}
680
681#[derive(Debug, Clone, PartialEq, Default)]
682pub struct BioSheetStrand {
683    pub start: BioAtomAddress,
684    pub end: BioAtomAddress,
685    pub hbond_atom2: BioAtomAddress,
686    pub hbond_atom1: BioAtomAddress,
687    pub sense: i32,
688    pub name: String,
689}
690
691#[derive(Debug, Clone, PartialEq, Default)]
692pub struct BioSheet {
693    pub name: String,
694    pub strands: Vec<BioSheetStrand>,
695}
696
697#[derive(Debug, Clone, Copy, PartialEq)]
698pub struct BioTransform {
699    pub mat: [[f64; 3]; 3],
700    pub vec: [f64; 3],
701}
702
703impl Default for BioTransform {
704    fn default() -> Self {
705        // Gemmi✔️✔️: struct Mat33 {
706        // Gemmi✔️✔️:   double a[3][3] = { {1.,0.,0.}, {0.,1.,0.}, {0.,0.,1.} };
707        // Gemmi✔️✔️:   Mat33() = default;
708        // Gemmi✔️✔️: struct Transform {
709        // Gemmi✔️✔️:   Mat33 mat;
710        // Gemmi✔️✔️:   Vec3 vec;
711        // Gemmi✔️✔️:   Vec3_() : x(0), y(0), z(0) {}
712        Self {
713            mat: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
714            vec: [0.0, 0.0, 0.0],
715        }
716    }
717}
718
719#[derive(Debug, Clone, PartialEq, Default)]
720pub struct BioNcsOperator {
721    pub id: String,
722    pub given: bool,
723    pub transform: BioTransform,
724}
725
726#[derive(Debug, Clone, PartialEq, Default)]
727pub struct BioAssemblyOperator {
728    pub name: String,
729    pub type_: String,
730    pub transform: BioTransform,
731}
732
733#[derive(Debug, Clone, PartialEq, Default)]
734pub struct BioAssemblyGenerator {
735    pub chains: Vec<String>,
736    pub subchains: Vec<String>,
737    pub operators: Vec<BioAssemblyOperator>,
738}
739
740#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
741pub enum BioAssemblySpecialKind {
742    #[default]
743    NA,
744    CompleteIcosahedral,
745    RepresentativeHelical,
746    CompletePoint,
747}
748
749#[derive(Debug, Clone, PartialEq, Default)]
750pub struct BioAssembly {
751    pub name: String,
752    pub author_determined: bool,
753    pub software_determined: bool,
754    pub special_kind: BioAssemblySpecialKind,
755    pub oligomeric_count: i32,
756    pub oligomeric_details: String,
757    pub software_name: String,
758    pub absa: Option<f64>,
759    pub ssa: Option<f64>,
760    pub more: Option<f64>,
761    pub generators: Vec<BioAssemblyGenerator>,
762}
763
764#[derive(Debug, Clone, Copy, PartialEq)]
765pub struct CrystalCell {
766    pub a: f64,
767    pub b: f64,
768    pub c: f64,
769    pub alpha: f64,
770    pub beta: f64,
771    pub gamma: f64,
772}
773
774#[derive(Debug, Clone, PartialEq)]
775pub struct CrystalInfo {
776    pub cell: CrystalCell,
777    pub spacegroup_hm: Option<String>,
778    pub z_pdb: Option<String>,
779    pub scale: Option<BioTransform>,
780    pub frac: BioTransform,
781    pub orth: BioTransform,
782    pub explicit_matrices: bool,
783    pub cs_count: i16,
784    pub cell_images: Vec<BioTransform>,
785}
786
787// ---------------------------------------------------------------------------
788// Coordinate block
789// ---------------------------------------------------------------------------
790
791/// 3D coordinates for all atoms, indexed by AtomId.
792/// Invariant: `len() == atoms.len()` in the owning BioStructure.
793#[derive(Debug, Clone, PartialEq, Default)]
794pub struct CoordinateBlock {
795    pub(crate) positions: Vec<[f64; 3]>,
796}
797
798impl CoordinateBlock {
799    #[must_use]
800    pub fn positions(&self) -> &[[f64; 3]] {
801        &self.positions
802    }
803}
804
805#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
806pub enum BioCoorFormat {
807    Pdb,
808    Mmcif,
809    Mmjson,
810    ChemComp,
811    #[default]
812    Unknown,
813    Detect,
814}
815
816// ---------------------------------------------------------------------------
817// Top-level BioStructure
818// ---------------------------------------------------------------------------
819
820/// Flat-row biomolecular structure.
821///
822/// Hierarchy: Model → Chain → Residue → Atom (all stored as flat Vecs).
823/// Row ids are snapshot-local indices; PDB/mmCIF source identifiers are stored
824/// in the `source` fields of each row.
825#[derive(Debug, Clone, PartialEq, Default)]
826pub struct BioStructure {
827    pub(crate) name: String,
828    pub(crate) input_format: BioCoorFormat,
829    pub(crate) atoms: Vec<AtomRow>,
830    pub(crate) residues: Vec<ResidueRow>,
831    pub(crate) chains: Vec<ChainRow>,
832    pub(crate) entities: Vec<EntityRow>,
833    pub(crate) models: Vec<ModelRow>,
834    pub(crate) coordinates: CoordinateBlock,
835    pub(crate) metadata: BioMetadata,
836    pub(crate) crystal: Option<CrystalInfo>,
837    pub(crate) resolution: Option<f64>,
838    pub(crate) non_ascii_line: Option<usize>,
839    pub(crate) raw_remarks: Vec<String>,
840    pub(crate) ter_status: char,
841    pub(crate) has_d_fraction: bool,
842    pub(crate) mod_residues: Vec<BioModRes>,
843    pub(crate) shortened_ccd_codes: Vec<(String, String)>,
844    pub(crate) conect_map: HashMap<i32, Vec<i32>>,
845    pub(crate) deferred_conn_records: Vec<String>,
846    pub(crate) connections: Vec<BioConnection>,
847    pub(crate) cispeps: Vec<BioCisPep>,
848    pub(crate) helices: Vec<BioHelix>,
849    pub(crate) sheets: Vec<BioSheet>,
850    pub(crate) remark_290_operators: Vec<String>,
851    pub(crate) assemblies: Vec<BioAssembly>,
852    pub(crate) has_origx: bool,
853    pub(crate) origx: BioTransform,
854    pub(crate) ncs_operators: Vec<BioNcsOperator>,
855    pub(crate) ncs_oper_identity_id: Option<String>,
856}
857
858impl BioStructure {
859    #[must_use]
860    pub fn new() -> Self {
861        Self::default()
862    }
863
864    /// Reads a Gemmi-aligned PDB structural record stream into a `BioStructure`.
865    pub fn from_pdb_str(text: &str) -> Result<Self, crate::io::bio::BioReadError> {
866        Self::from_pdb_str_with_params(text, crate::io::bio::BioPdbReadParams::default())
867    }
868
869    /// Reads a PDB file into the complete structural model.
870    pub fn from_pdb(
871        path: impl AsRef<std::path::Path>,
872    ) -> Result<Self, crate::io::bio::BioReadError> {
873        let path = path.as_ref();
874        let text =
875            std::fs::read_to_string(path).map_err(|error| crate::io::bio::BioReadError::Parse {
876                line_number: 0,
877                message: format!("failed to read PDB file '{}': {error}", path.display()),
878            })?;
879        Self::from_str_with_format(&text, &path.to_string_lossy(), BioCoorFormat::Pdb)
880    }
881
882    /// Reads a Gemmi-aligned PDB structural record stream with explicit PDB reader parameters.
883    pub fn from_pdb_str_with_params(
884        text: &str,
885        params: crate::io::bio::BioPdbReadParams,
886    ) -> Result<Self, crate::io::bio::BioReadError> {
887        crate::io::bio::read_pdb_bio_structure_from_str_with_params(text, params)
888    }
889
890    /// Reads a Gemmi-aligned mmCIF structural document into a `BioStructure`.
891    pub fn from_mmcif_str(text: &str, path: &str) -> Result<Self, crate::io::bio::BioReadError> {
892        Self::from_str_with_format(text, path, BioCoorFormat::Mmcif)
893    }
894
895    /// Reads an mmCIF file into the complete structural model.
896    pub fn from_mmcif(
897        path: impl AsRef<std::path::Path>,
898    ) -> Result<Self, crate::io::bio::BioReadError> {
899        let path = path.as_ref();
900        let text =
901            std::fs::read_to_string(path).map_err(|error| crate::io::bio::BioReadError::Parse {
902                line_number: 0,
903                message: format!("failed to read mmCIF file '{}': {error}", path.display()),
904            })?;
905        Self::from_mmcif_str(&text, &path.to_string_lossy())
906    }
907
908    /// Reads a Gemmi-aligned structure by detecting the input format from text.
909    pub fn from_structure_str(
910        text: &str,
911        path: &str,
912    ) -> Result<Self, crate::io::bio::BioReadError> {
913        Self::from_str_with_format(text, path, BioCoorFormat::Detect)
914    }
915
916    /// Reads a Gemmi-aligned structure using the requested coordinate format.
917    pub fn from_str_with_format(
918        text: &str,
919        path: &str,
920        format: BioCoorFormat,
921    ) -> Result<Self, crate::io::bio::BioReadError> {
922        crate::io::bio::read_structure_from_memory(text, path, format)
923    }
924
925    /// Serializes this complete structural model as Gemmi-aligned mmCIF.
926    pub fn to_mmcif(&self) -> Result<String, crate::io::bio::BioWriteError> {
927        self.to_mmcif_with_options(crate::io::bio::MmcifWriteOptions::default())
928    }
929
930    /// Serializes this complete structural model with explicit mmCIF output options.
931    pub fn to_mmcif_with_options(
932        &self,
933        options: crate::io::bio::MmcifWriteOptions,
934    ) -> Result<String, crate::io::bio::BioWriteError> {
935        crate::io::bio::bio_structure_to_mmcif(self, options)
936    }
937
938    /// Writes this complete structural model as Gemmi-aligned mmCIF.
939    pub fn write_mmcif(
940        &self,
941        path: impl AsRef<std::path::Path>,
942    ) -> Result<(), crate::io::bio::BioWriteError> {
943        self.write_mmcif_with_options(path, crate::io::bio::MmcifWriteOptions::default())
944    }
945
946    /// Writes this complete structural model with explicit mmCIF output options.
947    pub fn write_mmcif_with_options(
948        &self,
949        path: impl AsRef<std::path::Path>,
950        options: crate::io::bio::MmcifWriteOptions,
951    ) -> Result<(), crate::io::bio::BioWriteError> {
952        crate::io::bio::write_bio_structure_mmcif(self, path.as_ref(), options)
953    }
954
955    #[must_use]
956    pub fn num_atoms(&self) -> usize {
957        self.atoms.len()
958    }
959
960    #[must_use]
961    pub fn num_residues(&self) -> usize {
962        self.residues.len()
963    }
964
965    #[must_use]
966    pub fn num_chains(&self) -> usize {
967        self.chains.len()
968    }
969
970    #[must_use]
971    pub fn num_models(&self) -> usize {
972        self.models.len()
973    }
974
975    #[must_use]
976    pub fn num_entities(&self) -> usize {
977        self.entities.len()
978    }
979
980    #[must_use]
981    pub fn name(&self) -> &str {
982        &self.name
983    }
984
985    #[must_use]
986    pub fn input_format(&self) -> BioCoorFormat {
987        self.input_format
988    }
989
990    #[must_use]
991    pub fn atoms(&self) -> &[AtomRow] {
992        &self.atoms
993    }
994
995    #[must_use]
996    pub fn residues(&self) -> &[ResidueRow] {
997        &self.residues
998    }
999
1000    #[must_use]
1001    pub fn chains(&self) -> &[ChainRow] {
1002        &self.chains
1003    }
1004
1005    #[must_use]
1006    pub fn models(&self) -> &[ModelRow] {
1007        &self.models
1008    }
1009
1010    #[must_use]
1011    pub fn entities(&self) -> &[EntityRow] {
1012        &self.entities
1013    }
1014
1015    #[must_use]
1016    pub fn metadata(&self) -> &BioMetadata {
1017        &self.metadata
1018    }
1019
1020    #[must_use]
1021    pub fn crystal(&self) -> Option<&CrystalInfo> {
1022        self.crystal.as_ref()
1023    }
1024
1025    #[must_use]
1026    pub fn resolution(&self) -> Option<f64> {
1027        self.resolution
1028    }
1029
1030    #[must_use]
1031    pub fn ter_status(&self) -> char {
1032        self.ter_status
1033    }
1034
1035    #[must_use]
1036    pub fn connections(&self) -> &[BioConnection] {
1037        &self.connections
1038    }
1039
1040    #[must_use]
1041    pub fn cispeps(&self) -> &[BioCisPep] {
1042        &self.cispeps
1043    }
1044
1045    #[must_use]
1046    pub fn mod_residues(&self) -> &[BioModRes] {
1047        &self.mod_residues
1048    }
1049
1050    #[must_use]
1051    pub fn assemblies(&self) -> &[BioAssembly] {
1052        &self.assemblies
1053    }
1054
1055    #[must_use]
1056    pub fn has_origx(&self) -> bool {
1057        self.has_origx
1058    }
1059
1060    #[must_use]
1061    pub fn origx(&self) -> &BioTransform {
1062        &self.origx
1063    }
1064
1065    #[must_use]
1066    pub fn ncs_operators(&self) -> &[BioNcsOperator] {
1067        &self.ncs_operators
1068    }
1069
1070    #[must_use]
1071    pub fn ncs_oper_identity_id(&self) -> Option<&str> {
1072        self.ncs_oper_identity_id.as_deref()
1073    }
1074
1075    #[must_use]
1076    pub fn coordinates(&self) -> &CoordinateBlock {
1077        &self.coordinates
1078    }
1079
1080    #[must_use]
1081    pub fn atom_position(&self, atom: AtomId) -> Option<[f64; 3]> {
1082        self.coordinates
1083            .positions
1084            .get(atom.index() as usize)
1085            .copied()
1086    }
1087
1088    #[must_use]
1089    pub fn residue_atoms(&self, residue: ResidueId) -> Option<&[AtomRow]> {
1090        let row = self.residues.get(residue.index() as usize)?;
1091        let start = row.atom_span.start as usize;
1092        let end = row.atom_span.end() as usize;
1093        self.atoms.get(start..end)
1094    }
1095}