rust-sasa-python 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,12 @@ version = "1.2.0"
73
73
  source = "registry+https://github.com/rust-lang/crates.io-index"
74
74
  checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
75
75
 
76
+ [[package]]
77
+ name = "bitflags"
78
+ version = "2.10.0"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
81
+
76
82
  [[package]]
77
83
  name = "bumpalo"
78
84
  version = "3.19.0"
@@ -108,9 +114,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
108
114
 
109
115
  [[package]]
110
116
  name = "clap"
111
- version = "4.5.51"
117
+ version = "4.5.53"
112
118
  source = "registry+https://github.com/rust-lang/crates.io-index"
113
- checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
119
+ checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
114
120
  dependencies = [
115
121
  "clap_builder",
116
122
  "clap_derive",
@@ -118,9 +124,9 @@ dependencies = [
118
124
 
119
125
  [[package]]
120
126
  name = "clap_builder"
121
- version = "4.5.51"
127
+ version = "4.5.53"
122
128
  source = "registry+https://github.com/rust-lang/crates.io-index"
123
- checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
129
+ checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
124
130
  dependencies = [
125
131
  "anstream",
126
132
  "anstyle",
@@ -237,6 +243,12 @@ dependencies = [
237
243
  "miniz_oxide",
238
244
  ]
239
245
 
246
+ [[package]]
247
+ name = "fnv"
248
+ version = "1.0.7"
249
+ source = "registry+https://github.com/rust-lang/crates.io-index"
250
+ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
251
+
240
252
  [[package]]
241
253
  name = "hash32"
242
254
  version = "0.3.1"
@@ -280,9 +292,9 @@ dependencies = [
280
292
 
281
293
  [[package]]
282
294
  name = "indicatif"
283
- version = "0.18.2"
295
+ version = "0.18.3"
284
296
  source = "registry+https://github.com/rust-lang/crates.io-index"
285
- checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
297
+ checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
286
298
  dependencies = [
287
299
  "console",
288
300
  "portable-atomic",
@@ -515,18 +527,27 @@ dependencies = [
515
527
 
516
528
  [[package]]
517
529
  name = "pulp"
518
- version = "0.21.5"
530
+ version = "0.22.2"
519
531
  source = "registry+https://github.com/rust-lang/crates.io-index"
520
- checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907"
532
+ checksum = "2e205bb30d5b916c55e584c22201771bcf2bad9aabd5d4127f38387140c38632"
521
533
  dependencies = [
522
534
  "bytemuck",
523
535
  "cfg-if",
524
536
  "libm",
525
537
  "num-complex",
538
+ "paste",
539
+ "pulp-wasm-simd-flag",
540
+ "raw-cpuid",
526
541
  "reborrow",
527
542
  "version_check",
528
543
  ]
529
544
 
545
+ [[package]]
546
+ name = "pulp-wasm-simd-flag"
547
+ version = "0.1.0"
548
+ source = "registry+https://github.com/rust-lang/crates.io-index"
549
+ checksum = "40e24eee682d89fb193496edf918a7f407d30175b2e785fe057e4392dfd182e0"
550
+
530
551
  [[package]]
531
552
  name = "pyo3"
532
553
  version = "0.25.1"
@@ -591,9 +612,9 @@ dependencies = [
591
612
 
592
613
  [[package]]
593
614
  name = "quick-xml"
594
- version = "0.38.3"
615
+ version = "0.38.4"
595
616
  source = "registry+https://github.com/rust-lang/crates.io-index"
596
- checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
617
+ checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
597
618
  dependencies = [
598
619
  "memchr",
599
620
  "serde",
@@ -617,6 +638,15 @@ dependencies = [
617
638
  "proc-macro2 1.0.95",
618
639
  ]
619
640
 
641
+ [[package]]
642
+ name = "raw-cpuid"
643
+ version = "11.6.0"
644
+ source = "registry+https://github.com/rust-lang/crates.io-index"
645
+ checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
646
+ dependencies = [
647
+ "bitflags",
648
+ ]
649
+
620
650
  [[package]]
621
651
  name = "rawpointer"
622
652
  version = "0.2.1"
@@ -662,11 +692,12 @@ dependencies = [
662
692
 
663
693
  [[package]]
664
694
  name = "rust-sasa"
665
- version = "0.4.0"
695
+ version = "0.7.0"
666
696
  source = "registry+https://github.com/rust-lang/crates.io-index"
667
- checksum = "57cd14c05b75474f1c401cadec34be8c0b4981ba9bfe98921bc57947af5964b9"
697
+ checksum = "d92c8f75a02c33d1b3e3f233fa84b39cb127041c07ed77bbd0a25cd3fa07c1c5"
668
698
  dependencies = [
669
699
  "clap",
700
+ "fnv",
670
701
  "indicatif",
671
702
  "mimalloc",
672
703
  "nalgebra",
@@ -681,12 +712,13 @@ dependencies = [
681
712
 
682
713
  [[package]]
683
714
  name = "rust-sasa-python"
684
- version = "0.3.0"
715
+ version = "0.5.0"
685
716
  dependencies = [
686
717
  "mimalloc",
687
718
  "nalgebra",
688
719
  "num-complex",
689
720
  "pdbtbx",
721
+ "pulp",
690
722
  "pyo3",
691
723
  "rust-sasa",
692
724
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rust-sasa-python"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -12,7 +12,8 @@ crate-type = ["cdylib"]
12
12
  [dependencies]
13
13
  pdbtbx = "0.12.0"
14
14
  pyo3 = "0.25.1"
15
- rust-sasa = "0.4.0"
15
+ rust-sasa = "0.7.0"
16
16
  nalgebra = "0.33.2"
17
17
  mimalloc = "0.1.47"
18
18
  num-complex = "0.4.6"
19
+ pulp = "0.22.2"
@@ -34,7 +34,27 @@ SASACalculator(pdb_path: str)
34
34
  - Returns modified calculator for method chaining
35
35
 
36
36
  **`with_n_points(points: int) -> SASACalculator`**
37
- - Set the number of points for surface sampling (default: 1000)
37
+ - Set the number of points for surface sampling (default: 100)
38
+ - Returns modified calculator for method chaining
39
+
40
+ **`with_threads(threads: int) -> SASACalculator`**
41
+ - Set the number of threads for parallel processing (default: -1 uses all cores)
42
+ - Returns modified calculator for method chaining
43
+
44
+ **`with_include_hydrogens(include_hydrogens: bool) -> SASACalculator`**
45
+ - Set whether to include hydrogen atoms in the calculation (default: False)
46
+ - Returns modified calculator for method chaining
47
+
48
+ **`with_include_hetatms(include_hetatms: bool) -> SASACalculator`**
49
+ - Set whether to include HETATM records in the calculation (default: False)
50
+ - Returns modified calculator for method chaining
51
+
52
+ **`with_radii_file(path: str) -> SASACalculator`**
53
+ - Customize Van der Waals radii by providing a custom radii file (default: ProtOr by Tsai et al.)
54
+ - Returns modified calculator for method chaining
55
+
56
+ **`with_allow_vdw_fallback(allow: bool) -> SASACalculator`**
57
+ - Allow fallback to PDBTBX van der Waals radii when custom radius is not found (default: False)
38
58
  - Returns modified calculator for method chaining
39
59
 
40
60
  #### Calculation Methods
@@ -88,7 +108,7 @@ chain_results = sasa.calculate_chain_sasa("protein.pdb")
88
108
 
89
109
  For advanced use cases where you need to calculate SASA for custom atom sets:
90
110
 
91
- **`calculate_sasa_internal(atoms_in: list[tuple[tuple[float, float, float], float, int]], probe_radius: float, n_points: int) -> list[float]`**
111
+ **`calculate_sasa_internal(atoms_in: list[tuple[tuple[float, float, float], float, int]], probe_radius: float, n_points: int, threads: int) -> list[float]`**
92
112
 
93
113
  Calculate SASA for a set of atoms directly without PDB file parsing.
94
114
 
@@ -99,6 +119,7 @@ Calculate SASA for a set of atoms directly without PDB file parsing.
99
119
  - Atom index as int
100
120
  - `probe_radius`: Probe sphere radius in Ångströms
101
121
  - `n_points`: Number of points for surface sampling
122
+ - `threads`: Number of threads for parallel processing (-1 uses all cores)
102
123
 
103
124
  **Returns:**
104
125
  - List of SASA values (floats) corresponding to each input atom
@@ -115,13 +136,13 @@ atoms = [
115
136
  ]
116
137
 
117
138
  # Calculate SASA
118
- sasa_values = sasa.calculate_sasa_internal(atoms, probe_radius=1.4, n_points=100)
139
+ sasa_values = sasa.calculate_sasa_internal(atoms, probe_radius=1.4, n_points=100, threads=-1)
119
140
  print(f"SASA values: {sasa_values}")
120
141
  ```
121
142
 
122
- **`calculate_sasa_internal_at_residue_level(atoms_in: list[tuple[tuple[float, float, float], float, int]], probe_radius: float, n_points: int) -> list[ResidueResult]`**
143
+ **`calculate_sasa_internal_at_residue_level(atoms_in: list[tuple[tuple[float, float, float], float, int]], probe_radius: float, n_points: int, threads: int) -> list[ResidueResult]`**
123
144
 
124
- Calculate SASA at the residue level for a set of atoms directly without PDB file parsing. This function groups atoms by residue ID and calculates the average SASA per atom within each residue.
145
+ Calculate SASA at the residue level for a set of atoms directly without PDB file parsing. This function groups atoms by residue ID and calculates the total SASA for each residue.
125
146
 
126
147
  **Parameters:**
127
148
  - `atoms_in`: List of tuples, each containing:
@@ -130,13 +151,14 @@ Calculate SASA at the residue level for a set of atoms directly without PDB file
130
151
  - Residue ID as int (used to group atoms into residues)
131
152
  - `probe_radius`: Probe sphere radius in Ångströms
132
153
  - `n_points`: Number of points for surface sampling
154
+ - `threads`: Number of threads for parallel processing (-1 uses all cores)
133
155
 
134
156
  **Returns:**
135
157
  - List of `ResidueResult` objects with:
136
158
  - `chain_id`: Always "UNK" (unknown)
137
159
  - `residue_name`: Always "UNK" (unknown)
138
160
  - `residue_number`: The residue ID from input
139
- - `sasa`: Average SASA per atom in the residue
161
+ - `sasa`: Total SASA for the residue
140
162
 
141
163
  **Example:**
142
164
  ```python
@@ -151,9 +173,9 @@ atoms = [
151
173
  ]
152
174
 
153
175
  # Calculate residue-level SASA
154
- residues = sasa.calculate_sasa_internal_at_residue_level(atoms, probe_radius=1.4, n_points=100)
176
+ residues = sasa.calculate_sasa_internal_at_residue_level(atoms, probe_radius=1.4, n_points=100, threads=-1)
155
177
  for residue in residues:
156
- print(f"Residue {residue.residue_number}: {residue.sasa:.2f} Ų (avg per atom)")
178
+ print(f"Residue {residue.residue_number}: {residue.sasa:.2f} Ų")
157
179
  ```
158
180
 
159
181
  ## Usage Examples
@@ -235,6 +257,35 @@ for radius in radii:
235
257
  print(f"Radius {radius}: {result.total:.2f} Ų")
236
258
  ```
237
259
 
260
+ ### Advanced Configuration Examples
261
+
262
+ ```python
263
+ # Include hydrogen atoms in calculation
264
+ calc = (sasa.SASACalculator("protein.pdb")
265
+ .with_include_hydrogens(True)
266
+ .with_n_points(2000))
267
+ result = calc.calculate_protein()
268
+ print(f"SASA with hydrogens: {result.total:.2f} Ų")
269
+
270
+ # Include HETATM records (ligands, ions, water)
271
+ calc = (sasa.SASACalculator("protein.pdb")
272
+ .with_include_hetatms(True))
273
+ result = calc.calculate_protein()
274
+ print(f"SASA with HETATMs: {result.total:.2f} Ų")
275
+
276
+ # Control parallelization
277
+ calc = (sasa.SASACalculator("protein.pdb")
278
+ .with_threads(4)) # Use 4 threads
279
+ result = calc.calculate_protein()
280
+
281
+ # Use custom Van der Waals radii file
282
+ calc = (sasa.SASACalculator("protein.pdb")
283
+ .with_radii_file("custom_radii.txt")
284
+ .with_allow_vdw_fallback(True)) # Fallback to default if not found
285
+ result = calc.calculate_protein()
286
+ print(f"SASA with custom radii: {result.total:.2f} Ų")
287
+ ```
288
+
238
289
  ### Error Handling
239
290
 
240
291
  ```python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rust-sasa-python
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -21,7 +21,7 @@ A high-performance **Python library** for computing SASA (Solvent Accessible Sur
21
21
  ## Features
22
22
 
23
23
  - 🦀 **Powered by [RustSASA](https://github.com/maxall41/RustSASA)**: Leverages Rust's performance and safety.
24
- - ⚡️ **Ludicrous Speed**: **46X** faster than Biopython, **7X** faster than Freesasa.
24
+ - ⚡️ **Ludicrous Speed**: **63X** faster than Biopython, **5X** faster than Freesasa.
25
25
  - 🐍 **Pythonic Interface**: Clean, intuitive API.
26
26
  - 🔧 **Configurable**: Customizable probe radius and sampling points.
27
27
  - 📁 **PDB and mmCIF SUPPORT**: Supports both PDB and mmCIF files.
@@ -5,7 +5,7 @@ A high-performance **Python library** for computing SASA (Solvent Accessible Sur
5
5
  ## Features
6
6
 
7
7
  - 🦀 **Powered by [RustSASA](https://github.com/maxall41/RustSASA)**: Leverages Rust's performance and safety.
8
- - ⚡️ **Ludicrous Speed**: **46X** faster than Biopython, **7X** faster than Freesasa.
8
+ - ⚡️ **Ludicrous Speed**: **63X** faster than Biopython, **5X** faster than Freesasa.
9
9
  - 🐍 **Pythonic Interface**: Clean, intuitive API.
10
10
  - 🔧 **Configurable**: Customizable probe radius and sampling points.
11
11
  - 📁 **PDB and mmCIF SUPPORT**: Supports both PDB and mmCIF files.
@@ -3,7 +3,7 @@ use pdbtbx;
3
3
  use pyo3::exceptions::PyRuntimeError;
4
4
  use pyo3::prelude::*;
5
5
  use rust_sasa::options::SASAOptions;
6
- use rust_sasa::{Atom, SASAProcessor, calculate_sasa_internal as calculate_sasa_internal_internal};
6
+ use rust_sasa::{Atom, calculate_sasa_internal as calculate_sasa_internal_internal};
7
7
  use rust_sasa::{AtomLevel, ChainLevel, ProteinLevel, ResidueLevel};
8
8
  use simd::simd_sum; // As in long long
9
9
  use std::collections::HashMap;
@@ -79,6 +79,11 @@ pub struct SASACalculator {
79
79
  pdb_path: String,
80
80
  probe_radius: Option<f32>,
81
81
  n_points: Option<usize>,
82
+ radii_file: Option<String>,
83
+ threads: isize,
84
+ include_hydrogens: bool,
85
+ include_hetatms: bool,
86
+ allow_vdw_fallback: bool,
82
87
  }
83
88
 
84
89
  #[pymethods]
@@ -89,6 +94,11 @@ impl SASACalculator {
89
94
  pdb_path,
90
95
  probe_radius: None,
91
96
  n_points: None,
97
+ radii_file: None,
98
+ threads: -1,
99
+ include_hydrogens: false,
100
+ include_hetatms: false,
101
+ allow_vdw_fallback: false,
92
102
  }
93
103
  }
94
104
 
@@ -116,21 +126,73 @@ impl SASACalculator {
116
126
  self.clone()
117
127
  }
118
128
 
129
+ /// Set the number of threads for parallel processing
130
+ ///
131
+ /// Args:
132
+ /// threads: Number of threads (default: -1 uses all cores)
133
+ ///
134
+ /// Returns:
135
+ /// Self for method chaining
136
+ pub fn with_threads(&mut self, threads: isize) -> Self {
137
+ self.threads = threads;
138
+ self.clone()
139
+ }
140
+
141
+ /// Set whether to include hydrogens in the calculation
142
+ ///
143
+ /// Args:
144
+ /// include_hydrogens: Whether to include hydrogens (default: false)
145
+ ///
146
+ /// Returns:
147
+ /// Self for method chaining
148
+ pub fn with_include_hydrogens(&mut self, include_hydrogens: bool) -> Self {
149
+ self.include_hydrogens = include_hydrogens;
150
+ self.clone()
151
+ }
152
+
153
+ /// Set whether to include HETATM records in the calculation
154
+ ///
155
+ /// Args:
156
+ /// include_hetatms: Whether to include HETATM records (default: false)
157
+ ///
158
+ /// Returns:
159
+ /// Self for method chaining
160
+ pub fn with_include_hetatms(&mut self, include_hetatms: bool) -> Self {
161
+ self.include_hetatms = include_hetatms;
162
+ self.clone()
163
+ }
164
+
165
+ /// Customize Van der Waals radii file
166
+ ///
167
+ /// Args:
168
+ /// path: Path to the custom radii file (default: ProtOr by Tasi et al.)
169
+ ///
170
+ /// Returns:
171
+ /// Self for method chaining
172
+ pub fn with_radii_file(&mut self, path: &str) -> Self {
173
+ self.radii_file = Some(path.to_string());
174
+ self.clone()
175
+ }
176
+
177
+ /// Allow fallback to PDBTBX van der Waals radii when custom radius is not found (default: false)
178
+ ///
179
+ /// Args:
180
+ /// allow: Whether to allow fallback to PDBTBX van der Waals radii (default: false)
181
+ ///
182
+ /// Returns:
183
+ /// Self for method chaining
184
+ pub fn with_allow_vdw_fallback(&mut self, allow: bool) -> Self {
185
+ self.allow_vdw_fallback = allow;
186
+ self.clone()
187
+ }
188
+
119
189
  /// Calculate SASA at the protein level
120
190
  ///
121
191
  /// Returns:
122
192
  /// Protein: Object containing total, polar, and non-polar SASA values
123
193
  pub fn calculate_protein(&self) -> PyResult<Protein> {
124
194
  let pdb = self.load_pdb()?;
125
- let mut options = SASAOptions::<ProteinLevel>::new();
126
-
127
- if let Some(radius) = self.probe_radius {
128
- options = options.with_probe_radius(radius);
129
- }
130
-
131
- if let Some(points) = self.n_points {
132
- options = options.with_n_points(points);
133
- }
195
+ let options = self.apply_options(SASAOptions::<ProteinLevel>::new())?;
134
196
 
135
197
  match options.process(&pdb) {
136
198
  Ok(result) => Ok(Protein {
@@ -151,15 +213,7 @@ impl SASACalculator {
151
213
  /// List[Chain]: List of chain-level SASA results
152
214
  pub fn calculate_chain(&self) -> PyResult<Vec<Chain>> {
153
215
  let pdb = self.load_pdb()?;
154
- let mut options = SASAOptions::<ChainLevel>::new();
155
-
156
- if let Some(radius) = self.probe_radius {
157
- options = options.with_probe_radius(radius);
158
- }
159
-
160
- if let Some(points) = self.n_points {
161
- options = options.with_n_points(points);
162
- }
216
+ let options = self.apply_options(SASAOptions::<ChainLevel>::new())?;
163
217
 
164
218
  match options.process(&pdb) {
165
219
  Ok(results) => {
@@ -185,15 +239,7 @@ impl SASACalculator {
185
239
  /// List[Residue]: List of residue-level SASA results
186
240
  pub fn calculate_residue(&self) -> PyResult<Vec<Residue>> {
187
241
  let pdb = self.load_pdb()?;
188
- let mut options = SASAOptions::<ResidueLevel>::new();
189
-
190
- if let Some(radius) = self.probe_radius {
191
- options = options.with_probe_radius(radius);
192
- }
193
-
194
- if let Some(points) = self.n_points {
195
- options = options.with_n_points(points);
196
- }
242
+ let options = self.apply_options(SASAOptions::<ResidueLevel>::new())?;
197
243
 
198
244
  match options.process(&pdb) {
199
245
  Ok(results) => {
@@ -221,15 +267,7 @@ impl SASACalculator {
221
267
  /// List[float]: List of atom-level SASA values
222
268
  pub fn calculate_atom(&self) -> PyResult<Vec<f32>> {
223
269
  let pdb = self.load_pdb()?;
224
- let mut options = SASAOptions::<AtomLevel>::new();
225
-
226
- if let Some(radius) = self.probe_radius {
227
- options = options.with_probe_radius(radius);
228
- }
229
-
230
- if let Some(points) = self.n_points {
231
- options = options.with_n_points(points);
232
- }
270
+ let options = self.apply_options(SASAOptions::<AtomLevel>::new())?;
233
271
 
234
272
  match options.process(&pdb) {
235
273
  Ok(results) => Ok(results),
@@ -273,6 +311,29 @@ impl SASACalculator {
273
311
  ))),
274
312
  }
275
313
  }
314
+
315
+ fn apply_options<T>(&self, mut options: SASAOptions<T>) -> PyResult<SASAOptions<T>> {
316
+ if let Some(radius) = self.probe_radius {
317
+ options = options.with_probe_radius(radius);
318
+ }
319
+
320
+ if let Some(points) = self.n_points {
321
+ options = options.with_n_points(points);
322
+ }
323
+
324
+ options = options.with_threads(self.threads);
325
+ options = options.with_include_hydrogens(self.include_hydrogens);
326
+ options = options.with_include_hetatms(self.include_hetatms);
327
+ options = options.with_allow_vdw_fallback(self.allow_vdw_fallback);
328
+
329
+ if let Some(radii_file) = &self.radii_file {
330
+ options = options.with_radii_file(radii_file).map_err(|e| {
331
+ PyRuntimeError::new_err(format!("Failed to load radii file: {}", e))
332
+ })?;
333
+ }
334
+
335
+ Ok(options)
336
+ }
276
337
  }
277
338
 
278
339
  // Convenience functions for backward compatibility and simple use cases
@@ -305,6 +366,7 @@ pub fn calculate_sasa_internal(
305
366
  atoms_in: Vec<((f32, f32, f32), f32, usize)>,
306
367
  probe_radius: f32,
307
368
  n_points: usize,
369
+ threads: isize,
308
370
  ) -> PyResult<Vec<f32>> {
309
371
  let atoms: Vec<Atom> = atoms_in
310
372
  .into_iter()
@@ -320,7 +382,7 @@ pub fn calculate_sasa_internal(
320
382
  atoms.as_slice(),
321
383
  probe_radius,
322
384
  n_points,
323
- true,
385
+ threads,
324
386
  ))
325
387
  }
326
388
 
@@ -329,8 +391,10 @@ fn calculate_sasa_internal_at_residue_level(
329
391
  atoms_in: Vec<((f32, f32, f32), f32, usize)>,
330
392
  probe_radius: f32,
331
393
  n_points: usize,
394
+ threads: isize,
332
395
  ) -> PyResult<Vec<Residue>> {
333
- let atom_sasa = calculate_sasa_internal(atoms_in.clone(), probe_radius, n_points).unwrap();
396
+ let atom_sasa =
397
+ calculate_sasa_internal(atoms_in.clone(), probe_radius, n_points, threads).unwrap();
334
398
 
335
399
  let mut residue_groups: HashMap<usize, Vec<f32>> = HashMap::new();
336
400
 
@@ -0,0 +1,15 @@
1
+ use std::sync::LazyLock;
2
+
3
+ use pulp::Arch;
4
+
5
+ pub(crate) static ARCH: LazyLock<Arch> = LazyLock::new(Arch::new);
6
+
7
+ pub(crate) fn simd_sum(values: &[f32]) -> f32 {
8
+ let mut total = 0f32;
9
+ ARCH.dispatch(|| {
10
+ for x in values {
11
+ total += x;
12
+ }
13
+ });
14
+ total
15
+ }
@@ -0,0 +1,381 @@
1
+ import time
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ import rust_sasa_python as sasa
7
+
8
+ # Path to the example CIF file
9
+ EXAMPLE_CIF = Path(__file__).parent.parent / "example.cif"
10
+
11
+
12
+ def test_protein_calculation():
13
+ """Test protein-level SASA calculation."""
14
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
15
+ result = calculator.calculate_protein()
16
+
17
+ assert isinstance(result, sasa.Protein)
18
+ assert result.total > 0
19
+ assert result.polar >= 0
20
+ assert result.non_polar >= 0
21
+ assert abs(result.total - (result.polar + result.non_polar)) < 1.0
22
+
23
+
24
+ def test_chain_calculation():
25
+ """Test chain-level SASA calculation."""
26
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
27
+ results = calculator.calculate_chain()
28
+
29
+ assert isinstance(results, list)
30
+ assert len(results) > 0
31
+
32
+ for chain in results:
33
+ assert isinstance(chain, sasa.Chain)
34
+ assert isinstance(chain.chain_id, str)
35
+ assert chain.sasa >= 0
36
+
37
+
38
+ def test_residue_calculation():
39
+ """Test residue-level SASA calculation."""
40
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
41
+ results = calculator.calculate_residue()
42
+
43
+ assert isinstance(results, list)
44
+ assert len(results) > 0
45
+
46
+ for residue in results:
47
+ assert isinstance(residue, sasa.Residue)
48
+ assert isinstance(residue.chain_id, str)
49
+ assert isinstance(residue.residue_name, str)
50
+ assert isinstance(residue.residue_number, int)
51
+ assert residue.sasa >= 0
52
+
53
+
54
+ def test_atom_calculation():
55
+ """Test atom-level SASA calculation."""
56
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
57
+ results = calculator.calculate_atom()
58
+
59
+ assert isinstance(results, list)
60
+ assert len(results) > 0
61
+
62
+ for atom_sasa in results:
63
+ assert isinstance(atom_sasa, float)
64
+ assert atom_sasa >= 0
65
+
66
+
67
+ def test_builder_pattern():
68
+ """Test configuration using builder pattern."""
69
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.4).with_n_points(500)
70
+ result = calculator.calculate_protein()
71
+
72
+ assert result.total > 0
73
+ assert "probe_radius=1.4" in repr(calculator)
74
+ assert "n_points=500" in repr(calculator)
75
+
76
+
77
+ def test_different_configurations():
78
+ """Test that different configurations give different results."""
79
+ calc1 = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.2)
80
+ calc2 = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.6)
81
+
82
+ result1 = calc1.calculate_protein()
83
+ result2 = calc2.calculate_protein()
84
+
85
+ # Different probe radii should give different results
86
+ assert result1.total != result2.total
87
+
88
+
89
+ def test_convenience_functions():
90
+ """Test all convenience functions work."""
91
+ protein = sasa.calculate_protein_sasa(str(EXAMPLE_CIF))
92
+ chains = sasa.calculate_chain_sasa(str(EXAMPLE_CIF))
93
+ residues = sasa.calculate_residue_sasa(str(EXAMPLE_CIF))
94
+ atoms = sasa.calculate_atom_sasa(str(EXAMPLE_CIF))
95
+
96
+ assert isinstance(protein, sasa.Protein)
97
+ assert isinstance(chains, list) and len(chains) > 0
98
+ assert isinstance(residues, list) and len(residues) > 0
99
+ assert isinstance(atoms, list) and len(atoms) > 0
100
+
101
+
102
+ def test_consistency_between_granularities():
103
+ """Test that different granularities give consistent totals."""
104
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_n_points(500)
105
+
106
+ protein_result = calculator.calculate_protein()
107
+ chain_results = calculator.calculate_chain()
108
+ residue_results = calculator.calculate_residue()
109
+ atom_results = calculator.calculate_atom()
110
+
111
+ # Sum of parts should approximately equal whole
112
+ chain_total = sum(chain.sasa for chain in chain_results)
113
+ residue_total = sum(residue.sasa for residue in residue_results)
114
+ atom_total = sum(atom_results)
115
+
116
+ tolerance = 0.1
117
+ assert abs(protein_result.total - chain_total) < tolerance
118
+ assert abs(protein_result.total - residue_total) < tolerance
119
+ assert abs(protein_result.total - atom_total) < tolerance
120
+
121
+
122
+ def test_convenience_vs_calculator():
123
+ """Test that convenience functions give same results as calculator."""
124
+ # Using calculator
125
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
126
+ calc_protein = calculator.calculate_protein()
127
+
128
+ # Using convenience function
129
+ conv_protein = sasa.calculate_protein_sasa(str(EXAMPLE_CIF))
130
+
131
+ # Results should be identical
132
+ assert abs(calc_protein.total - conv_protein.total) < 0.001
133
+
134
+
135
+ def test_nonexistent_file():
136
+ """Test handling of nonexistent file."""
137
+ with pytest.raises(Exception):
138
+ calculator = sasa.SASACalculator("nonexistent_file.pdb")
139
+ calculator.calculate_protein()
140
+
141
+
142
+ def test_calculation_speed():
143
+ """Test that calculations complete quickly."""
144
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_n_points(1000)
145
+
146
+ start_time = time.time()
147
+ result = calculator.calculate_protein()
148
+ end_time = time.time()
149
+
150
+ calculation_time = end_time - start_time
151
+ assert calculation_time < 2.0 # Should be very fast
152
+ assert result.total > 0
153
+
154
+
155
+ def test_repr_methods():
156
+ """Test that repr methods work for all objects."""
157
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
158
+
159
+ # Test calculator repr
160
+ assert "SASACalculator(" in repr(calculator)
161
+
162
+ # Test result reprs
163
+ protein = calculator.calculate_protein()
164
+ chains = calculator.calculate_chain()
165
+ residues = calculator.calculate_residue()
166
+
167
+ assert "Protein(" in repr(protein)
168
+ if chains:
169
+ assert "Chain(" in repr(chains[0])
170
+ if residues:
171
+ assert "Residue(" in repr(residues[0])
172
+
173
+
174
+ def test_workflow_example():
175
+ """Test a realistic workflow."""
176
+ # Quick calculation
177
+ quick_result = sasa.calculate_protein_sasa(str(EXAMPLE_CIF))
178
+ assert quick_result.total > 0
179
+
180
+ # Detailed analysis
181
+ calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.4).with_n_points(500)
182
+
183
+ protein = calculator.calculate_protein()
184
+ chains = calculator.calculate_chain()
185
+ residues = calculator.calculate_residue()
186
+
187
+ # All should be valid
188
+ assert protein.total > 0
189
+ assert len(chains) >= 1
190
+ assert len(residues) >= 1
191
+
192
+ # Should have expected structure
193
+ if residues:
194
+ first_residue = residues[0]
195
+ assert hasattr(first_residue, "chain_id")
196
+ assert hasattr(first_residue, "residue_name")
197
+ assert hasattr(first_residue, "residue_number")
198
+ assert first_residue.sasa >= 0
199
+
200
+
201
+ def test_with_threads():
202
+ """Test thread configuration."""
203
+ # Test with different thread counts
204
+ calc_single = sasa.SASACalculator(str(EXAMPLE_CIF)).with_threads(1)
205
+ calc_multi = sasa.SASACalculator(str(EXAMPLE_CIF)).with_threads(4)
206
+ calc_auto = sasa.SASACalculator(str(EXAMPLE_CIF)).with_threads(-1)
207
+
208
+ result_single = calc_single.calculate_protein()
209
+ result_multi = calc_multi.calculate_protein()
210
+ result_auto = calc_auto.calculate_protein()
211
+
212
+ # Results should be identical regardless of thread count
213
+ assert abs(result_single.total - result_multi.total) < 0.01
214
+ assert abs(result_single.total - result_auto.total) < 0.01
215
+
216
+
217
+ def test_with_include_hydrogens():
218
+ """Test hydrogen inclusion configuration."""
219
+ calc_no_h = sasa.SASACalculator(str(EXAMPLE_CIF)).with_include_hydrogens(False)
220
+ calc_with_h = sasa.SASACalculator(str(EXAMPLE_CIF)).with_include_hydrogens(True)
221
+
222
+ result_no_h = calc_no_h.calculate_protein()
223
+ result_with_h = calc_with_h.calculate_protein()
224
+
225
+ # Both should return valid results
226
+ assert result_no_h.total > 0
227
+ assert result_with_h.total > 0
228
+
229
+ # Atom counts should differ if hydrogens are present
230
+ atoms_no_h = calc_no_h.calculate_atom()
231
+ atoms_with_h = calc_with_h.calculate_atom()
232
+
233
+ assert len(atoms_no_h) > 0
234
+ assert len(atoms_with_h) > 0
235
+
236
+
237
+ def test_with_include_hetatms():
238
+ """Test HETATM inclusion configuration."""
239
+ calc_no_het = sasa.SASACalculator(str(EXAMPLE_CIF)).with_include_hetatms(False)
240
+ calc_with_het = sasa.SASACalculator(str(EXAMPLE_CIF)).with_include_hetatms(True)
241
+
242
+ result_no_het = calc_no_het.calculate_protein()
243
+ result_with_het = calc_with_het.calculate_protein()
244
+
245
+ # Both should return valid results
246
+ assert result_no_het.total > 0
247
+ assert result_with_het.total > 0
248
+
249
+ # Atom counts may differ if HETATMs are present
250
+ atoms_no_het = calc_no_het.calculate_atom()
251
+ atoms_with_het = calc_with_het.calculate_atom()
252
+
253
+ assert len(atoms_no_het) > 0
254
+ assert len(atoms_with_het) > 0
255
+
256
+
257
+ def test_with_allow_vdw_fallback():
258
+ """Test VDW fallback configuration."""
259
+ # Test that fallback can be enabled/disabled
260
+ calc_no_fallback = sasa.SASACalculator(str(EXAMPLE_CIF)).with_allow_vdw_fallback(False)
261
+ calc_with_fallback = sasa.SASACalculator(str(EXAMPLE_CIF)).with_allow_vdw_fallback(True)
262
+
263
+ result_no_fallback = calc_no_fallback.calculate_protein()
264
+ result_with_fallback = calc_with_fallback.calculate_protein()
265
+
266
+ # Both should return valid results for standard PDB files
267
+ assert result_no_fallback.total > 0
268
+ assert result_with_fallback.total > 0
269
+
270
+
271
+ def test_multiple_config_chaining():
272
+ """Test chaining multiple configuration methods."""
273
+ calculator = (
274
+ sasa.SASACalculator(str(EXAMPLE_CIF))
275
+ .with_probe_radius(1.4)
276
+ .with_n_points(1000)
277
+ .with_threads(2)
278
+ .with_include_hydrogens(False)
279
+ .with_include_hetatms(False)
280
+ .with_allow_vdw_fallback(True)
281
+ )
282
+
283
+ # Should be able to calculate at all levels
284
+ protein = calculator.calculate_protein()
285
+ chains = calculator.calculate_chain()
286
+ residues = calculator.calculate_residue()
287
+ atoms = calculator.calculate_atom()
288
+
289
+ assert protein.total > 0
290
+ assert len(chains) > 0
291
+ assert len(residues) > 0
292
+ assert len(atoms) > 0
293
+
294
+
295
+ def test_probe_radius_variations():
296
+ """Test various probe radius values."""
297
+ radii = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0]
298
+ results = []
299
+
300
+ for radius in radii:
301
+ calc = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(radius)
302
+ result = calc.calculate_protein()
303
+ results.append(result.total)
304
+
305
+ # All results should be positive
306
+ assert all(r > 0 for r in results)
307
+ # Larger probe radii should generally give smaller SASA values
308
+ # (larger probe can't access smaller crevices)
309
+ assert results[-1] < results[0]
310
+
311
+
312
+ def test_n_points_variations():
313
+ """Test various n_points values for accuracy."""
314
+ points = [50, 100, 500, 1000]
315
+ results = []
316
+
317
+ for n in points:
318
+ calc = sasa.SASACalculator(str(EXAMPLE_CIF)).with_n_points(n)
319
+ result = calc.calculate_protein()
320
+ results.append(result.total)
321
+
322
+ # Higher point counts should give more consistent results
323
+ assert all(r > 0 for r in results)
324
+ # Results should converge (difference between high point counts should be small)
325
+ assert abs(results[-1] - results[-2]) < abs(results[1] - results[0])
326
+
327
+
328
+ def test_calculate_sasa_internal_with_threads():
329
+ """Test low-level calculate_sasa_internal function with threads parameter."""
330
+ # Define simple atom set
331
+ atoms = [
332
+ ((0.0, 0.0, 0.0), 1.7, 0),
333
+ ((2.0, 0.0, 0.0), 1.2, 1),
334
+ ((0.0, 2.0, 0.0), 1.55, 2),
335
+ ]
336
+
337
+ # Test with different thread counts
338
+ sasa_single = sasa.calculate_sasa_internal(atoms, 1.4, 100, 1)
339
+ sasa_multi = sasa.calculate_sasa_internal(atoms, 1.4, 100, 4)
340
+ sasa_auto = sasa.calculate_sasa_internal(atoms, 1.4, 100, -1)
341
+
342
+ # Should return same number of values
343
+ assert len(sasa_single) == len(atoms)
344
+ assert len(sasa_multi) == len(atoms)
345
+ assert len(sasa_auto) == len(atoms)
346
+
347
+ # Results should be nearly identical
348
+ for i in range(len(atoms)):
349
+ assert abs(sasa_single[i] - sasa_multi[i]) < 0.01
350
+ assert abs(sasa_single[i] - sasa_auto[i]) < 0.01
351
+
352
+
353
+ def test_calculate_sasa_internal_at_residue_level_with_threads():
354
+ """Test low-level residue calculation with threads parameter."""
355
+ # Define atoms grouped by residue
356
+ atoms = [
357
+ ((0.0, 0.0, 0.0), 1.7, 1),
358
+ ((1.5, 0.0, 0.0), 1.2, 1),
359
+ ((0.0, 3.0, 0.0), 1.55, 2),
360
+ ]
361
+
362
+ # Test with different thread counts
363
+ residues_single = sasa.calculate_sasa_internal_at_residue_level(atoms, 1.4, 100, 1)
364
+ residues_multi = sasa.calculate_sasa_internal_at_residue_level(atoms, 1.4, 100, 4)
365
+ residues_auto = sasa.calculate_sasa_internal_at_residue_level(atoms, 1.4, 100, -1)
366
+
367
+ # Should return same number of residues
368
+ assert len(residues_single) == 2
369
+ assert len(residues_multi) == 2
370
+ assert len(residues_auto) == 2
371
+
372
+ # Sort by residue_number to ensure consistent ordering
373
+ residues_single = sorted(residues_single, key=lambda r: r.residue_number)
374
+ residues_multi = sorted(residues_multi, key=lambda r: r.residue_number)
375
+ residues_auto = sorted(residues_auto, key=lambda r: r.residue_number)
376
+
377
+ # Results should be nearly identical when matched by residue_number
378
+ for r1, r2, r3 in zip(residues_single, residues_multi, residues_auto):
379
+ assert r1.residue_number == r2.residue_number == r3.residue_number
380
+ assert abs(r1.sasa - r2.sasa) < 0.01
381
+ assert abs(r1.sasa - r3.sasa) < 0.01
@@ -1,33 +0,0 @@
1
- const LANES: usize = 16;
2
- #[allow(clippy::needless_range_loop)]
3
- pub fn simd_sum(values: &[f32]) -> f32 {
4
- let chunks = values.chunks_exact(LANES);
5
- let remainder = chunks.remainder();
6
-
7
- let sum = chunks.fold([0.0f32; LANES], |mut acc, chunk| {
8
- let chunk: [f32; LANES] = chunk.try_into().unwrap();
9
- for i in 0..LANES {
10
- acc[i] += chunk[i];
11
- }
12
- acc
13
- });
14
-
15
- let remainder: f32 = remainder.iter().copied().sum();
16
-
17
- let mut reduced = 0.0f32;
18
- for i in 0..LANES {
19
- reduced += sum[i];
20
- }
21
- reduced + remainder
22
- }
23
-
24
- pub(crate) fn serialize_chain_id(s: &str) -> isize {
25
- let mut result = 0;
26
- for c in s.chars() {
27
- if c.is_ascii_alphabetic() {
28
- let position = c.to_ascii_uppercase() as isize - 64;
29
- result = result * 10 + position;
30
- }
31
- }
32
- result
33
- }
@@ -1,198 +0,0 @@
1
- import time
2
- from pathlib import Path
3
-
4
- import pytest
5
-
6
- import rust_sasa_python as sasa
7
-
8
- # Path to the example CIF file
9
- EXAMPLE_CIF = Path(__file__).parent.parent / "example.cif"
10
-
11
-
12
- def test_protein_calculation():
13
- """Test protein-level SASA calculation."""
14
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
15
- result = calculator.calculate_protein()
16
-
17
- assert isinstance(result, sasa.Protein)
18
- assert result.total > 0
19
- assert result.polar >= 0
20
- assert result.non_polar >= 0
21
- assert abs(result.total - (result.polar + result.non_polar)) < 1.0
22
-
23
-
24
- def test_chain_calculation():
25
- """Test chain-level SASA calculation."""
26
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
27
- results = calculator.calculate_chain()
28
-
29
- assert isinstance(results, list)
30
- assert len(results) > 0
31
-
32
- for chain in results:
33
- assert isinstance(chain, sasa.Chain)
34
- assert isinstance(chain.chain_id, str)
35
- assert chain.sasa >= 0
36
-
37
-
38
- def test_residue_calculation():
39
- """Test residue-level SASA calculation."""
40
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
41
- results = calculator.calculate_residue()
42
-
43
- assert isinstance(results, list)
44
- assert len(results) > 0
45
-
46
- for residue in results:
47
- assert isinstance(residue, sasa.Residue)
48
- assert isinstance(residue.chain_id, str)
49
- assert isinstance(residue.residue_name, str)
50
- assert isinstance(residue.residue_number, int)
51
- assert residue.sasa >= 0
52
-
53
-
54
- def test_atom_calculation():
55
- """Test atom-level SASA calculation."""
56
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
57
- results = calculator.calculate_atom()
58
-
59
- assert isinstance(results, list)
60
- assert len(results) > 0
61
-
62
- for atom_sasa in results:
63
- assert isinstance(atom_sasa, float)
64
- assert atom_sasa >= 0
65
-
66
-
67
- def test_builder_pattern():
68
- """Test configuration using builder pattern."""
69
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.4).with_n_points(500)
70
- result = calculator.calculate_protein()
71
-
72
- assert result.total > 0
73
- assert "probe_radius=1.4" in repr(calculator)
74
- assert "n_points=500" in repr(calculator)
75
-
76
-
77
- def test_different_configurations():
78
- """Test that different configurations give different results."""
79
- calc1 = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.2)
80
- calc2 = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.6)
81
-
82
- result1 = calc1.calculate_protein()
83
- result2 = calc2.calculate_protein()
84
-
85
- # Different probe radii should give different results
86
- assert result1.total != result2.total
87
-
88
-
89
- def test_convenience_functions():
90
- """Test all convenience functions work."""
91
- protein = sasa.calculate_protein_sasa(str(EXAMPLE_CIF))
92
- chains = sasa.calculate_chain_sasa(str(EXAMPLE_CIF))
93
- residues = sasa.calculate_residue_sasa(str(EXAMPLE_CIF))
94
- atoms = sasa.calculate_atom_sasa(str(EXAMPLE_CIF))
95
-
96
- assert isinstance(protein, sasa.Protein)
97
- assert isinstance(chains, list) and len(chains) > 0
98
- assert isinstance(residues, list) and len(residues) > 0
99
- assert isinstance(atoms, list) and len(atoms) > 0
100
-
101
-
102
- def test_consistency_between_granularities():
103
- """Test that different granularities give consistent totals."""
104
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_n_points(500)
105
-
106
- protein_result = calculator.calculate_protein()
107
- chain_results = calculator.calculate_chain()
108
- residue_results = calculator.calculate_residue()
109
- atom_results = calculator.calculate_atom()
110
-
111
- # Sum of parts should approximately equal whole
112
- chain_total = sum(chain.sasa for chain in chain_results)
113
- residue_total = sum(residue.sasa for residue in residue_results)
114
- atom_total = sum(atom_results)
115
-
116
- tolerance = 0.1
117
- assert abs(protein_result.total - chain_total) < tolerance
118
- assert abs(protein_result.total - residue_total) < tolerance
119
- assert abs(protein_result.total - atom_total) < tolerance
120
-
121
-
122
- def test_convenience_vs_calculator():
123
- """Test that convenience functions give same results as calculator."""
124
- # Using calculator
125
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
126
- calc_protein = calculator.calculate_protein()
127
-
128
- # Using convenience function
129
- conv_protein = sasa.calculate_protein_sasa(str(EXAMPLE_CIF))
130
-
131
- # Results should be identical
132
- assert abs(calc_protein.total - conv_protein.total) < 0.001
133
-
134
-
135
- def test_nonexistent_file():
136
- """Test handling of nonexistent file."""
137
- with pytest.raises(Exception):
138
- calculator = sasa.SASACalculator("nonexistent_file.pdb")
139
- calculator.calculate_protein()
140
-
141
-
142
- def test_calculation_speed():
143
- """Test that calculations complete quickly."""
144
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_n_points(1000)
145
-
146
- start_time = time.time()
147
- result = calculator.calculate_protein()
148
- end_time = time.time()
149
-
150
- calculation_time = end_time - start_time
151
- assert calculation_time < 2.0 # Should be very fast
152
- assert result.total > 0
153
-
154
-
155
- def test_repr_methods():
156
- """Test that repr methods work for all objects."""
157
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF))
158
-
159
- # Test calculator repr
160
- assert "SASACalculator(" in repr(calculator)
161
-
162
- # Test result reprs
163
- protein = calculator.calculate_protein()
164
- chains = calculator.calculate_chain()
165
- residues = calculator.calculate_residue()
166
-
167
- assert "Protein(" in repr(protein)
168
- if chains:
169
- assert "Chain(" in repr(chains[0])
170
- if residues:
171
- assert "Residue(" in repr(residues[0])
172
-
173
-
174
- def test_workflow_example():
175
- """Test a realistic workflow."""
176
- # Quick calculation
177
- quick_result = sasa.calculate_protein_sasa(str(EXAMPLE_CIF))
178
- assert quick_result.total > 0
179
-
180
- # Detailed analysis
181
- calculator = sasa.SASACalculator(str(EXAMPLE_CIF)).with_probe_radius(1.4).with_n_points(500)
182
-
183
- protein = calculator.calculate_protein()
184
- chains = calculator.calculate_chain()
185
- residues = calculator.calculate_residue()
186
-
187
- # All should be valid
188
- assert protein.total > 0
189
- assert len(chains) >= 1
190
- assert len(residues) >= 1
191
-
192
- # Should have expected structure
193
- if residues:
194
- first_residue = residues[0]
195
- assert hasattr(first_residue, "chain_id")
196
- assert hasattr(first_residue, "residue_name")
197
- assert hasattr(first_residue, "residue_number")
198
- assert first_residue.sasa >= 0