haddock_restraints/core/sasa.rs
1use pdbtbx::{Atom, Residue};
2use rust_sasa::calculate_sasa as calculate_rust_sasa;
3use rust_sasa::{SASALevel, SASAResult};
4use std::process;
5
6/// Represents an atom with additional solvent accessible surface area (SASA) information.
7#[derive(Debug)]
8pub struct ExtendedAtom {
9 /// The underlying atom.
10 pub atom: Atom,
11 /// The solvent accessible surface area of the atom.
12 pub sasa: f64,
13}
14
15impl ExtendedAtom {
16 /// Creates a new `ExtendedAtom` instance.
17 ///
18 /// # Arguments
19 ///
20 /// * `atom` - The underlying `Atom` object.
21 /// * `sasa` - The solvent accessible surface area of the atom.
22 ///
23 /// # Returns
24 ///
25 /// A new `ExtendedAtom` instance.
26 pub fn new(atom: Atom, sasa: f64) -> Self {
27 ExtendedAtom { atom, sasa }
28 }
29}
30
31/// Represents a residue with extended SASA information.
32pub struct ExtendedRes {
33 /// The underlying residue.
34 pub residue: Residue,
35 /// The chain identifier of the residue.
36 pub chain: String,
37 /// The SASA of the backbone atoms.
38 pub sasa_bb: f64,
39 /// The SASA of the side chain atoms.
40 pub sasa_sc: f64,
41 /// The relative SASA of the backbone atoms.
42 pub rel_sasa_bb: f64,
43 /// The relative SASA of the side chain atoms.
44 pub rel_sasa_sc: f64,
45 /// The total relative SASA of the residue.
46 pub rel_sasa_total: f64,
47}
48
49impl ExtendedRes {
50 /// Creates a new `ExtendedRes` instance.
51 ///
52 /// # Arguments
53 ///
54 /// * `residue` - The underlying `Residue` object.
55 /// * `chain` - The chain identifier of the residue.
56 /// * `sasa_bb` - The SASA of the backbone atoms.
57 /// * `sasa_sc` - The SASA of the side chain atoms.
58 /// * `rel_sasa_bb` - The relative SASA of the backbone atoms.
59 /// * `rel_sasa_sc` - The relative SASA of the side chain atoms.
60 /// * `rel_sasa_total` - The total relative SASA of the residue.
61 ///
62 /// # Returns
63 ///
64 /// A new `ExtendedRes` instance.
65 pub fn new(
66 residue: Residue,
67 chain: String,
68 sasa_bb: f64,
69 sasa_sc: f64,
70 rel_sasa_bb: f64,
71 rel_sasa_sc: f64,
72 rel_sasa_total: f64,
73 ) -> Self {
74 ExtendedRes {
75 residue,
76 chain,
77 sasa_bb,
78 sasa_sc,
79 rel_sasa_bb,
80 rel_sasa_sc,
81 rel_sasa_total,
82 }
83 }
84}
85
86/// Calculates the Solvent Accessible Surface Area (SASA) for a given PDB structure.
87///
88/// This function performs SASA calculations for each atom in the structure and aggregates
89/// the results at the residue level. It also calculates relative SASA values based on
90/// standard accessible surface areas for each residue type.
91///
92/// # Arguments
93///
94/// * `pdbtbx_struct` - A mutable reference to a `pdbtbx::PDB` structure.
95///
96/// # Returns
97///
98/// A `Vec<ExtendedRes>` containing SASA information for each residue in the structure.
99///
100/// # Panics
101///
102/// This function will panic if:
103/// - The number of atoms in the pdbtbx structure doesn't match the number of atoms in the SASA calculation.
104/// - A residue name is encountered that doesn't have a corresponding relative accessibility value.
105///
106/// # Steps
107///
108/// 1. Calculates SASA for each atom using the freesasa library.
109/// 2. Creates `ExtendedAtom` instances for each atom, storing SASA values.
110/// 3. Aggregates atom SASA values into `ExtendedRes` instances for each residue.
111/// 4. Calculates relative SASA values for backbone, side chain, and total residue.
112///
113/// # Notes
114///
115/// - The function modifies the input `pdbtbx_struct` by setting B-factors to SASA values.
116/// - SASA calculations are performed using the freesasa library with error-only verbosity.
117/// - Relative SASA values are calculated based on standard accessible surface areas defined in `REL_ASA`.
118///
119pub fn calculate_sasa(mut pdbtbx_struct: pdbtbx::PDB) -> Vec<ExtendedRes> {
120 // ================================================================================
121 // Calculate the SASA of each atom
122
123 let result = calculate_rust_sasa(&pdbtbx_struct, None, None, SASALevel::Atom).unwrap();
124
125 let atom_sasa: Vec<f64> = if let SASAResult::Atom(sasa_vec) = result {
126 // Convert Vec<f32> to Vec<f64>
127 sasa_vec.into_iter().map(|x| x as f64).collect()
128 } else {
129 panic!("Unexpected result type: expected Atom variant");
130 };
131
132 // Create a vector of ExtendedAtoms containing the SASA of each atom
133 let mut extended_atoms: Vec<ExtendedAtom> = Vec::new();
134
135 // Use the Atom from pdbtbx as base for the ExtendedAtom
136 // let mut rng = rand::thread_rng();
137 // for atom in pdbtbx_struct.atoms() {
138
139 // Create a vector as big as pdbtbx_struct.atoms()
140 // let atom_sasa: Vec<f64> = (0..pdbtbx_atom_count).map(|_| rng.gen()).collect();
141
142 for (atom, sasa) in pdbtbx_struct.atoms_mut().zip(atom_sasa.iter()) {
143 // Create a random sasa value for development
144 // let n: f64 = rng.gen();
145
146 // Find the atom in the atom_sasa vector
147 // let n = atom_sasa.iter().find(|(a, _)| a == atom).unwrap();
148 // let mut atom = atom;
149 let _ = atom.set_b_factor(*sasa);
150
151 let extended_atom = ExtendedAtom::new(atom.clone(), *sasa);
152 extended_atoms.push(extended_atom);
153 // Add a new property to the `atom` and a value
154 }
155
156 // Loop over the extended atoms and figure out to which residue they belong to
157 let mut extended_residues: Vec<ExtendedRes> = Vec::new();
158 for chain in pdbtbx_struct.chains() {
159 for residue in chain.residues() {
160 let mut sasa_bb = 0.0;
161 let mut sasa_sc = 0.0;
162 for atom in residue.atoms() {
163 // Find the atom in the extended atoms vector
164 let extended_atom = extended_atoms.iter().find(|&x| x.atom == *atom).unwrap();
165 // Add the SASA of the atom to the corresponding residue
166 if atom.is_backbone() {
167 sasa_bb += extended_atom.sasa;
168 } else {
169 sasa_sc += extended_atom.sasa;
170 }
171 }
172 let extended_residue = ExtendedRes::new(
173 residue.clone(),
174 chain.id().to_string(),
175 sasa_bb,
176 sasa_sc,
177 0.0,
178 0.0,
179 0.0,
180 );
181 extended_residues.push(extended_residue);
182 }
183 }
184
185 // Calculate the relative SASA of each residue, based on `asa::REL_ASA`
186 for (e_res, _res) in extended_residues
187 .iter_mut()
188 .zip(pdbtbx_struct.residues_mut())
189 {
190 let resname = e_res.residue.name().unwrap();
191
192 // if let Some(rel_asa) =
193 match REL_ASA.iter().find(|(name, _)| *name == resname) {
194 Some(rel_asa) => {
195 let rel_sasa_bb = (e_res.sasa_bb / rel_asa.1.bb) * 100_f64;
196 let rel_sasa_sc = (e_res.sasa_sc / rel_asa.1.sc) * 100_f64;
197 let rel_total = (e_res.sasa_bb + e_res.sasa_sc / rel_asa.1.total) * 100_f64;
198
199 e_res.rel_sasa_bb = rel_sasa_bb;
200 e_res.rel_sasa_sc = rel_sasa_sc;
201 e_res.rel_sasa_total = rel_total;
202 }
203 None => {
204 eprintln!("\n### ERROR CALCULATING SASA ###");
205 eprintln!(
206 "# No relative accessibility found for residue `{}`",
207 resname
208 );
209 process::exit(1);
210 }
211 }
212 }
213 extended_residues
214}
215
216/// Represents the Accessible Surface Area (ASA) values for a residue.
217///
218/// This struct holds the total ASA and its breakdown into backbone (bb) and side chain (sc) components.
219pub struct AsaValues {
220 /// The total Accessible Surface Area.
221 pub total: f64,
222
223 /// The Accessible Surface Area of the backbone atoms.
224 pub bb: f64,
225
226 /// The Accessible Surface Area of the side chain atoms.
227 pub sc: f64,
228}
229
230pub const REL_ASA: &[(&str, AsaValues)] = &[
231 (
232 "ALA",
233 AsaValues {
234 total: 107.95,
235 bb: 38.54,
236 sc: 69.41,
237 },
238 ),
239 (
240 "CYS",
241 AsaValues {
242 total: 134.28,
243 bb: 37.53,
244 sc: 96.75,
245 },
246 ),
247 (
248 "ASP",
249 AsaValues {
250 total: 140.39,
251 bb: 37.70,
252 sc: 102.69,
253 },
254 ),
255 (
256 "GLU",
257 AsaValues {
258 total: 172.25,
259 bb: 37.51,
260 sc: 134.74,
261 },
262 ),
263 (
264 "PHE",
265 AsaValues {
266 total: 199.48,
267 bb: 35.37,
268 sc: 164.11,
269 },
270 ),
271 (
272 "GLY",
273 AsaValues {
274 total: 80.10,
275 bb: 47.77,
276 sc: 32.33,
277 },
278 ),
279 (
280 "HIS",
281 AsaValues {
282 total: 182.88,
283 bb: 35.80,
284 sc: 147.08,
285 },
286 ),
287 (
288 "ILE",
289 AsaValues {
290 total: 175.12,
291 bb: 37.16,
292 sc: 137.96,
293 },
294 ),
295 (
296 "LYS",
297 AsaValues {
298 total: 200.81,
299 bb: 37.51,
300 sc: 163.30,
301 },
302 ),
303 (
304 "LEU",
305 AsaValues {
306 total: 178.63,
307 bb: 37.51,
308 sc: 141.12,
309 },
310 ),
311 (
312 "MET",
313 AsaValues {
314 total: 194.15,
315 bb: 37.51,
316 sc: 156.64,
317 },
318 ),
319 (
320 "ASN",
321 AsaValues {
322 total: 143.94,
323 bb: 37.70,
324 sc: 106.24,
325 },
326 ),
327 (
328 "PRO",
329 AsaValues {
330 total: 136.13,
331 bb: 16.23,
332 sc: 119.90,
333 },
334 ),
335 (
336 "GLN",
337 AsaValues {
338 total: 178.50,
339 bb: 37.51,
340 sc: 140.99,
341 },
342 ),
343 (
344 "ARG",
345 AsaValues {
346 total: 238.76,
347 bb: 37.51,
348 sc: 201.25,
349 },
350 ),
351 (
352 "SER",
353 AsaValues {
354 total: 116.50,
355 bb: 38.40,
356 sc: 78.11,
357 },
358 ),
359 (
360 "THR",
361 AsaValues {
362 total: 139.27,
363 bb: 37.57,
364 sc: 101.70,
365 },
366 ),
367 (
368 "VAL",
369 AsaValues {
370 total: 151.44,
371 bb: 37.16,
372 sc: 114.28,
373 },
374 ),
375 (
376 "TRP",
377 AsaValues {
378 total: 249.36,
379 bb: 38.10,
380 sc: 211.26,
381 },
382 ),
383 (
384 "TYR",
385 AsaValues {
386 total: 212.76,
387 bb: 35.38,
388 sc: 177.38,
389 },
390 ),
391 (
392 "ASH",
393 AsaValues {
394 total: 140.39,
395 bb: 37.70,
396 sc: 102.69,
397 },
398 ),
399 (
400 "DDZ",
401 AsaValues {
402 total: 107.95,
403 bb: 38.54,
404 sc: 69.41,
405 },
406 ),
407 (
408 "GLH",
409 AsaValues {
410 total: 172.25,
411 bb: 37.51,
412 sc: 134.74,
413 },
414 ),
415 (
416 "CYM",
417 AsaValues {
418 total: 134.28,
419 bb: 37.53,
420 sc: 96.75,
421 },
422 ),
423 (
424 "CSP",
425 AsaValues {
426 total: 134.28,
427 bb: 37.53,
428 sc: 96.75,
429 },
430 ),
431 (
432 "CYF",
433 AsaValues {
434 total: 134.28,
435 bb: 37.53,
436 sc: 96.75,
437 },
438 ),
439 (
440 "CYC",
441 AsaValues {
442 total: 134.28,
443 bb: 37.53,
444 sc: 96.75,
445 },
446 ),
447 (
448 "CFE",
449 AsaValues {
450 total: 134.28,
451 bb: 37.53,
452 sc: 96.75,
453 },
454 ),
455 (
456 "NEP",
457 AsaValues {
458 total: 182.88,
459 bb: 35.80,
460 sc: 147.08,
461 },
462 ),
463 (
464 "ALY",
465 AsaValues {
466 total: 200.81,
467 bb: 37.51,
468 sc: 163.30,
469 },
470 ),
471 (
472 "MLZ",
473 AsaValues {
474 total: 200.81,
475 bb: 37.51,
476 sc: 163.30,
477 },
478 ),
479 (
480 "MLY",
481 AsaValues {
482 total: 200.81,
483 bb: 37.51,
484 sc: 163.30,
485 },
486 ),
487 (
488 "M3L",
489 AsaValues {
490 total: 200.81,
491 bb: 37.51,
492 sc: 163.30,
493 },
494 ),
495 (
496 "HYP",
497 AsaValues {
498 total: 136.13,
499 bb: 16.23,
500 sc: 119.90,
501 },
502 ),
503 (
504 "SEP",
505 AsaValues {
506 total: 116.50,
507 bb: 38.40,
508 sc: 78.11,
509 },
510 ),
511 (
512 "TOP",
513 AsaValues {
514 total: 139.27,
515 bb: 37.57,
516 sc: 101.70,
517 },
518 ),
519 (
520 "TYP",
521 AsaValues {
522 total: 212.76,
523 bb: 35.38,
524 sc: 177.38,
525 },
526 ),
527 (
528 "PTR",
529 AsaValues {
530 total: 212.76,
531 bb: 35.38,
532 sc: 177.38,
533 },
534 ),
535 (
536 "TYS",
537 AsaValues {
538 total: 212.76,
539 bb: 35.38,
540 sc: 177.38,
541 },
542 ),
543 (
544 "PNS",
545 AsaValues {
546 total: 116.50,
547 bb: 38.40,
548 sc: 78.11,
549 },
550 ),
551 (
552 "QSR",
553 AsaValues {
554 total: 390.53,
555 bb: 26.64,
556 sc: 363.88,
557 },
558 ),
559];
560
561#[cfg(test)]
562mod test {
563
564 use crate::core::structure;
565
566 use super::*;
567
568 // TODO: Add more tests
569
570 #[test]
571 fn test_calculate_sasa() {
572 let pdb = structure::load_pdb("tests/data/complex.pdb").unwrap();
573 let extended_residues = calculate_sasa(pdb);
574
575 assert_eq!(extended_residues.len(), 116);
576 }
577}