@platforma-open/milaboratories.3d-structure-prediction.software 1.1.0 → 1.1.1
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.
- package/dist/artifacts/py-archive/archive.json +1 -1
- package/dist/artifacts/py-docker/docker_x64.json +1 -1
- package/dist/tengo/software/immunebuilder-predict.sw.json +1 -1
- package/package.json +9 -6
- package/.turbo/turbo-build.log +0 -423
- package/CHANGELOG.md +0 -65
- package/Dockerfile +0 -63
- package/pkg-platforma-open-milaboratories.3d-structure-prediction.software-py-archive-1.1.0.tgz +0 -0
- package/src_python/numbering.py +0 -103
- package/src_python/pdb_writer.py +0 -136
- package/src_python/requirements.txt +0 -5
- package/src_python/run_immunebuilder.py +0 -676
- package/src_python/sanitize.py +0 -150
package/src_python/numbering.py
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""IMGT numbering helpers.
|
|
2
|
-
|
|
3
|
-
ImmuneBuilder runs ANARCI internally and exposes numbered residues on the
|
|
4
|
-
Antibody object. Default scheme is typically Chothia unless configured. Until
|
|
5
|
-
runtime testing confirms the scheme emitted by `antibody.numbered_sequences`,
|
|
6
|
-
we trust what comes out and record the scheme in provenance; if a Chothia
|
|
7
|
-
fallback is needed, switch to post-hoc renumbering via the `anarci` CLI here.
|
|
8
|
-
|
|
9
|
-
IMGT CDR ranges (per spec R27):
|
|
10
|
-
CDRH1: 27-38 CDRH2: 56-65 CDRH3: 105-117
|
|
11
|
-
CDRL1: 27-38 CDRL2: 56-65 CDRL3: 105-117
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
|
|
18
|
-
IMGT_CDR_RANGES: dict[str, dict[str, tuple[int, int]]] = {
|
|
19
|
-
"H": {"CDR1": (27, 38), "CDR2": (56, 65), "CDR3": (105, 117)},
|
|
20
|
-
"L": {"CDR1": (27, 38), "CDR2": (56, 65), "CDR3": (105, 117)},
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class NumberedResidue:
|
|
26
|
-
chain: str # "H" or "L"
|
|
27
|
-
pos: str # IMGT position as string; may include insertion code (e.g. "111A").
|
|
28
|
-
aa: str
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
def imgt_int(self) -> int:
|
|
32
|
-
digits = "".join(c for c in self.pos if c.isdigit())
|
|
33
|
-
return int(digits) if digits else 0
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def region(self) -> str:
|
|
37
|
-
ranges = IMGT_CDR_RANGES.get(self.chain, {})
|
|
38
|
-
n = self.imgt_int
|
|
39
|
-
for name, (lo, hi) in ranges.items():
|
|
40
|
-
if lo <= n <= hi:
|
|
41
|
-
return name
|
|
42
|
-
return "FR"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def extract_numbered_residues(antibody) -> list[NumberedResidue]:
|
|
46
|
-
"""Pull residue numbering out of an ImmuneBuilder Antibody.
|
|
47
|
-
|
|
48
|
-
`antibody.numbered_sequences` is {'H': [((pos, ins), aa), ...], 'L': ...}.
|
|
49
|
-
For VHH only 'H' is present. Residues are returned in N-to-C order per
|
|
50
|
-
chain, heavy first then light.
|
|
51
|
-
"""
|
|
52
|
-
out: list[NumberedResidue] = []
|
|
53
|
-
numbered = getattr(antibody, "numbered_sequences", {}) or {}
|
|
54
|
-
for chain in ("H", "L"):
|
|
55
|
-
chain_data = numbered.get(chain, [])
|
|
56
|
-
for entry in chain_data:
|
|
57
|
-
(pos, ins), aa = entry
|
|
58
|
-
pos_str = f"{pos}{ins.strip()}" if ins and ins.strip() else str(pos)
|
|
59
|
-
out.append(NumberedResidue(chain=chain, pos=pos_str, aa=aa))
|
|
60
|
-
return out
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def cdr_ranges_in_pdb_notation(chain: str) -> dict[str, tuple[str, str]]:
|
|
64
|
-
"""Return CDR ranges formatted for REMARK 99 CDR* records.
|
|
65
|
-
|
|
66
|
-
Example: {"CDR1": ("H27", "H38"), ...} for chain H.
|
|
67
|
-
"""
|
|
68
|
-
ranges = IMGT_CDR_RANGES.get(chain, {})
|
|
69
|
-
return {
|
|
70
|
-
name: (f"{chain}{lo}", f"{chain}{hi}")
|
|
71
|
-
for name, (lo, hi) in ranges.items()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def cdrh3_length(residues: list[NumberedResidue]) -> int:
|
|
76
|
-
"""Count residues in CDRH3."""
|
|
77
|
-
return sum(1 for r in residues if r.chain == "H" and r.region == "CDR3")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# IMGT 42/49/50/52 are the same four physical FR2 residues historically
|
|
81
|
-
# described as Kabat 37/44/45/47. FR2 has no insertion-prone loops, so the
|
|
82
|
-
# correspondence is canonical (Vincke et al. 2009; Mitchell & Colwell 2018).
|
|
83
|
-
VHH_HALLMARKS_IMGT: dict[int, tuple[str, ...]] = {
|
|
84
|
-
42: ("F", "Y"), # canonical VHH; conventional VH carries V
|
|
85
|
-
49: ("E",), # canonical VHH; conventional VH carries G
|
|
86
|
-
50: ("R",), # canonical VHH; conventional VH carries L
|
|
87
|
-
52: ("G",), # canonical VHH; conventional VH carries W
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def vhh_hallmarks_present(residues: list[NumberedResidue]) -> bool:
|
|
92
|
-
"""R15 — return True when the heavy chain looks like a canonical VHH.
|
|
93
|
-
|
|
94
|
-
Reads IMGT-numbered residues already extracted from the predicted Antibody
|
|
95
|
-
(no second ANARCI call). Tolerates one humanized position: True iff at
|
|
96
|
-
least 2 of the 4 hallmark positions carry a canonical VHH residue.
|
|
97
|
-
"""
|
|
98
|
-
by_pos = {r.imgt_int: r.aa for r in residues if r.chain == "H"}
|
|
99
|
-
matches = sum(
|
|
100
|
-
1 for pos, expected in VHH_HALLMARKS_IMGT.items()
|
|
101
|
-
if by_pos.get(pos) in expected
|
|
102
|
-
)
|
|
103
|
-
return matches >= 2
|
package/src_python/pdb_writer.py
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
"""PDB writer that augments ImmuneBuilder output with REMARKs.
|
|
2
|
-
|
|
3
|
-
ImmuneBuilder writes a standard PDB. Per spec R26/R28/R29 we need to:
|
|
4
|
-
- Clamp per-atom B-factor to [0.00, 99.99] to fit PDB `%6.2f` precision.
|
|
5
|
-
- Inject a TITLE line identifying the predictor.
|
|
6
|
-
- Inject REMARK 99 PROVENANCE block (version, seed, block version).
|
|
7
|
-
- Inject REMARK 99 CDR* records with IMGT CDR ranges.
|
|
8
|
-
|
|
9
|
-
PDB content must be byte-stable for identical inputs so the platforma
|
|
10
|
-
backend can cache and reuse downstream results — a wall-clock prediction
|
|
11
|
-
timestamp would break every run's CID, so we omit it. The seed + version
|
|
12
|
-
fields plus the input sequences are sufficient to reproduce the prediction.
|
|
13
|
-
|
|
14
|
-
The cleanest approach is: let ImmuneBuilder write a temp PDB, then post-
|
|
15
|
-
process the lines — injecting REMARKs after TITLE and clamping B-factors on
|
|
16
|
-
ATOM/HETATM lines in one pass.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
from numbering import IMGT_CDR_RANGES, cdr_ranges_in_pdb_notation
|
|
24
|
-
|
|
25
|
-
B_FACTOR_MIN = 0.00
|
|
26
|
-
B_FACTOR_MAX = 99.99
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _clamp_b_factor(line: str) -> tuple[str, bool]:
|
|
30
|
-
"""Clamp B-factor column on ATOM/HETATM lines. Returns (new_line, clamped)."""
|
|
31
|
-
if not (line.startswith("ATOM") or line.startswith("HETATM")):
|
|
32
|
-
return line, False
|
|
33
|
-
if len(line) < 66:
|
|
34
|
-
return line, False
|
|
35
|
-
raw = line[60:66]
|
|
36
|
-
try:
|
|
37
|
-
value = float(raw)
|
|
38
|
-
except ValueError:
|
|
39
|
-
return line, False
|
|
40
|
-
clamped = max(B_FACTOR_MIN, min(B_FACTOR_MAX, value))
|
|
41
|
-
if clamped == value:
|
|
42
|
-
return line, False
|
|
43
|
-
return f"{line[:60]}{clamped:6.2f}{line[66:]}", True
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _cdr_remark_lines(mode: str) -> list[str]:
|
|
47
|
-
"""Emit REMARK 99 CDR* lines (IMGT ranges)."""
|
|
48
|
-
lines: list[str] = []
|
|
49
|
-
chains = ["H"] if mode == "NanoBodyBuilder2" else ["H", "L"]
|
|
50
|
-
for chain in chains:
|
|
51
|
-
for cdr_name, (lo, hi) in cdr_ranges_in_pdb_notation(chain).items():
|
|
52
|
-
# e.g. "REMARK 99 PLATFORMA CDRH1 H27-H38"
|
|
53
|
-
label = f"CDR{chain}{cdr_name[-1]}" # CDR1 → CDRH1 / CDRL1
|
|
54
|
-
lines.append(f"REMARK 99 PLATFORMA {label} {lo}-{hi}")
|
|
55
|
-
return lines
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _provenance_remark_lines(
|
|
59
|
-
*,
|
|
60
|
-
immunebuilder_version: str,
|
|
61
|
-
torch_seed: int,
|
|
62
|
-
block_version: str,
|
|
63
|
-
numbering_scheme: str,
|
|
64
|
-
) -> list[str]:
|
|
65
|
-
return [
|
|
66
|
-
"REMARK 99 PROVENANCE",
|
|
67
|
-
f"REMARK 99 PROVENANCE immunebuilder-version={immunebuilder_version}",
|
|
68
|
-
f"REMARK 99 PROVENANCE torch-seed={torch_seed}",
|
|
69
|
-
f"REMARK 99 PROVENANCE block-version={block_version}",
|
|
70
|
-
f"REMARK 99 PROVENANCE numbering-scheme={numbering_scheme}",
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _title_line(mode: str) -> str:
|
|
75
|
-
builder = "ABodyBuilder2" if mode == "ABodyBuilder2" else "NanoBodyBuilder2"
|
|
76
|
-
return f"TITLE {builder} prediction (Platforma Structure Prediction block)"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def augment_pdb(
|
|
80
|
-
source_path: Path,
|
|
81
|
-
dest_path: Path,
|
|
82
|
-
*,
|
|
83
|
-
mode: str,
|
|
84
|
-
immunebuilder_version: str,
|
|
85
|
-
torch_seed: int,
|
|
86
|
-
block_version: str,
|
|
87
|
-
numbering_scheme: str,
|
|
88
|
-
) -> dict:
|
|
89
|
-
"""Rewrite source_path → dest_path with TITLE + REMARK injections + B clamping.
|
|
90
|
-
|
|
91
|
-
Returns stats dict: {clamped_count: int, injected_remarks: int}.
|
|
92
|
-
"""
|
|
93
|
-
with open(source_path, "r") as f:
|
|
94
|
-
src_lines = [line.rstrip("\n") for line in f]
|
|
95
|
-
|
|
96
|
-
title = _title_line(mode)
|
|
97
|
-
provenance = _provenance_remark_lines(
|
|
98
|
-
immunebuilder_version=immunebuilder_version,
|
|
99
|
-
torch_seed=torch_seed,
|
|
100
|
-
block_version=block_version,
|
|
101
|
-
numbering_scheme=numbering_scheme,
|
|
102
|
-
)
|
|
103
|
-
cdr = _cdr_remark_lines(mode)
|
|
104
|
-
injected = [title, *provenance, *cdr]
|
|
105
|
-
|
|
106
|
-
out_lines: list[str] = []
|
|
107
|
-
clamped = 0
|
|
108
|
-
header_done = False
|
|
109
|
-
|
|
110
|
-
for line in src_lines:
|
|
111
|
-
# Skip any existing TITLE that ImmuneBuilder might have put down.
|
|
112
|
-
if not header_done and line.startswith("TITLE"):
|
|
113
|
-
continue
|
|
114
|
-
if not header_done and line.startswith(("ATOM", "HETATM", "MODEL")):
|
|
115
|
-
# Inject our block before the coordinate section starts.
|
|
116
|
-
out_lines.extend(injected)
|
|
117
|
-
header_done = True
|
|
118
|
-
|
|
119
|
-
new_line, was_clamped = _clamp_b_factor(line)
|
|
120
|
-
out_lines.append(new_line)
|
|
121
|
-
if was_clamped:
|
|
122
|
-
clamped += 1
|
|
123
|
-
|
|
124
|
-
# If the PDB had no ATOM/HETATM/MODEL lines at all (shouldn't happen),
|
|
125
|
-
# still emit the REMARKs so downstream parsers don't break.
|
|
126
|
-
if not header_done:
|
|
127
|
-
out_lines = injected + out_lines
|
|
128
|
-
|
|
129
|
-
with open(dest_path, "w") as f:
|
|
130
|
-
f.write("\n".join(out_lines))
|
|
131
|
-
f.write("\n")
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
"clamped_count": clamped,
|
|
135
|
-
"injected_remarks": len(injected),
|
|
136
|
-
}
|