1use crate::{fixed, hybrid36};
4use molframe_core::{
5 Backpressure, Batch, BatchContinuity, BatchDemand, BatchLease, BatchSource, ChunkId, Code,
6 ContinuityLevel, DatasetId, Diagnostic, Element, ExecutionContext, LogicalRow,
7 MissingElementPolicy, Presence, ReadOptions, SourceBytes, StructureAtomRecord, StructureBatch,
8 StructureBatchBuffer, StructureBatchBuilder, StructureBatchError, StructureBatchPool,
9};
10use num_traits::ToPrimitive;
11
12const RETAINED_BYTES_PER_ROW: usize = 128;
13const DICTIONARY_HEADROOM: usize = 64 * 1024;
14
15#[derive(Debug)]
17pub struct PdbBatchSource<S> {
18 source: S,
19 options: ReadOptions,
20 dataset: DatasetId,
21 next_chunk: ChunkId,
22 next_row: LogicalRow,
23 cursor: u64,
24 window_bytes: usize,
25 model: i32,
26 continuation: ContinuityLevel,
27 finished: bool,
28 pool: StructureBatchPool,
29}
30
31impl<S: SourceBytes> PdbBatchSource<S> {
32 pub fn new(
39 source: S,
40 options: ReadOptions,
41 dataset: DatasetId,
42 first_chunk: ChunkId,
43 first_row: LogicalRow,
44 window_bytes: usize,
45 ) -> Result<Self, StructureBatchError> {
46 if window_bytes == 0 {
47 return Err(StructureBatchError::DemandTooSmall {
48 required: 1,
49 available: 0,
50 });
51 }
52 Ok(Self {
53 source,
54 options,
55 dataset,
56 next_chunk: first_chunk,
57 next_row: first_row,
58 cursor: 0,
59 window_bytes,
60 model: 1,
61 continuation: ContinuityLevel::None,
62 finished: false,
63 pool: StructureBatchPool::new(),
64 })
65 }
66
67 fn row_capacity(demand: BatchDemand) -> Result<u32, StructureBatchError> {
68 let byte_rows =
69 demand.max_bytes.saturating_sub(DICTIONARY_HEADROOM) / RETAINED_BYTES_PER_ROW;
70 let rows = demand.max_rows.min(byte_rows).min(u32::MAX as usize);
71 u32::try_from(rows).map_err(|_| StructureBatchError::DemandTooSmall {
72 required: RETAINED_BYTES_PER_ROW,
73 available: demand.max_bytes,
74 })
75 }
76
77 fn reserve_bytes(capacity: u32, demand: BatchDemand) -> usize {
78 (capacity as usize)
79 .saturating_mul(RETAINED_BYTES_PER_ROW)
80 .saturating_add(DICTIONARY_HEADROOM)
81 .min(demand.max_bytes)
82 }
83
84 fn pull_batches(
85 &mut self,
86 demand: BatchDemand,
87 capacity: u32,
88 reserved: usize,
89 context: &ExecutionContext,
90 ) -> Result<Backpressure<BatchLease<StructureBatch>>, StructureBatchError> {
91 loop {
92 let descriptor = molframe_core::ChunkDescriptor::new(
93 self.dataset,
94 self.next_chunk,
95 self.next_row,
96 capacity,
97 )?;
98 let Some((builder, reservation)) =
99 self.pool
100 .acquire(descriptor, reserved, demand.max_bytes, context)?
101 else {
102 return Ok(Backpressure::Pending);
103 };
104 let requested = self.window_bytes.min(demand.max_bytes.max(1));
105 let length = self.source.len_hint();
106 let window = self.source.window(self.cursor, requested)?;
107 let bytes = window.bytes();
108 if bytes.is_empty() {
109 self.finished = true;
110 return Ok(Backpressure::Finished);
111 }
112 let source_finished = length
113 .is_some_and(|length| window.end().is_ok_and(|window_end| window_end >= length));
114 let complete = complete_prefix(bytes, source_finished);
115 if complete == 0 {
116 let required = bytes.len().saturating_add(1);
117 return Err(StructureBatchError::RecordExceedsBudget {
118 required,
119 available: requested,
120 });
121 }
122 let model_checkpoint = self.model;
123 let parsed = match parse_window(
124 &bytes[..complete],
125 builder,
126 capacity,
127 self.continuation,
128 &self.options,
129 &mut self.model,
130 ) {
131 Ok(parsed) => parsed,
132 Err(error) => {
133 self.model = model_checkpoint;
134 return Err(error);
135 }
136 };
137 if parsed.rows == 0 {
138 self.pool.recycle(parsed.batch, reservation);
139 self.cursor = advance(self.cursor, parsed.consumed)?;
140 if source_finished && parsed.consumed == complete {
141 self.finished = true;
142 return Ok(Backpressure::Finished);
143 }
144 continue;
145 }
146 let batch = parsed.batch;
147 if !demand.accepts(batch.batch()) {
148 self.model = model_checkpoint;
149 let required = batch.batch().retained_bytes();
150 self.pool.recycle(batch, reservation);
151 return Err(StructureBatchError::DemandTooSmall {
152 required,
153 available: demand.max_bytes,
154 });
155 }
156 let next_cursor = advance(self.cursor, parsed.consumed)?;
157 let (next_row, next_chunk) =
158 match next_identity(self.next_row, self.next_chunk, parsed.rows) {
159 Ok(identity) => identity,
160 Err(error) => {
161 self.model = model_checkpoint;
162 self.pool.recycle(batch, reservation);
163 return Err(error);
164 }
165 };
166 let continuation = if parsed.ended_for_demand {
167 ContinuityLevel::Residue
168 } else {
169 ContinuityLevel::None
170 };
171 let lease = match self.pool.lease(batch, reservation) {
172 Ok(lease) => lease,
173 Err(error) => {
174 self.model = model_checkpoint;
175 return Err(error);
176 }
177 };
178 self.cursor = next_cursor;
179 self.next_row = next_row;
180 self.next_chunk = next_chunk;
181 self.continuation = continuation;
182 return Ok(Backpressure::Ready(lease));
183 }
184 }
185}
186
187impl<S: SourceBytes> BatchSource for PdbBatchSource<S> {
188 type Batch = StructureBatch;
189 type Error = StructureBatchError;
190
191 fn next_batch(
192 &mut self,
193 demand: BatchDemand,
194 context: &ExecutionContext,
195 ) -> Result<Backpressure<BatchLease<Self::Batch>>, Self::Error> {
196 if self.finished {
197 return Ok(Backpressure::Finished);
198 }
199 if !demand.can_accept_work() {
200 return Ok(Backpressure::Pending);
201 }
202 let capacity = Self::row_capacity(demand)?;
203 if capacity == 0 {
204 return Err(StructureBatchError::DemandTooSmall {
205 required: RETAINED_BYTES_PER_ROW + DICTIONARY_HEADROOM,
206 available: demand.max_bytes,
207 });
208 }
209 self.pull_batches(
210 demand,
211 capacity,
212 Self::reserve_bytes(capacity, demand),
213 context,
214 )
215 }
216}
217
218fn next_identity(
219 row: LogicalRow,
220 chunk: ChunkId,
221 rows: u32,
222) -> Result<(LogicalRow, ChunkId), StructureBatchError> {
223 let row = row
224 .get()
225 .checked_add(u64::from(rows))
226 .map(LogicalRow::new)
227 .ok_or_else(identity_overflow)?;
228 let chunk = chunk.next().ok_or_else(identity_overflow)?;
229 Ok((row, chunk))
230}
231
232struct ParsedWindow {
233 batch: StructureBatchBuffer,
234 consumed: usize,
235 rows: u32,
236 ended_for_demand: bool,
237}
238
239fn parse_window(
240 bytes: &[u8],
241 mut builder: StructureBatchBuilder,
242 capacity: u32,
243 continuity: ContinuityLevel,
244 options: &ReadOptions,
245 model: &mut i32,
246) -> Result<ParsedWindow, StructureBatchError> {
247 let mut consumed = 0usize;
248 let mut rows = 0u32;
249 for raw in bytes.split_inclusive(|byte| *byte == b'\n') {
250 let line_bytes = match raw.strip_suffix(b"\n") {
251 Some(value) => value,
252 None => raw,
253 };
254 let line_bytes = match line_bytes.strip_suffix(b"\r") {
255 Some(value) => value,
256 None => line_bytes,
257 };
258 let line = std::str::from_utf8(line_bytes)
259 .map_err(|_| Diagnostic::new(Code::E1201).with_message("input is not valid text"))?;
260 let record = fixed::record(line);
261 if record == "MODEL" {
262 *model = model_number_or_one(
263 fixed::integer(line, 11, 14).and_then(|value| i32::try_from(value).ok()),
264 );
265 } else if matches!(record, "ATOM" | "HETATM") {
266 if rows == capacity {
267 break;
268 }
269 builder.push(atom_record(line, *model, options))?;
270 rows += 1;
271 }
272 consumed = consumed.saturating_add(raw.len());
273 }
274 let ended_for_demand = rows == capacity && consumed < bytes.len();
275 builder.set_continuity(BatchContinuity {
276 before: continuity,
277 after: if ended_for_demand {
278 ContinuityLevel::Residue
279 } else {
280 ContinuityLevel::None
281 },
282 });
283 Ok(ParsedWindow {
284 batch: builder.finish()?,
285 consumed,
286 rows,
287 ended_for_demand,
288 })
289}
290
291fn atom_record<'a>(line: &'a str, model: i32, options: &ReadOptions) -> StructureAtomRecord<'a> {
292 let raw_atom = fixed::raw(line, 13, 16);
293 let declared = fixed::text(line, 77, 78);
294 let element = match Element::from_symbol(declared) {
295 Some(element) => element,
296 None => match options.missing_element_policy {
297 MissingElementPolicy::PreserveUnknown => Element::UNKNOWN,
298 MissingElementPolicy::InferFromAtomName => Element::infer_from_pdb_atom_name(raw_atom),
299 },
300 };
301 let position = fixed::real(line, 31, 38)
302 .and_then(|value| value.to_f32())
303 .zip(fixed::real(line, 39, 46).and_then(|value| value.to_f32()))
304 .zip(fixed::real(line, 47, 54).and_then(|value| value.to_f32()))
305 .map(|((x, y), z)| [x, y, z]);
306 let sequence =
307 hybrid36::decode(fixed::raw(line, 23, 26), 4).and_then(|value| i32::try_from(value).ok());
308 let atom_site_id =
309 hybrid36::decode(fixed::raw(line, 7, 11), 5).and_then(|value| u32::try_from(value).ok());
310 StructureAtomRecord {
311 model,
312 chain: fixed::text(line, 22, 22),
313 component: fixed::text(line, 18, 20),
314 sequence: value_or_min(sequence),
315 insertion: fixed::text(line, 27, 27),
316 atom: raw_atom.trim(),
317 alternate: fixed::text(line, 17, 17),
318 element,
319 position,
320 occupancy: optional_real(line, 55, 60, 1.0),
321 b_factor: optional_real(line, 61, 66, 0.0),
322 formal_charge: formal_charge(fixed::text(line, 79, 80)),
323 atom_site_id: identifier_or_zero(atom_site_id),
324 heterogen: fixed::record(line) == "HETATM",
325 }
326}
327
328fn value_or_min(value: Option<i32>) -> i32 {
329 let Some(value) = value else {
330 return i32::MIN;
331 };
332 value
333}
334
335fn model_number_or_one(value: Option<i32>) -> i32 {
336 let Some(value) = value else {
337 return 1;
338 };
339 value
340}
341
342fn identifier_or_zero(value: Option<u32>) -> u32 {
343 let Some(value) = value else {
344 return 0;
345 };
346 value
347}
348
349fn optional_real(line: &str, start: usize, end: usize, absent: f32) -> (f32, Presence) {
350 match fixed::real(line, start, end).and_then(|value| value.to_f32()) {
351 Some(value) => (value, Presence::Present),
352 None => (absent, Presence::Unknown),
353 }
354}
355
356fn formal_charge(text: &str) -> (i8, Presence) {
357 let bytes = text.as_bytes();
358 if bytes.len() != 2 || !bytes[0].is_ascii_digit() {
359 return (0, Presence::Inapplicable);
360 }
361 let magnitude = (bytes[0] - b'0').cast_signed();
362 match bytes[1] {
363 b'+' => (magnitude, Presence::Present),
364 b'-' => (-magnitude, Presence::Present),
365 _ => (0, Presence::Unknown),
366 }
367}
368
369fn complete_prefix(bytes: &[u8], source_finished: bool) -> usize {
370 if source_finished {
371 return bytes.len();
372 }
373 bytes
374 .iter()
375 .rposition(|byte| *byte == b'\n')
376 .map_or(0, |position| position + 1)
377}
378
379fn advance(start: u64, count: usize) -> Result<u64, StructureBatchError> {
380 let count = u64::try_from(count).map_err(|_| identity_overflow())?;
381 start.checked_add(count).ok_or_else(identity_overflow)
382}
383
384fn identity_overflow() -> StructureBatchError {
385 StructureBatchError::Diagnostic(
386 Diagnostic::new(Code::E1903).with_message("PDB batch identity exceeds u64"),
387 )
388}
389
390#[cfg(test)]
391#[path = "batch_tests.rs"]
392mod tests;