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