@platforma-open/milaboratories.3d-structure-prediction.software 1.1.0 → 1.1.2

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.
@@ -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
@@ -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
- }
@@ -1,5 +0,0 @@
1
- ImmuneBuilder==1.2
2
- torch==2.7.0
3
- biopython==1.85
4
- openmm==8.3.1
5
- pdbfixer==1.12.0