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}