shape-complementarity 0.1.0__cp310-abi3-win_amd64.whl
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.
- shape_complementarity/__init__.py +20 -0
- shape_complementarity/_core.pyd +0 -0
- shape_complementarity/batch.py +80 -0
- shape_complementarity/io.py +402 -0
- shape_complementarity-0.1.0.dist-info/METADATA +12 -0
- shape_complementarity-0.1.0.dist-info/RECORD +9 -0
- shape_complementarity-0.1.0.dist-info/WHEEL +4 -0
- shape_complementarity-0.1.0.dist-info/licenses/LICENSE +21 -0
- shape_complementarity-0.1.0.dist-info/sboms/shape-complementarity.cyclonedx.json +1380 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from shape_complementarity._core import ScResult, compute_sc
|
|
2
|
+
from shape_complementarity.batch import score_many
|
|
3
|
+
from shape_complementarity.io import (
|
|
4
|
+
from_biotite,
|
|
5
|
+
from_boltzgen_refold,
|
|
6
|
+
from_boltzgen_structure,
|
|
7
|
+
from_pdb,
|
|
8
|
+
from_structure,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"compute_sc",
|
|
13
|
+
"ScResult",
|
|
14
|
+
"from_pdb",
|
|
15
|
+
"from_structure",
|
|
16
|
+
"from_biotite",
|
|
17
|
+
"from_boltzgen_structure",
|
|
18
|
+
"from_boltzgen_refold",
|
|
19
|
+
"score_many",
|
|
20
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Multiprocessing batch scoring of PDB files.
|
|
2
|
+
|
|
3
|
+
Uses ProcessPoolExecutor with the 'spawn' start method (required on macOS).
|
|
4
|
+
Rust-side Rayon parallelism is disabled by default to avoid oversubscription
|
|
5
|
+
when many worker processes are already running concurrently.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
import multiprocessing as mp
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _score_one(args: tuple) -> dict:
|
|
15
|
+
"""Top-level worker function (must be picklable — no closures)."""
|
|
16
|
+
pdb_path, chains_a, chains_b, kwargs = args
|
|
17
|
+
try:
|
|
18
|
+
from shape_complementarity.io import from_pdb
|
|
19
|
+
|
|
20
|
+
result = from_pdb(pdb_path, chains_a, chains_b, **kwargs)
|
|
21
|
+
return {
|
|
22
|
+
"path": str(pdb_path),
|
|
23
|
+
"sc": result.sc,
|
|
24
|
+
"median_distance": result.median_distance,
|
|
25
|
+
"trimmed_area": result.trimmed_area,
|
|
26
|
+
"atoms_a": result.atoms_a,
|
|
27
|
+
"atoms_b": result.atoms_b,
|
|
28
|
+
"status": "ok",
|
|
29
|
+
"error": None,
|
|
30
|
+
}
|
|
31
|
+
except Exception as exc: # noqa: BLE001
|
|
32
|
+
return {
|
|
33
|
+
"path": str(pdb_path),
|
|
34
|
+
"sc": float("nan"),
|
|
35
|
+
"median_distance": float("nan"),
|
|
36
|
+
"trimmed_area": float("nan"),
|
|
37
|
+
"atoms_a": 0,
|
|
38
|
+
"atoms_b": 0,
|
|
39
|
+
"status": "error",
|
|
40
|
+
"error": str(exc),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def score_many(
|
|
45
|
+
pdb_paths: list,
|
|
46
|
+
chains_a: list[str],
|
|
47
|
+
chains_b: list[str] | None = None,
|
|
48
|
+
n_workers: int = 8,
|
|
49
|
+
parallel: bool = False,
|
|
50
|
+
**kwargs,
|
|
51
|
+
) -> "pd.DataFrame":
|
|
52
|
+
"""Score many PDB files in parallel.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
pdb_paths: list of file paths
|
|
56
|
+
chains_a: chain IDs for molecule A
|
|
57
|
+
chains_b: chain IDs for molecule B (None = complement of chains_a)
|
|
58
|
+
n_workers: number of worker processes
|
|
59
|
+
parallel: enable Rayon parallelism inside each worker (default False
|
|
60
|
+
to avoid oversubscription with multiple processes)
|
|
61
|
+
**kwargs: forwarded to from_pdb (model, include_hetatm, etc.)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
DataFrame with columns:
|
|
65
|
+
path, sc, median_distance, trimmed_area, atoms_a, atoms_b,
|
|
66
|
+
status ('ok' or 'error'), error (None or message string)
|
|
67
|
+
"""
|
|
68
|
+
import pandas as pd
|
|
69
|
+
|
|
70
|
+
kwargs["parallel"] = parallel
|
|
71
|
+
|
|
72
|
+
args_list = [(str(p), chains_a, chains_b, kwargs) for p in pdb_paths]
|
|
73
|
+
|
|
74
|
+
ctx = mp.get_context("spawn")
|
|
75
|
+
with concurrent.futures.ProcessPoolExecutor(
|
|
76
|
+
max_workers=n_workers, mp_context=ctx
|
|
77
|
+
) as executor:
|
|
78
|
+
rows = list(executor.map(_score_one, args_list))
|
|
79
|
+
|
|
80
|
+
return pd.DataFrame(rows)
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""PDB/CIF parsing for pysc.
|
|
2
|
+
|
|
3
|
+
Parsing logic mirrors src/bin/sc.rs in sc-rs exactly so that parity tests pass:
|
|
4
|
+
- ATOM records only (HETATM skipped unless include_hetatm=True)
|
|
5
|
+
- Alternate locations: keep ' ' and 'A', skip all others
|
|
6
|
+
- Hydrogens: excluded by default using the same heuristic as the CLI
|
|
7
|
+
|
|
8
|
+
BoltzGen integration (from_biotite, from_boltzgen_structure, from_boltzgen_refold)
|
|
9
|
+
is appended at the bottom. The recommended entry point for BoltzGen output is
|
|
10
|
+
from_boltzgen_refold() on the refold_cif/*.cif files, which contain full all-atom
|
|
11
|
+
coordinates validated by Boltz. from_pdb() also works for the same files.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from Bio.PDB.Structure import Structure as BioStructure
|
|
22
|
+
|
|
23
|
+
from shape_complementarity._core import ScResult, compute_sc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Hydrogen detection (mirrors sc-rs bin/sc.rs) ────────────────────────────
|
|
27
|
+
|
|
28
|
+
def _is_hydrogen(atom_name: str, element: str) -> bool:
|
|
29
|
+
"""Mirror the hydrogen-detection logic in sc-rs bin/sc.rs."""
|
|
30
|
+
elem = element.strip().upper()
|
|
31
|
+
name = atom_name.strip()
|
|
32
|
+
if elem == "H":
|
|
33
|
+
return True
|
|
34
|
+
if name.startswith("H"):
|
|
35
|
+
return True
|
|
36
|
+
if name.endswith("H"):
|
|
37
|
+
return True
|
|
38
|
+
# Catch names like "1H", "2HB" (digit-prefixed hydrogen names)
|
|
39
|
+
if "H" in name and name[:1].isdigit():
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── biopython helpers ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def _select_real_atom(atom):
|
|
47
|
+
"""Return a concrete (non-disordered) Atom for altloc ' ' or 'A'.
|
|
48
|
+
|
|
49
|
+
Returns None if no acceptable altloc exists.
|
|
50
|
+
"""
|
|
51
|
+
if atom.is_disordered():
|
|
52
|
+
child_dict = atom.child_dict
|
|
53
|
+
for altloc in ("A", " "):
|
|
54
|
+
if altloc in child_dict:
|
|
55
|
+
return child_dict[altloc]
|
|
56
|
+
return None
|
|
57
|
+
altloc = atom.altloc
|
|
58
|
+
if altloc not in (" ", "A"):
|
|
59
|
+
return None
|
|
60
|
+
return atom
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_atom_arrays(
|
|
64
|
+
model,
|
|
65
|
+
chains: list[str],
|
|
66
|
+
include_hetatm: bool,
|
|
67
|
+
include_hydrogens: bool,
|
|
68
|
+
) -> tuple[list[list[float]], list[str], list[str]]:
|
|
69
|
+
coords: list[list[float]] = []
|
|
70
|
+
atom_names: list[str] = []
|
|
71
|
+
res_names: list[str] = []
|
|
72
|
+
|
|
73
|
+
for chain in model.get_chains():
|
|
74
|
+
if chain.id not in chains:
|
|
75
|
+
continue
|
|
76
|
+
for residue in chain.get_residues():
|
|
77
|
+
# residue.id[0] == ' ' for standard ATOM records
|
|
78
|
+
het = residue.id[0]
|
|
79
|
+
if not include_hetatm and het != " ":
|
|
80
|
+
continue
|
|
81
|
+
for disordered_or_atom in residue.get_atoms():
|
|
82
|
+
real = _select_real_atom(disordered_or_atom)
|
|
83
|
+
if real is None:
|
|
84
|
+
continue
|
|
85
|
+
atom_name = real.name.strip()
|
|
86
|
+
element = (real.element or "").strip()
|
|
87
|
+
if not include_hydrogens and _is_hydrogen(atom_name, element):
|
|
88
|
+
continue
|
|
89
|
+
c = real.coord
|
|
90
|
+
coords.append([float(c[0]), float(c[1]), float(c[2])])
|
|
91
|
+
atom_names.append(atom_name)
|
|
92
|
+
res_names.append(residue.resname.strip())
|
|
93
|
+
|
|
94
|
+
return coords, atom_names, res_names
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _load_structure(path: str | Path):
|
|
98
|
+
from Bio.PDB import MMCIFParser, PDBParser
|
|
99
|
+
|
|
100
|
+
path = Path(path)
|
|
101
|
+
suffix = path.suffix.lower()
|
|
102
|
+
if suffix in (".cif", ".mmcif"):
|
|
103
|
+
parser = MMCIFParser(QUIET=True)
|
|
104
|
+
else:
|
|
105
|
+
parser = PDBParser(QUIET=True)
|
|
106
|
+
return parser.get_structure(path.stem, str(path))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── Public biopython-based API ───────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def from_structure(
|
|
112
|
+
structure: "BioStructure",
|
|
113
|
+
chains_a: list[str],
|
|
114
|
+
chains_b: list[str] | None = None,
|
|
115
|
+
model: int = 0,
|
|
116
|
+
include_hetatm: bool = False,
|
|
117
|
+
include_hydrogens: bool = False,
|
|
118
|
+
parallel: bool = True,
|
|
119
|
+
) -> ScResult:
|
|
120
|
+
"""Compute SC from a biopython Structure object.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
structure: biopython Structure (any source)
|
|
124
|
+
chains_a: chain IDs for molecule A
|
|
125
|
+
chains_b: chain IDs for molecule B; None = all chains not in chains_a
|
|
126
|
+
model: model index (0-based)
|
|
127
|
+
include_hetatm: include HETATM residues (default False, matches sc-rs)
|
|
128
|
+
include_hydrogens: include hydrogen atoms (default False, matches sc-rs)
|
|
129
|
+
parallel: enable Rayon parallelism inside sc-rs
|
|
130
|
+
"""
|
|
131
|
+
models = list(structure.get_models())
|
|
132
|
+
if model >= len(models):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"model index {model} out of range (structure has {len(models)} model(s))"
|
|
135
|
+
)
|
|
136
|
+
m = models[model]
|
|
137
|
+
|
|
138
|
+
if chains_b is None:
|
|
139
|
+
all_chain_ids = {ch.id for ch in m.get_chains()}
|
|
140
|
+
chains_b = sorted(all_chain_ids - set(chains_a))
|
|
141
|
+
|
|
142
|
+
coords_a, names_a, res_a = _extract_atom_arrays(m, chains_a, include_hetatm, include_hydrogens)
|
|
143
|
+
coords_b, names_b, res_b = _extract_atom_arrays(m, chains_b, include_hetatm, include_hydrogens)
|
|
144
|
+
|
|
145
|
+
return compute_sc(coords_a, names_a, res_a, coords_b, names_b, res_b, parallel)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def from_pdb(
|
|
149
|
+
pdb_path: str | Path,
|
|
150
|
+
chains_a: list[str],
|
|
151
|
+
chains_b: list[str] | None = None,
|
|
152
|
+
model: int = 0,
|
|
153
|
+
include_hetatm: bool = False,
|
|
154
|
+
include_hydrogens: bool = False,
|
|
155
|
+
parallel: bool = True,
|
|
156
|
+
) -> ScResult:
|
|
157
|
+
"""Compute SC from a PDB or mmCIF file.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
pdb_path: path to .pdb, .ent, or .cif file
|
|
161
|
+
chains_a: chain IDs for molecule A
|
|
162
|
+
chains_b: chain IDs for molecule B; None = all chains not in chains_a
|
|
163
|
+
model: model index (0-based, default first model)
|
|
164
|
+
include_hetatm: include HETATM residues (default False)
|
|
165
|
+
include_hydrogens: include hydrogen atoms (default False)
|
|
166
|
+
parallel: enable Rayon parallelism inside sc-rs
|
|
167
|
+
"""
|
|
168
|
+
structure = _load_structure(pdb_path)
|
|
169
|
+
return from_structure(
|
|
170
|
+
structure,
|
|
171
|
+
chains_a,
|
|
172
|
+
chains_b,
|
|
173
|
+
model=model,
|
|
174
|
+
include_hetatm=include_hetatm,
|
|
175
|
+
include_hydrogens=include_hydrogens,
|
|
176
|
+
parallel=parallel,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── BoltzGen integration ─────────────────────────────────────────────────────
|
|
181
|
+
#
|
|
182
|
+
# BoltzGen pipeline output layout (relative to output_dir):
|
|
183
|
+
#
|
|
184
|
+
# intermediate_designs/<id>.npz – Structure NPZ, backbone only
|
|
185
|
+
# (sidechains are [0,0,0])
|
|
186
|
+
# intermediate_designs_inverse_folded/
|
|
187
|
+
# <id>.npz – Structure NPZ, full all-atom
|
|
188
|
+
# fold_out_npz/<id>.npz – raw fold tensors (coords + confidences)
|
|
189
|
+
# refold_cif/<id>.cif – ← USE THIS for SC scoring
|
|
190
|
+
# refold_design_cif/<id>.cif – binder-only refold
|
|
191
|
+
#
|
|
192
|
+
# The refold_cif files are full-atom mmCIF with pLDDT in B-factors. They are
|
|
193
|
+
# the Boltz-validated structures and the right input for SC. Both from_pdb()
|
|
194
|
+
# and from_boltzgen_refold() accept them.
|
|
195
|
+
#
|
|
196
|
+
# Chain naming in BoltzGen output: the binder chain is typically the last chain
|
|
197
|
+
# (e.g. "B" when the target is "A"), but verify from the CIF or the Record JSON
|
|
198
|
+
# rather than assuming.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _extract_boltzgen_chains(
|
|
202
|
+
structure,
|
|
203
|
+
target_chains: list[str],
|
|
204
|
+
include_hydrogens: bool,
|
|
205
|
+
) -> tuple[list[list[float]], list[str], list[str]]:
|
|
206
|
+
"""Extract atom arrays from a BoltzGen Structure object for the given chains.
|
|
207
|
+
|
|
208
|
+
Iterates chains → residues → atoms using the absolute-index layout of
|
|
209
|
+
Structure.chains / .residues / .atoms (all indices are into the global arrays).
|
|
210
|
+
Skips atoms where is_present is False.
|
|
211
|
+
"""
|
|
212
|
+
all_chain_names = [str(n) for n in structure.chains["name"]]
|
|
213
|
+
target_set = set(target_chains)
|
|
214
|
+
|
|
215
|
+
coords: list[list[float]] = []
|
|
216
|
+
atom_names: list[str] = []
|
|
217
|
+
res_names: list[str] = []
|
|
218
|
+
|
|
219
|
+
for ci, chain in enumerate(structure.chains):
|
|
220
|
+
if str(chain["name"]) not in target_set:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
res_start = int(chain["res_idx"])
|
|
224
|
+
res_count = int(chain["res_num"])
|
|
225
|
+
|
|
226
|
+
for ri in range(res_start, res_start + res_count):
|
|
227
|
+
res = structure.residues[ri]
|
|
228
|
+
res_name = str(res["name"])
|
|
229
|
+
a_start = int(res["atom_idx"])
|
|
230
|
+
a_count = int(res["atom_num"])
|
|
231
|
+
|
|
232
|
+
for ai in range(a_start, a_start + a_count):
|
|
233
|
+
atom = structure.atoms[ai]
|
|
234
|
+
if not bool(atom["is_present"]):
|
|
235
|
+
continue
|
|
236
|
+
atom_name = str(atom["name"])
|
|
237
|
+
if not include_hydrogens and _is_hydrogen(atom_name, ""):
|
|
238
|
+
continue
|
|
239
|
+
c = atom["coords"]
|
|
240
|
+
coords.append([float(c[0]), float(c[1]), float(c[2])])
|
|
241
|
+
atom_names.append(atom_name)
|
|
242
|
+
res_names.append(res_name)
|
|
243
|
+
|
|
244
|
+
missing = target_set - set(all_chain_names)
|
|
245
|
+
if missing:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"Chain(s) {sorted(missing)} not found in Structure. "
|
|
248
|
+
f"Available: {list(dict.fromkeys(all_chain_names))}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return coords, atom_names, res_names
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def from_boltzgen_structure(
|
|
255
|
+
structure,
|
|
256
|
+
chains_a: list[str],
|
|
257
|
+
chains_b: list[str] | None = None,
|
|
258
|
+
include_hydrogens: bool = False,
|
|
259
|
+
parallel: bool = True,
|
|
260
|
+
) -> ScResult:
|
|
261
|
+
"""Compute SC from an in-memory BoltzGen Structure object.
|
|
262
|
+
|
|
263
|
+
Accepts any object with .atoms / .residues / .chains numpy structured arrays
|
|
264
|
+
matching the BoltzGen dtype layout (boltzgen.data.data.Structure).
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
structure: BoltzGen Structure (or duck-typed equivalent)
|
|
268
|
+
chains_a: chain IDs for molecule A (e.g. ["B"] for binder)
|
|
269
|
+
chains_b: chain IDs for molecule B; None = all chains not in chains_a
|
|
270
|
+
include_hydrogens: include hydrogen atoms (default False)
|
|
271
|
+
parallel: enable Rayon parallelism inside sc-rs
|
|
272
|
+
|
|
273
|
+
Important: use post-refold structures for meaningful SC scores. Structures
|
|
274
|
+
from intermediate_designs/ have zeroed sidechain coordinates and will give
|
|
275
|
+
unreliable results. Prefer from_boltzgen_refold() on the refold_cif files,
|
|
276
|
+
or load the Structure NPZ from intermediate_designs_inverse_folded/ after
|
|
277
|
+
refolding completes.
|
|
278
|
+
"""
|
|
279
|
+
all_chain_names = [str(n) for n in structure.chains["name"]]
|
|
280
|
+
|
|
281
|
+
if chains_b is None:
|
|
282
|
+
chains_a_set = set(chains_a)
|
|
283
|
+
chains_b = list(dict.fromkeys(
|
|
284
|
+
n for n in all_chain_names if n not in chains_a_set
|
|
285
|
+
))
|
|
286
|
+
|
|
287
|
+
coords_a, names_a, res_a = _extract_boltzgen_chains(structure, chains_a, include_hydrogens)
|
|
288
|
+
coords_b, names_b, res_b = _extract_boltzgen_chains(structure, chains_b, include_hydrogens)
|
|
289
|
+
|
|
290
|
+
return compute_sc(coords_a, names_a, res_a, coords_b, names_b, res_b, parallel)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def from_biotite(
|
|
294
|
+
atom_array,
|
|
295
|
+
chains_a: list[str],
|
|
296
|
+
chains_b: list[str] | None = None,
|
|
297
|
+
include_hetatm: bool = False,
|
|
298
|
+
include_hydrogens: bool = False,
|
|
299
|
+
parallel: bool = True,
|
|
300
|
+
) -> ScResult:
|
|
301
|
+
"""Compute SC from a biotite AtomArray or AtomArrayStack (first model used).
|
|
302
|
+
|
|
303
|
+
BoltzGen's analysis stack (analyze_utils.py) works with biotite AtomArrays.
|
|
304
|
+
This function is the natural bridge when you already have an AtomArray loaded.
|
|
305
|
+
|
|
306
|
+
Example — scoring a BoltzGen refold CIF with biotite directly:
|
|
307
|
+
|
|
308
|
+
import biotite.structure.io.pdbx as pdbx
|
|
309
|
+
cif = pdbx.CIFFile.read("refold_cif/design_0.cif")
|
|
310
|
+
atoms = pdbx.get_structure(cif, model=1, use_author_fields=False)
|
|
311
|
+
result = pysc.from_biotite(atoms, chains_a=["B"], chains_b=["A"])
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
atom_array: biotite AtomArray (or first-model slice of AtomArrayStack)
|
|
315
|
+
chains_a: chain IDs for molecule A
|
|
316
|
+
chains_b: chain IDs for molecule B; None = all chains not in chains_a
|
|
317
|
+
include_hetatm: include hetero atoms (default False)
|
|
318
|
+
include_hydrogens: include hydrogen atoms (default False)
|
|
319
|
+
parallel: enable Rayon parallelism inside sc-rs
|
|
320
|
+
"""
|
|
321
|
+
# Support AtomArrayStack by taking the first model
|
|
322
|
+
try:
|
|
323
|
+
import biotite.structure as struc
|
|
324
|
+
if isinstance(atom_array, struc.AtomArrayStack):
|
|
325
|
+
atom_array = atom_array[0]
|
|
326
|
+
except ImportError:
|
|
327
|
+
pass # duck-typing fallback: assume it already behaves like an AtomArray
|
|
328
|
+
|
|
329
|
+
all_chains = list(dict.fromkeys(str(c) for c in atom_array.chain_id))
|
|
330
|
+
if chains_b is None:
|
|
331
|
+
chains_a_set = set(chains_a)
|
|
332
|
+
chains_b = [c for c in all_chains if c not in chains_a_set]
|
|
333
|
+
|
|
334
|
+
def _extract(chain_ids: list[str]) -> tuple[list, list, list]:
|
|
335
|
+
mask = np.zeros(len(atom_array), dtype=bool)
|
|
336
|
+
for ch in chain_ids:
|
|
337
|
+
mask |= atom_array.chain_id == ch
|
|
338
|
+
if not include_hetatm:
|
|
339
|
+
mask &= ~atom_array.hetero
|
|
340
|
+
sub = atom_array[mask]
|
|
341
|
+
|
|
342
|
+
coords: list[list[float]] = []
|
|
343
|
+
anames: list[str] = []
|
|
344
|
+
rnames: list[str] = []
|
|
345
|
+
for i in range(len(sub)):
|
|
346
|
+
name = str(sub.atom_name[i])
|
|
347
|
+
elem = str(sub.element[i]) if hasattr(sub, "element") else ""
|
|
348
|
+
if not include_hydrogens and _is_hydrogen(name, elem):
|
|
349
|
+
continue
|
|
350
|
+
c = sub.coord[i]
|
|
351
|
+
coords.append([float(c[0]), float(c[1]), float(c[2])])
|
|
352
|
+
anames.append(name)
|
|
353
|
+
rnames.append(str(sub.res_name[i]))
|
|
354
|
+
return coords, anames, rnames
|
|
355
|
+
|
|
356
|
+
coords_a, names_a, res_a = _extract(chains_a)
|
|
357
|
+
coords_b, names_b, res_b = _extract(chains_b)
|
|
358
|
+
return compute_sc(coords_a, names_a, res_a, coords_b, names_b, res_b, parallel)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def from_boltzgen_refold(
|
|
362
|
+
refold_cif_path: str | Path,
|
|
363
|
+
chains_a: list[str],
|
|
364
|
+
chains_b: list[str] | None = None,
|
|
365
|
+
include_hydrogens: bool = False,
|
|
366
|
+
parallel: bool = True,
|
|
367
|
+
) -> ScResult:
|
|
368
|
+
"""Compute SC from a BoltzGen refold_cif/*.cif file using biotite.
|
|
369
|
+
|
|
370
|
+
This is the recommended entry point when scoring BoltzGen designs.
|
|
371
|
+
It mirrors the CIF-loading pattern used by BoltzGen's own analyze_utils.py.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
refold_cif_path: path to refold_cif/<id>.cif or refold_design_cif/<id>.cif
|
|
375
|
+
chains_a: chain IDs for molecule A (typically the binder)
|
|
376
|
+
chains_b: chain IDs for molecule B; None = all chains not in chains_a
|
|
377
|
+
include_hydrogens: include hydrogen atoms (default False)
|
|
378
|
+
parallel: enable Rayon parallelism inside sc-rs
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
ImportError: if biotite is not installed. Install with: pip install biotite
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
import biotite.structure.io.pdbx as pdbx
|
|
385
|
+
except ImportError as exc:
|
|
386
|
+
raise ImportError(
|
|
387
|
+
"biotite is required for from_boltzgen_refold(). "
|
|
388
|
+
"Install it with: pip install biotite"
|
|
389
|
+
) from exc
|
|
390
|
+
|
|
391
|
+
cif_file = pdbx.CIFFile.read(str(refold_cif_path))
|
|
392
|
+
# model=1 is 1-based in biotite; use_author_fields=False uses label_* fields
|
|
393
|
+
# (consistent with how BoltzGen writes chain IDs)
|
|
394
|
+
atom_array = pdbx.get_structure(cif_file, model=1, use_author_fields=False)
|
|
395
|
+
return from_biotite(
|
|
396
|
+
atom_array,
|
|
397
|
+
chains_a,
|
|
398
|
+
chains_b,
|
|
399
|
+
include_hetatm=False,
|
|
400
|
+
include_hydrogens=include_hydrogens,
|
|
401
|
+
parallel=parallel,
|
|
402
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shape-complementarity
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Dist: numpy>=1.24
|
|
5
|
+
Requires-Dist: biopython>=1.83
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Summary: PyO3 bindings to sc-rs for Lawrence-Colman Shape Complementarity
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/aarteixeira/shape-complementarity/issues
|
|
11
|
+
Project-URL: Homepage, https://github.com/aarteixeira/shape-complementarity
|
|
12
|
+
Project-URL: Repository, https://github.com/aarteixeira/shape-complementarity
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
shape_complementarity/__init__.py,sha256=2oRvOqa5HGZdzwCnbcfOP3EVJTjYYSa-eDgKhMdKNfg,464
|
|
2
|
+
shape_complementarity/_core.pyd,sha256=LONuhCzz0OXGI3K4zSrooHfsUre_wVzocVRuxE4pz0Y,671744
|
|
3
|
+
shape_complementarity/batch.py,sha256=WGcowAbk1o-kvNMJljLK0Y33zkta5W0JEB31ccgdW8E,2657
|
|
4
|
+
shape_complementarity/io.py,sha256=WTaoF8JraQy-0J_xyFTiKcwEk4NaSKnzzqQcXu4c1AY,15839
|
|
5
|
+
shape_complementarity-0.1.0.dist-info/METADATA,sha256=JbHV5DCxUPnx8T4wevEaf_EFT-VVqRGM6BqfsEDuR5w,496
|
|
6
|
+
shape_complementarity-0.1.0.dist-info/WHEEL,sha256=OUT0XP5TL9Hq-6CIgsb5m6BAU8pfcNqYjx0xnFDWhNs,96
|
|
7
|
+
shape_complementarity-0.1.0.dist-info/licenses/LICENSE,sha256=XKKSDU9WlUEAyPNlRhq6e2xhVNpJc097JwPZJ1rUnRE,1077
|
|
8
|
+
shape_complementarity-0.1.0.dist-info/sboms/shape-complementarity.cyclonedx.json,sha256=NoXa_punHoyjufzla--bJWmuH99mafVybrK-39pDXys,42436
|
|
9
|
+
shape_complementarity-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|