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.
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/Cargo.lock +45 -13
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/Cargo.toml +3 -2
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/DOCS.md +59 -8
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/PKG-INFO +2 -2
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/README.md +1 -1
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/src/lib.rs +103 -39
- rust_sasa_python-0.5.0/src/simd.rs +15 -0
- rust_sasa_python-0.5.0/tests/test_sasa.py +381 -0
- rust_sasa_python-0.3.0/src/simd.rs +0 -33
- rust_sasa_python-0.3.0/tests/test_sasa.py +0 -198
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/.github/workflows/CI.yml +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/.gitignore +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/LICENSE.md +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/example.cif +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/pyproject.toml +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/pytest.ini +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/rust_sasa_python.pyi +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/tests/__init__.py +0 -0
- {rust_sasa_python-0.3.0 → rust_sasa_python-0.5.0}/uv.lock +0 -0
|
@@ -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.
|
|
117
|
+
version = "4.5.53"
|
|
112
118
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
113
|
-
checksum = "
|
|
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.
|
|
127
|
+
version = "4.5.53"
|
|
122
128
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
123
|
-
checksum = "
|
|
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.
|
|
295
|
+
version = "0.18.3"
|
|
284
296
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
285
|
-
checksum = "
|
|
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.
|
|
530
|
+
version = "0.22.2"
|
|
519
531
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
520
|
-
checksum = "
|
|
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.
|
|
615
|
+
version = "0.38.4"
|
|
595
616
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
596
|
-
checksum = "
|
|
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.
|
|
695
|
+
version = "0.7.0"
|
|
666
696
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
667
|
-
checksum = "
|
|
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.
|
|
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
|
+
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.
|
|
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:
|
|
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
|
|
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`:
|
|
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} Ų
|
|
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
|
+
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**: **
|
|
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**: **
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|