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
22#[path = "identity.rs"]
23mod identity;
24pub use identity::PdbIdentifierNamespace;
25pub(crate) use identity::{atom_name, chain_name, component_name, residue_sequence};
26
27#[derive(Clone, Debug)]
29pub struct PdbOptions {
30 chain_map: HashMap<Box<str>, Box<str>>,
33 hybrid36: bool,
38 namespace: PdbIdentifierNamespace,
40}
41
42impl Default for PdbOptions {
43 fn default() -> Self {
44 Self {
45 chain_map: HashMap::new(),
46 hybrid36: false,
47 namespace: PdbIdentifierNamespace::Label,
48 }
49 }
50}
51
52impl PdbOptions {
53 #[must_use]
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 #[must_use]
61 pub fn chain_map(mut self, from: impl Into<Box<str>>, to: impl Into<Box<str>>) -> Self {
62 self.chain_map.insert(from.into(), to.into());
63 self
64 }
65
66 #[must_use]
68 pub const fn hybrid36(mut self, enabled: bool) -> Self {
69 self.hybrid36 = enabled;
70 self
71 }
72
73 #[must_use]
75 pub const fn namespace(mut self, namespace: PdbIdentifierNamespace) -> Self {
76 self.namespace = namespace;
77 self
78 }
79
80 pub(crate) const fn identifier_namespace(&self) -> PdbIdentifierNamespace {
81 self.namespace
82 }
83
84 pub(crate) const fn uses_hybrid36(&self) -> bool {
85 self.hybrid36
86 }
87
88 pub(crate) fn label_for<'a>(&'a self, label: &'a str) -> &'a str {
90 match self.chain_map.get(label) {
91 Some(replacement) => replacement,
92 None => label,
93 }
94 }
95}
96
97pub fn write(structure: &Structure, options: &PdbOptions) -> Result<String, Vec<Diagnostic>> {
104 write_selected(structure, options, &SelectAll)
105}
106
107pub fn write_selected(
113 structure: &Structure,
114 options: &PdbOptions,
115 select: &impl Select,
116) -> Result<String, Vec<Diagnostic>> {
117 let mut out = String::with_capacity(structure.atom_count() as usize * 81);
118 render_selected(&mut out, structure, options, select)?;
119 Ok(out)
120}
121
122pub(super) fn render_selected(
123 out: &mut impl fmt::Write,
124 structure: &Structure,
125 options: &PdbOptions,
126 select: &impl Select,
127) -> Result<(), Vec<Diagnostic>> {
128 let mut refusals = check_capacity(structure, options, select, RequiredAtomFields::Pdb);
129 refusals.extend(super::secondary::preflight(structure, options, select));
130 if !refusals.is_empty() {
131 return Err(refusals);
132 }
133
134 let data = structure.data();
135
136 if let Some(headers) = data.extensions.get::<PdbHeaders>(PDB_HEADERS_EXTENSION) {
137 for record in headers.records() {
138 if matches!(record.name(), "HELIX" | "SHEET")
139 && structure
140 .secondary_structure()
141 .iter()
142 .any(|state| state.is_helix() || state.is_strand())
143 {
144 continue;
145 }
146 let _ = writeln!(out, "{}", record.line());
147 }
148 } else {
149 write_generated_metadata(out, structure);
150 }
151 super::secondary::write(out, structure, options, select);
152
153 let mut serial = 1i64;
154 for chain in data.chains() {
155 if !select.accept_chain(chain.index()) {
156 continue;
157 }
158 let label = chain_label(&chain, options.namespace);
159 let label = options.label_for(label);
160
161 for residue in chain.residues() {
162 if !select.accept_residue(residue.index()) {
163 continue;
164 }
165 for atom in residue.atoms() {
166 if !select.accept_atom(atom.index()) {
167 continue;
168 }
169 if let Err(finding) =
170 write_atom(out, structure, &atom, &residue, label, serial, options)
171 {
172 return Err(vec![finding]);
173 }
174 serial += 1;
175 }
176 }
177 let _ = writeln!(out, "TER {:>5}", serial_field(serial, options));
178 serial += 1;
179 }
180 let _ = out.write_str("END\n");
181 Ok(())
182}
183
184fn write_generated_metadata(out: &mut impl fmt::Write, structure: &Structure) {
185 let data = structure.data();
186 if let Some(id) = &data.entry.id {
187 let _ = writeln!(out, "HEADER {:<40}{:<9} {:>4}", "", "", id);
188 }
189 if let Some(title) = &data.entry.title {
190 let _ = writeln!(out, "TITLE {title}");
191 }
192 if let Some(method) = &data.entry.method {
193 let _ = writeln!(out, "EXPDTA {method}");
194 }
195 if let Some(cell) = data.cell {
196 let _ = writeln!(
197 out,
198 "CRYST1{:9.3}{:9.3}{:9.3}{:7.2}{:7.2}{:7.2} {:<11}",
199 cell.lengths[0],
200 cell.lengths[1],
201 cell.lengths[2],
202 cell.angles[0],
203 cell.angles[1],
204 cell.angles[2],
205 match data.entry.space_group.as_deref() {
207 Some(symbol) => symbol,
208 None => "P 1",
209 },
210 );
211 }
212}
213
214fn write_atom(
215 out: &mut impl fmt::Write,
216 structure: &Structure,
217 atom: &AtomRef<'_>,
218 residue: &ResidueRef<'_>,
219 chain: &str,
220 serial: i64,
221 options: &PdbOptions,
222) -> Result<(), Diagnostic> {
223 let record = if residue.is_het() { "HETATM" } else { "ATOM " };
224 let name = required_text(
225 atom_name(*atom, options.namespace),
226 "atom identifier",
227 *atom,
228 options,
229 )?;
230 let element = match atom.element() {
231 Some(element) => element.symbol(),
232 None => "",
233 };
234 let position = required_value(atom.position(), "coordinates", *atom, options)?;
238 let seq = i64::from(required_value(
239 residue_sequence(*residue, options.namespace),
240 "residue sequence identifier",
241 *atom,
242 options,
243 )?);
244 let component = required_text(
245 component_name(*atom, *residue, options.namespace),
246 "component identifier",
247 *atom,
248 options,
249 )?;
250 let occupancy = required_value(atom.occupancy(), "occupancy", *atom, options)?;
251 let b_factor = required_value(atom.b_factor(), "B factor", *atom, options)?;
252
253 let _ = writeln!(
254 out,
255 "{record}{serial:>5} {name:<4}{alt:1}{comp:>3} {chain:>1}{seq}{ins:1} \
256 {x:8.3}{y:8.3}{z:8.3}{occ:6.2}{b:6.2} {element:>2}",
257 serial = serial_field(serial, options),
258 name = atom_name_field(name),
259 alt = alt_field(structure, atom),
260 comp = component,
261 seq = residue_field(seq, options),
262 ins = insertion_code(*residue),
263 x = f64::from(position[0]),
264 y = f64::from(position[1]),
265 z = f64::from(position[2]),
266 occ = f64::from(occupancy),
267 b = f64::from(b_factor),
268 );
269 if let Some(u) = structure.data().anisotropy.for_atom(atom.index()) {
272 super::aniso::Record {
273 atom,
274 structure,
275 residue,
276 name,
277 component,
278 chain,
279 seq: &residue_field(seq, options),
280 serial: &serial_field(serial, options),
281 u: &u,
282 }
283 .write(out);
284 }
285 Ok(())
286}
287
288pub(crate) fn atom_name_field(name: &str) -> String {
293 if name.len() >= 4 {
294 name.to_owned()
295 } else {
296 format!(" {name:<3}")
297 }
298}
299
300pub(crate) fn alt_field<'a>(structure: &'a Structure, atom: &AtomRef<'a>) -> &'a str {
305 let Some(alt) = atom.alt_id() else { return " " };
306 let Some(symbol) = alt.symbol() else {
307 return " ";
308 };
309 match structure.resolve(symbol) {
310 Some(label) => label,
311 None => " ",
312 }
313}
314
315pub(crate) fn serial_field(serial: i64, options: &PdbOptions) -> String {
316 match options
317 .hybrid36
318 .then(|| hybrid36::encode(serial, 5))
319 .flatten()
320 {
321 Some(field) => field,
322 None => format!("{serial:>5}"),
323 }
324}
325
326pub(crate) fn residue_field(seq: i64, options: &PdbOptions) -> String {
327 match options.hybrid36.then(|| hybrid36::encode(seq, 4)).flatten() {
328 Some(field) => field,
329 None => format!("{seq:>4}"),
330 }
331}
332
333pub(crate) fn chain_label<'a>(
334 chain: &'a ChainRef<'a>,
335 namespace: PdbIdentifierNamespace,
336) -> &'a str {
337 let Some(name) = chain_name(*chain, namespace) else {
338 return "";
339 };
340 name
341}
342
343pub(crate) fn insertion_code(residue: ResidueRef<'_>) -> &str {
344 let Some(code) = residue.ins_code() else {
345 return " ";
346 };
347 code
348}
349
350#[cfg(test)]
351#[path = "pdb_tests.rs"]
352mod tests;