1use super::atom::{AtomChunk, AtomRecord};
9use super::parent::ParentMapping;
10use super::stats::AtomChunkStats;
11use crate::column::{EncodedColumn, Presence, ValidityMask};
12use crate::coords::CoordinateBlock;
13use crate::symbol::SymbolId;
14
15pub const TARGET_CHUNK_ATOMS: u32 = 4096;
21
22#[derive(Debug, Default)]
55pub struct ChunkBuilder {
56 chunks: Vec<AtomChunk>,
57 coords: CoordinateBlock,
58 target: u32,
59 model: u32,
60 pending: Pending,
61}
62
63#[derive(Debug, Default)]
65struct Pending {
66 first_atom: u32,
67 element: Vec<u8>,
68 atom_name: Vec<SymbolId>,
69 auth_atom_name: Vec<u32>,
70 has_auth_names: bool,
71 alternate_component_id: Vec<u32>,
72 has_alternate_components: bool,
73 alt_id: Vec<u32>,
74 residue: Vec<u32>,
75 occupancy: Vec<f32>,
76 occupancy_presence: Vec<Presence>,
77 b_factor: Vec<f32>,
78 b_factor_presence: Vec<Presence>,
79 formal_charge: Vec<i8>,
80 formal_charge_presence: Vec<Presence>,
81 atom_site_id: Vec<u32>,
82 coord_presence: Vec<Presence>,
83 stats: AtomChunkStats,
84}
85
86impl Pending {
87 fn len(&self) -> usize {
88 self.element.len()
89 }
90
91 fn is_empty(&self) -> bool {
92 self.element.is_empty()
93 }
94
95 fn last_residue(&self) -> Option<u32> {
96 self.residue.last().copied()
97 }
98
99 fn starts_new_residue(&self, residue: u32) -> bool {
100 self.last_residue() != Some(residue)
101 }
102
103 fn should_close_before(&self, target: u32, residue: u32) -> bool {
104 self.len() as u64 >= u64::from(target) && self.starts_new_residue(residue)
105 }
106
107 fn begin(&mut self, first_atom: u32, model: u32) {
108 if !self.is_empty() {
109 return;
110 }
111
112 self.first_atom = first_atom;
113 self.stats.model = model;
114 }
115
116 fn push_record(&mut self, record: &AtomRecord) {
117 self.element.push(record.element.atomic_number());
118 self.atom_name.push(record.atom_name);
119 self.push_auth_atom_name(record);
120 self.push_alternate_component(record);
121 self.alt_id.push(record.alt_id.get());
122 self.residue.push(record.residue.get());
123
124 push_with_presence(
125 &mut self.occupancy,
126 &mut self.occupancy_presence,
127 record.occupancy,
128 );
129
130 push_with_presence(
131 &mut self.b_factor,
132 &mut self.b_factor_presence,
133 record.b_factor,
134 );
135
136 push_with_presence(
137 &mut self.formal_charge,
138 &mut self.formal_charge_presence,
139 record.formal_charge,
140 );
141
142 self.atom_site_id.push(record.atom_site_id);
143 self.observe(record);
144 }
145
146 fn push_auth_atom_name(&mut self, record: &AtomRecord) {
147 match record.auth_atom_name.get() {
148 Some(name) => {
149 self.auth_atom_name.push(name.get());
150 self.has_auth_names = true;
151 }
152 None => self.auth_atom_name.push(u32::MAX),
153 }
154 }
155
156 fn push_alternate_component(&mut self, record: &AtomRecord) {
157 match record.alternate_component_id.get() {
158 Some(component) => {
159 self.alternate_component_id.push(component.get());
160 self.has_alternate_components = true;
161 }
162 None => self.alternate_component_id.push(u32::MAX),
163 }
164 }
165
166 fn observe(&mut self, record: &AtomRecord) {
167 self.stats.elements.insert(record.element);
168 self.stats.has_hydrogen |= record.element.is_hydrogen();
169 self.stats.has_altloc |= !record.alt_id.is_blank();
170
171 if record.occupancy.1.is_present() {
172 self.stats.occupancy.observe(record.occupancy.0);
173 }
174
175 if record.b_factor.1.is_present() {
176 self.stats.b_factor.observe(record.b_factor.0);
177 }
178
179 self.observe_residue(record.residue.get());
180 }
181
182 fn observe_residue(&mut self, residue: u32) {
183 if self.residue.len() == 1 {
184 self.stats.residue_min = residue;
185 self.stats.residue_max = residue;
186 return;
187 }
188
189 self.stats.residue_min = self.stats.residue_min.min(residue);
190 self.stats.residue_max = self.stats.residue_max.max(residue);
191 }
192
193 fn reset(&mut self) {
194 self.first_atom = 0;
195 self.has_auth_names = false;
196 self.has_alternate_components = false;
197
198 self.element.clear();
199 self.atom_name.clear();
200 self.auth_atom_name.clear();
201 self.alternate_component_id.clear();
202 self.alt_id.clear();
203 self.residue.clear();
204 self.occupancy.clear();
205 self.occupancy_presence.clear();
206 self.b_factor.clear();
207 self.b_factor_presence.clear();
208 self.formal_charge.clear();
209 self.formal_charge_presence.clear();
210 self.atom_site_id.clear();
211 self.coord_presence.clear();
212
213 self.stats = AtomChunkStats::default();
214 }
215}
216
217impl ChunkBuilder {
218 #[must_use]
220 pub fn new() -> Self {
221 Self {
222 target: TARGET_CHUNK_ATOMS,
223 ..Self::default()
224 }
225 }
226
227 #[must_use]
232 pub fn with_target(target: u32) -> Self {
233 Self {
234 target: target.max(1),
235 ..Self::default()
236 }
237 }
238
239 pub fn reserve(&mut self, atoms: usize) {
241 self.coords = CoordinateBlock::with_capacity(atoms);
242 }
243
244 pub fn start_model(&mut self, model: u32) {
249 self.close();
250 self.model = model;
251 }
252
253 pub fn push(&mut self, record: AtomRecord) {
255 self.close_before(&record);
256 self.pending.begin(self.coords.len(), self.model);
257 self.push_position(record.position);
258 self.pending.push_record(&record);
259 }
260
261 fn close_before(&mut self, record: &AtomRecord) {
262 if self
263 .pending
264 .should_close_before(self.target, record.residue.get())
265 {
266 self.close();
267 }
268 }
269
270 fn push_position(&mut self, position: Option<[f32; 3]>) {
271 if let Some(position) = position {
272 self.coords.push(position);
273 self.pending.stats.bounds.extend(position);
274 self.pending.coord_presence.push(Presence::Present);
275 } else {
276 self.coords.push([f32::NAN; 3]);
280 self.pending.coord_presence.push(Presence::Unknown);
281 self.pending.stats.has_missing_coords = true;
282 }
283 }
284
285 fn close(&mut self) {
287 if self.pending.is_empty() {
288 return;
289 }
290
291 let chunk = self.build_chunk();
292 self.chunks.push(chunk);
293 self.pending.reset();
294 }
295
296 fn build_chunk(&mut self) -> AtomChunk {
297 let end = self.coords.len();
298 let pending = &mut self.pending;
299 let first = pending.first_atom;
300 let len = end - first;
301
302 let occupancy_validity = compact_validity(&pending.occupancy_presence, len);
303 let b_factor_validity = compact_validity(&pending.b_factor_presence, len);
304 let formal_charge_validity = compact_validity(&pending.formal_charge_presence, len);
305 let coord_validity = compact_validity(&pending.coord_presence, len);
306
307 let element = EncodedColumn::encode(&pending.element);
308
309 let atom_name = EncodedColumn::plain(&pending.atom_name);
311
312 let auth_atom_name = pending
313 .has_auth_names
314 .then(|| EncodedColumn::plain(&pending.auth_atom_name));
315 let alternate_component_id = pending
316 .has_alternate_components
317 .then(|| EncodedColumn::encode(&pending.alternate_component_id));
318
319 let alt_id = EncodedColumn::encode(&pending.alt_id);
320 let residue = ParentMapping::block_indexed(&pending.residue);
321 let occupancy = EncodedColumn::plain(&pending.occupancy);
322 let b_factor = EncodedColumn::plain(&pending.b_factor);
323 let formal_charge = EncodedColumn::encode(&pending.formal_charge);
324 let atom_site_id = EncodedColumn::encode(&pending.atom_site_id);
325 let stats = std::mem::take(&mut pending.stats);
326
327 AtomChunk {
328 atoms: first..end,
329 model: self.model,
330 element,
331 atom_name,
332 auth_atom_name,
333 alternate_component_id,
334 alt_id,
335 residue,
336 occupancy,
337 occupancy_validity,
338 b_factor,
339 b_factor_validity,
340 formal_charge,
341 formal_charge_validity,
342 atom_site_id,
343 coord_validity,
344 stats,
345 }
346 }
347
348 #[must_use]
350 pub fn finish(mut self) -> (Vec<AtomChunk>, CoordinateBlock) {
351 self.close();
352 (self.chunks, self.coords)
353 }
354}
355
356fn push_with_presence<T>(values: &mut Vec<T>, presences: &mut Vec<Presence>, entry: (T, Presence)) {
357 let (value, presence) = entry;
358 values.push(value);
359 presences.push(presence);
360}
361
362fn compact_validity(presences: &[Presence], len: u32) -> ValidityMask {
363 ValidityMask::from_bounded_iter(presences.iter().copied(), len)
364}
365
366#[cfg(test)]
367#[path = "builder_tests.rs"]
368mod tests;