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}