1use super::capacity::{RequiredAtomFields, check_capacity, required_text, required_value};
14use crate::hybrid36;
15use crate::{PDB_HEADERS_EXTENSION, PdbHeaders};
16use molframe_core::diagnostic::Diagnostic;
17use molframe_core::io::{Select, SelectAll};
18use molframe_core::structure::{AtomRef, ChainRef, ResidueRef, Structure};
19use std::collections::HashMap;
20use std::fmt;
21
22mod identity;
23use super::columns::terminus_fields;
24pub(crate) use super::columns::{atom_name_field, element_column};
25pub use identity::PdbIdentifierNamespace;
26pub(crate) use identity::{atom_name, chain_name, component_name, residue_sequence};
27
28#[derive(Clone, Debug)]
30pub struct PdbOptions {
31 chain_map: HashMap<Box<str>, Box<str>>,
34 hybrid36: bool,
39 namespace: PdbIdentifierNamespace,
41 space_group: Option<Box<str>>,
44 extra_records: Option<Box<str>>,
47}
48
49impl Default for PdbOptions {
50 fn default() -> Self {
51 Self {
52 chain_map: HashMap::new(),
53 hybrid36: false,
54 namespace: PdbIdentifierNamespace::Label,
55 space_group: None,
56 extra_records: None,
57 }
58 }
59}
60
61impl PdbOptions {
62 #[must_use]
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 #[must_use]
70 pub fn chain_map(mut self, from: impl Into<Box<str>>, to: impl Into<Box<str>>) -> Self {
71 self.chain_map.insert(from.into(), to.into());
72 self
73 }
74
75 #[must_use]
77 pub const fn hybrid36(mut self, enabled: bool) -> Self {
78 self.hybrid36 = enabled;
79 self
80 }
81
82 #[must_use]
84 pub const fn namespace(mut self, namespace: PdbIdentifierNamespace) -> Self {
85 self.namespace = namespace;
86 self
87 }
88
89 #[must_use]
91 pub fn with_space_group(mut self, symbol: impl Into<Box<str>>) -> Self {
92 self.space_group = Some(symbol.into());
93 self
94 }
95
96 #[must_use]
98 pub fn with_extra_records(mut self, records: impl Into<Box<str>>) -> Self {
99 self.extra_records = Some(records.into());
100 self
101 }
102
103 #[must_use]
105 pub fn extra_records(&self) -> Option<&str> {
106 self.extra_records.as_deref()
107 }
108
109 pub(crate) const fn identifier_namespace(&self) -> PdbIdentifierNamespace {
110 self.namespace
111 }
112
113 pub(crate) fn space_group_fallback(&self) -> Option<&str> {
114 self.space_group.as_deref()
115 }
116
117 pub(crate) const fn uses_hybrid36(&self) -> bool {
118 self.hybrid36
119 }
120
121 pub(crate) fn label_for<'a>(&'a self, label: &'a str) -> &'a str {
123 match self.chain_map.get(label) {
124 Some(replacement) => replacement,
125 None => label,
126 }
127 }
128}
129
130pub fn write(structure: &Structure, options: &PdbOptions) -> Result<String, Vec<Diagnostic>> {
137 write_selected(structure, options, &SelectAll)
138}
139
140pub fn write_selected(
146 structure: &Structure,
147 options: &PdbOptions,
148 select: &impl Select,
149) -> Result<String, Vec<Diagnostic>> {
150 let mut out = String::with_capacity(structure.atom_count() as usize * 81);
151 render_selected(&mut out, structure, options, select)?;
152 Ok(out)
153}
154
155pub(super) fn render_selected(
156 out: &mut impl fmt::Write,
157 structure: &Structure,
158 options: &PdbOptions,
159 select: &impl Select,
160) -> Result<(), Vec<Diagnostic>> {
161 let mut refusals = check_capacity(structure, options, select, RequiredAtomFields::Pdb);
162 refusals.extend(super::secondary::preflight(structure, options, select));
163 if !refusals.is_empty() {
164 return Err(refusals);
165 }
166
167 let data = structure.data();
168
169 let mut has_ssbond = false;
170 let mut has_link = false;
171 if let Some(headers) = data.extensions.get::<PdbHeaders>(PDB_HEADERS_EXTENSION) {
172 has_ssbond = headers
173 .records()
174 .iter()
175 .any(|record| record.name() == "SSBOND");
176 has_link = headers
177 .records()
178 .iter()
179 .any(|record| record.name() == "LINK");
180 for record in headers.records() {
181 if matches!(record.name(), "HELIX" | "SHEET")
182 && structure
183 .secondary_structure()
184 .iter()
185 .any(|state| state.is_helix() || state.is_strand())
186 {
187 continue;
188 }
189 let _ = writeln!(out, "{}", record.line());
190 }
191 } else {
192 super::metadata::write_generated(out, structure, options);
193 }
194 super::secondary::write(out, structure, options, select);
195 super::connections::write_links(out, structure, options, select, has_ssbond, has_link);
196
197 let track = super::connections::has_connect(structure);
198 let mut serials: HashMap<u32, i64> = HashMap::new();
199 let count = structure.model_count();
200 if count <= 1 {
201 write_model(
202 out,
203 structure,
204 None,
205 options,
206 select,
207 track.then_some(&mut serials),
208 )?;
209 } else {
210 let mut written = 0usize;
211 for number in 0..count {
212 let Ok(index) = u32::try_from(number) else {
213 break;
214 };
215 let model = molframe_core::index::ModelIndex::new(index);
216 if !select.accept_model(model) {
217 continue;
218 }
219 let Some((snapshot, local)) = structure.model_snapshot(model) else {
220 continue;
221 };
222 if structure.is_ragged_ensemble() && written > 0 {
226 super::models::refuse_foreign_filter(structure, select)?;
227 }
228 written += 1;
229 let frame = if structure.is_ragged_ensemble() {
232 let refusals = check_capacity(&snapshot, options, select, RequiredAtomFields::Pdb);
233 if !refusals.is_empty() {
234 return Err(refusals);
235 }
236 None
237 } else {
238 let frame = structure.model_positions(local);
239 if let Some(frame) = frame {
240 super::models::check_frame(&snapshot, frame, number, select)?;
241 }
242 frame
243 };
244 let _ = writeln!(out, "MODEL {:>4}", number + 1);
245 let tracked = (number == 0 && track).then_some(&mut serials);
246 write_model(out, &snapshot, frame, options, select, tracked)?;
247 let _ = writeln!(out, "ENDMDL");
248 }
249 }
250 super::connections::write_connect(out, structure, options, select, &serials);
251 let _ = out.write_str("END\n");
252 Ok(())
253}
254
255fn write_model(
257 out: &mut impl fmt::Write,
258 structure: &Structure,
259 frame: Option<&[[f32; 3]]>,
260 options: &PdbOptions,
261 select: &impl Select,
262 mut serials: Option<&mut HashMap<u32, i64>>,
263) -> Result<(), Vec<Diagnostic>> {
264 let mut serial = 1i64;
265 for chain in structure.data().chains() {
266 if !select.accept_chain(chain.index()) {
267 continue;
268 }
269 let label = chain_label(&chain, options.namespace);
270 let label = options.label_for(label);
271 let mut polymer = false;
272 let mut terminus: Option<String> = None;
273
274 for residue in chain.residues() {
275 if !select.accept_residue(residue.index()) {
276 continue;
277 }
278 if !residue.is_het() {
279 terminus = terminus_fields(&residue, label, options);
280 polymer = true;
281 }
282 for atom in residue.atoms() {
283 if !select.accept_atom(atom.index()) {
284 continue;
285 }
286 let position = match frame {
287 Some(frame) => atom
288 .position()
289 .and(frame.get(atom.index().as_usize()).copied()),
290 None => atom.position(),
291 };
292 let placement = Placement {
293 chain: label,
294 serial,
295 position,
296 };
297 if let Err(finding) =
298 write_atom(out, structure, &atom, &residue, &placement, options)
299 {
300 return Err(vec![finding]);
301 }
302 if let Some(serials) = serials.as_deref_mut() {
303 serials.insert(atom.index().get(), serial);
304 }
305 serial += 1;
306 }
307 }
308 if polymer {
310 let terminus = match terminus.as_deref() {
311 Some(text) => text,
312 None => "",
313 };
314 let _ = writeln!(out, "TER {}{}", serial_field(serial, options), terminus);
315 serial += 1;
316 }
317 }
318 Ok(())
319}
320
321struct Placement<'a> {
323 chain: &'a str,
324 serial: i64,
325 position: Option<[f32; 3]>,
326}
327fn charge_field(charge: Option<i8>) -> String {
329 match charge {
330 Some(value) if value > 0 => format!("{value}+"),
331 Some(value) if value < 0 => format!("{}-", value.unsigned_abs()),
332 _ => String::new(),
333 }
334}
335
336fn write_atom(
337 out: &mut impl fmt::Write,
338 structure: &Structure,
339 atom: &AtomRef<'_>,
340 residue: &ResidueRef<'_>,
341 placement: &Placement<'_>,
342 options: &PdbOptions,
343) -> Result<(), Diagnostic> {
344 let Placement {
345 chain,
346 serial,
347 position,
348 } = *placement;
349 let record = if residue.is_het() { "HETATM" } else { "ATOM " };
350 let name = required_text(
351 atom_name(*atom, options.namespace),
352 "atom identifier",
353 *atom,
354 options,
355 )?;
356 let element = element_column(atom);
357 let position = required_value(position, "coordinates", *atom, options)?;
361 let seq = i64::from(required_value(
362 residue_sequence(*residue, options.namespace),
363 "residue sequence identifier",
364 *atom,
365 options,
366 )?);
367 let component = required_text(
368 component_name(*atom, *residue, options.namespace),
369 "component identifier",
370 *atom,
371 options,
372 )?;
373 let occupancy = required_value(atom.occupancy(), "occupancy", *atom, options)?;
374 let b_factor = required_value(atom.b_factor(), "B factor", *atom, options)?;
375
376 let _ = writeln!(
377 out,
378 "{record}{serial:>5} {name:<4}{alt:1}{comp:>3} {chain:>1}{seq}{ins:1} \
379 {x:8.3}{y:8.3}{z:8.3}{occ:6.2}{b:6.2} {element:>2}{charge}",
380 serial = serial_field(serial, options),
381 name = atom_name_field(name, &element),
382 alt = alt_field(structure, atom),
383 comp = component,
384 seq = residue_field(seq, options),
385 ins = insertion_code(*residue),
386 x = f64::from(position[0]),
387 y = f64::from(position[1]),
388 z = f64::from(position[2]),
389 occ = f64::from(occupancy),
390 b = f64::from(b_factor),
391 charge = charge_field(atom.formal_charge()),
392 );
393 if let Some(u) = structure.data().anisotropy.for_atom(atom.index()) {
396 super::aniso::Record {
397 atom,
398 structure,
399 residue,
400 name,
401 component,
402 chain,
403 seq: &residue_field(seq, options),
404 serial: &serial_field(serial, options),
405 u: &u,
406 }
407 .write(out);
408 }
409 Ok(())
410}
411
412pub(crate) fn alt_field<'a>(structure: &'a Structure, atom: &AtomRef<'a>) -> &'a str {
417 let Some(alt) = atom.alt_id() else { return " " };
418 let Some(symbol) = alt.symbol() else {
419 return " ";
420 };
421 match structure.resolve(symbol) {
422 Some(label) => label,
423 None => " ",
424 }
425}
426
427pub(crate) fn serial_field(serial: i64, options: &PdbOptions) -> String {
428 match options
429 .hybrid36
430 .then(|| hybrid36::encode(serial, 5))
431 .flatten()
432 {
433 Some(field) => field,
434 None => format!("{serial:>5}"),
435 }
436}
437
438pub(crate) fn residue_field(seq: i64, options: &PdbOptions) -> String {
439 match options.hybrid36.then(|| hybrid36::encode(seq, 4)).flatten() {
440 Some(field) => field,
441 None => format!("{seq:>4}"),
442 }
443}
444
445pub(crate) fn chain_label<'a>(
446 chain: &'a ChainRef<'a>,
447 namespace: PdbIdentifierNamespace,
448) -> &'a str {
449 let Some(name) = chain_name(*chain, namespace) else {
450 return "";
451 };
452 name
453}
454
455pub(crate) fn insertion_code(residue: ResidueRef<'_>) -> &str {
456 let Some(code) = residue.ins_code() else {
457 return " ";
458 };
459 code
460}
461
462#[cfg(test)]
463#[path = "pdb_tests.rs"]
464mod tests;