mod connectivity;
mod finalize;
mod hierarchy;
mod links;
mod metadata;
mod models;
mod rows;
mod secondary;
mod variants;
pub(super) use rows::{charge_in, charge_of, serial_in, serial_of, starts_with_coordinate_keyword};
use models::{AtomIdentity, ModelRead};
use super::lines::Line;
use crate::fixed;
use crate::header::PdbHeaders;
use crate::hybrid36;
use links::{LinkAtoms, LinkRequest};
use molframe_core::chunk::{AtomRecord, ChunkBuilder};
use molframe_core::column::Presence;
use molframe_core::coords::CoordinateBlock;
use molframe_core::diagnostic::{Code, Diagnostic, Diagnostics};
use molframe_core::element::Element;
use molframe_core::index::{AtomIndex, EntityIndex, ResidueIndex};
use molframe_core::io::{Format, MissingElementPolicy, ReadOptions};
use molframe_core::optional::OptionalSymbol;
use molframe_core::span::{ByteSpan, Position};
use molframe_core::structure::StructureData;
use molframe_core::symbol::SymbolId;
use num_traits::ToPrimitive;
use std::collections::BTreeMap;
#[derive(Clone, Copy, PartialEq, Eq)]
struct ResidueKey {
chain: SymbolId,
seq: i64,
ins_code: OptionalSymbol,
}
#[derive(Clone, Copy, Default)]
struct Divergence {
ragged: bool,
values: bool,
}
pub(super) struct ReadState<'a> {
options: &'a ReadOptions,
data: StructureData,
builder: ChunkBuilder,
findings: Diagnostics,
frame: CoordinateBlock,
frames: Vec<CoordinateBlock>,
model_number: i32,
model_numbers: Vec<i32>,
model_read: ModelRead,
chain_label: Option<SymbolId>,
chain_first_residue: u32,
residue_key: Option<ResidueKey>,
residue_position: u32,
atom_position: u32,
entity: Option<EntityIndex>,
saw_atoms: bool,
malformed_atom: u64,
topology_locked: bool,
expected_chunk: usize,
later_models: Divergence,
serial_to_atom: BTreeMap<u32, AtomIndex>,
conect: Vec<(u32, u32, Position)>,
links: Vec<LinkRequest>,
link_atoms: LinkAtoms,
key_scratch: String,
anisou: Vec<(u32, [f32; 6])>,
last_atom_serial: Option<u32>,
variant: Format,
partial_charges: Vec<(f64, Presence)>,
radii: Vec<(f64, Presence)>,
autodock_types: Vec<(SymbolId, Presence)>,
headers: PdbHeaders,
branches: usize,
}
impl<'a> ReadState<'a> {
pub(super) fn new(options: &'a ReadOptions, variant: Format) -> Self {
Self {
options,
data: StructureData::empty(),
builder: ChunkBuilder::new(),
findings: Diagnostics::new(),
frame: CoordinateBlock::new(),
frames: Vec::new(),
model_number: 1,
model_numbers: Vec::new(),
model_read: ModelRead::Before,
chain_label: None,
chain_first_residue: 0,
residue_key: None,
residue_position: 0,
atom_position: 0,
entity: None,
saw_atoms: false,
malformed_atom: 0,
topology_locked: false,
expected_chunk: 0,
later_models: Divergence::default(),
serial_to_atom: BTreeMap::new(),
conect: Vec::new(),
links: Vec::new(),
link_atoms: LinkAtoms::new(),
key_scratch: String::new(),
anisou: Vec::new(),
last_atom_serial: None,
variant,
partial_charges: Vec::new(),
radii: Vec::new(),
autodock_types: Vec::new(),
headers: PdbHeaders::default(),
branches: 0,
}
}
pub(super) fn line(&mut self, line: &Line<'_>) {
let record = fixed::record(line.text);
if record == "BRANCH" && self.variant == Format::Pdbqt {
self.branches += 1;
}
if !self.options.categories.keeps_record(record) {
if starts_with_coordinate_keyword(line.text) {
self.malformed_atom += 1;
}
return;
}
self.observe_metadata(record, line);
match record {
"ATOM" | "HETATM" => self.atom(line),
"MODEL" => self.model(line),
"ENDMDL" => self.end_model(),
"TER" => self.close_chain(),
"CRYST1" => self.cell(line),
"HEADER" => self.header(line),
"TITLE" => self.title(line),
"EXPDTA" => self.method(line),
"CONECT" => self.conect(line),
"SSBOND" => self.ssbond(line),
"LINK" => self.link(line),
"ANISOU" => self.anisou(line),
_ if starts_with_coordinate_keyword(line.text) => {
self.malformed_atom += 1;
}
_ => {}
}
}
fn end_model(&mut self) {
self.close_chain();
}
fn anisou(&mut self, line: &Line<'_>) {
let serial = serial_of(line);
let Some(last) = self.last_atom_serial else {
return;
};
if serial != last {
return;
}
let mut components = [0.0_f32; 6];
let columns = [(29, 35), (36, 42), (43, 49), (50, 56), (57, 63), (64, 70)];
for (position, (from, to)) in columns.iter().enumerate() {
let Some(value) = fixed::integer(line.text, *from, *to) else {
return;
};
let Some(scaled) = value.to_f64().and_then(|value| (value / 10_000.0).to_f32()) else {
return;
};
components[position] = scaled;
}
self.anisou.push((last, components));
}
fn atom(&mut self, line: &Line<'_>) {
if matches!(self.model_read, ModelRead::Ignoring) {
return;
}
if matches!(self.model_read, ModelRead::Before) {
self.model_read = ModelRead::Reading;
self.model_numbers.push(self.model_number);
}
let raw_name = fixed::raw(line.text, 13, 16);
let element = self.element_of(line, raw_name);
if self.options.discard_hydrogens && element.is_hydrogen() {
return;
}
let Some(signature) = self.atom_identity(line, raw_name, element) else {
return;
};
if self.topology_locked {
self.later_atom(line, signature);
return;
}
let AtomIdentity {
chain,
seq,
ins_code,
atom_name,
component_id,
alt_id,
element,
..
} = signature;
self.variant_atom(line);
self.begin_chain_if_new(chain);
self.begin_residue_if_new(line, chain, seq, ins_code, component_id);
let Some(position) = self.residue_position.checked_sub(1) else {
self.findings.push(
Diagnostic::new(Code::E1901)
.with_message("atom row could not be assigned to a residue")
.at(ByteSpan::empty(line.at)),
);
return;
};
let residue = ResidueIndex::new(position);
let position = self.position_of(line);
let (occupancy, b_factor) = if self.variant == Format::Pqr {
((1.0, Presence::Unknown), (0.0, Presence::Unknown))
} else {
(
self.optional_real(line, line.occupancy(), "occupancy", 1.0),
self.optional_real(line, line.b_factor(), "B factor", 0.0),
)
};
let alternate_component_id = self.alternate_component_of(line, residue, component_id);
let serial = serial_of(line);
if serial != 0 {
self.serial_to_atom
.entry(serial)
.or_insert(AtomIndex::new(self.atom_position));
self.last_atom_serial = Some(serial);
}
self.builder.push(AtomRecord {
position,
element,
atom_name,
auth_atom_name: OptionalSymbol::some(atom_name),
alternate_component_id,
alt_id,
residue,
occupancy,
b_factor,
formal_charge: if matches!(self.variant, Format::Pqr | Format::Pdbqt) {
(0, Presence::Unknown)
} else {
charge_of(line)
},
atom_site_id: serial,
});
self.note_link_atom(line, AtomIndex::new(self.atom_position), element);
self.atom_position += 1;
self.saw_atoms = true;
}
fn element_of(&mut self, line: &Line<'_>, raw_name: &str) -> Element {
let declared = match line.numbers {
Some(numbers) => numbers.declared,
None => Element::from_symbol(fixed::text(line.text, 77, 78)),
};
if let Some(element) = declared {
return element;
}
let inferred = match self.options.missing_element_policy {
MissingElementPolicy::PreserveUnknown => Element::UNKNOWN,
MissingElementPolicy::InferFromAtomName => Element::infer_from_pdb_atom_name(raw_name),
};
self.findings.push(
Diagnostic::new(Code::W3203)
.at(ByteSpan::empty(line.at))
.with_context("atom name", raw_name.trim())
.with_context("inferred", inferred.symbol()),
);
inferred
}
fn position_of(&mut self, line: &Line<'_>) -> Option<[f32; 3]> {
let [x, y, z] = match line.numbers {
Some(numbers) => numbers.position,
None => [
fixed::real(line.text, 31, 38),
fixed::real(line.text, 39, 46),
fixed::real(line.text, 47, 54),
],
};
if let (Some(x), Some(y), Some(z)) = (x, y, z)
&& let Some(position) = x
.to_f32()
.zip(y.to_f32())
.zip(z.to_f32())
.map(|((x, y), z)| [x, y, z])
{
return Some(position);
}
self.findings.push(
Diagnostic::new(Code::E1202)
.with_message("a coordinate could not be read")
.at(ByteSpan::empty(line.at)),
);
None
}
fn optional_real(
&mut self,
line: &Line<'_>,
value: Option<f64>,
field: &'static str,
when_absent: f32,
) -> (f32, Presence) {
let Some(value) = value else {
return (when_absent, Presence::Unknown);
};
if let Some(value) = value.to_f32() {
return (value, Presence::Present);
}
self.findings.push(
Diagnostic::new(Code::E1202)
.with_message("a numeric atom field exceeds the supported floating-point range")
.with_context("field", field)
.at(ByteSpan::empty(line.at)),
);
(when_absent, Presence::Unknown)
}
fn alternate_component_of(
&mut self,
line: &Line<'_>,
residue: ResidueIndex,
component_id: SymbolId,
) -> OptionalSymbol {
let Some(primary) = self.data.topology.residues.label_comp_id(residue) else {
return OptionalSymbol::NONE;
};
if primary == component_id {
return OptionalSymbol::NONE;
}
self.findings.push(
Diagnostic::new(Code::W3012)
.at(ByteSpan::empty(line.at))
.with_context("component", self.component_text(primary))
.with_context("alternate component", self.component_text(component_id)),
);
OptionalSymbol::some(component_id)
}
}