chq1155 commited on
Commit
11b3eba
·
verified ·
1 Parent(s): 2327665

fix rescore.py bugs: missing holo->apo Kabsch (Q_apo=0 for all designs), residue-ID alignment on renumbered chains (PXDesign/Protenix), HETATM chain-A dropped, single-seed -> 3-seed ensemble, add .cif support

Browse files

Replaces old v2 rescore script with the paper's canonical v5-S2 3-seed ensemble pipeline (seeds 1024/5555/789). Fixes:
1. Adds second Kabsch (holo->apo) on binder coords so the binder sits next to the apo receptor slot; previously Q_apo=0 for every design because 1CFD sits ~34 A from 3CLN in world frame.
2. Sequence-anchored ordinal Kabsch instead of residue-ID intersection — the ID intersection paired the wrong CAs whenever chain A was renumbered from 1 (PXDesign, Protenix, Proteina).
3. HETATM-safe receptor extraction — PXDesign writes chain A as HETATM and get_residues(only_standard=True) dropped every one.
4. 3-seed ensemble (seeds 1024/5555/789) with per-seed and mean scores.
5. Accepts .cif in addition to .pdb.
6. --design_dir / --target CLI args instead of hardcoded internal paths.

Files changed (1) hide show
  1. code/scripts/rescore.py +247 -159
code/scripts/rescore.py CHANGED
@@ -1,177 +1,265 @@
1
  #!/usr/bin/env python3
2
- """
3
- Re-score binder PDB designs with a Q_theta checkpoint.
4
-
5
- Walks a directory of designs (binder PDB + sibling holo / apo receptor PDBs),
6
- runs each through DifferentiableQTheta, and writes per-design
7
- S = Q_theta(holo) - Q_theta(apo) plus the raw holo/apo scores to JSON.
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  Usage:
10
- python code/scripts/rescore.py \\
11
- --checkpoint checkpoints/Q_theta_phase2.pt \\
12
- --gpu 0
 
 
13
  """
14
- import os, sys, json, argparse, glob, logging
 
 
 
 
 
 
15
  import numpy as np
16
  import torch
17
- from pathlib import Path
18
 
19
- logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
20
  logger = logging.getLogger(__name__)
21
 
22
- BASE = str(Path(__file__).resolve().parent.parent.parent)
23
- sys.path.insert(0, os.path.join(BASE, 'code'))
24
- sys.path.insert(0, BASE)
25
 
26
  from models.differentiable_features import DifferentiableQTheta
27
- from utils.pdb_utils import load_structure, get_residues, get_backbone_coords, get_aa_indices, align_structures
28
-
29
- HOLO_PDB = os.path.join(BASE, 'data/pdbs/cam_holo/3CLN.pdb')
30
- APO_PDB = os.path.join(BASE, 'data/pdbs/cam_apo/1CFD.pdb')
31
-
32
-
33
- def score_pdb_list(dq, pdb_list, ref_resnums, ref_coords, device):
34
- """Score a list of design PDB files."""
35
- results = []
36
- for pdb_path in pdb_list:
37
- name = os.path.basename(pdb_path).replace(".pdb", "")
38
- try:
39
- design_model = load_structure(pdb_path)
40
- chains = [c.id for c in design_model.get_chains()]
41
- rec_chain = 'A' if 'A' in chains else chains[0]
42
- binder_chain = 'B' if 'B' in chains else [c for c in chains if c != rec_chain][0]
43
-
44
- rec_res = get_residues(design_model[rec_chain])
45
- binder_res = get_residues(design_model[binder_chain])
46
- rec_coords_d, _ = get_backbone_coords(rec_res)
47
- binder_coords, binder_mask = get_backbone_coords(binder_res)
48
- binder_aa_idx = get_aa_indices(binder_res)
49
-
50
- design_resnums = {r.get_id()[1]: i for i, r in enumerate(rec_res)}
51
- common = sorted(set(design_resnums.keys()) & set(ref_resnums.keys()))
52
- if len(common) < 10:
53
- logger.warning(f" Skip {name}: <10 common residues")
54
- continue
55
-
56
- d_ca = rec_coords_d[[design_resnums[r] for r in common], 1]
57
- r_ca = ref_coords[[ref_resnums[r] for r in common], 1]
58
- mobile_center = d_ca.mean(0)
59
- ref_center = r_ca.mean(0)
60
- _, R = align_structures(d_ca, r_ca)
61
-
62
- flat = binder_coords.reshape(-1, 3) - mobile_center
63
- aligned_binder = (flat @ R.T + ref_center).reshape(-1, 4, 3)
64
-
65
- coords_t = torch.from_numpy(aligned_binder).float().to(device)
66
- mask_t = torch.from_numpy(binder_mask).bool().to(device)
67
- aa_t = torch.from_numpy(binder_aa_idx).long().to(device)
68
-
69
- with torch.no_grad():
70
- q_holo = dq.score(coords_t, mask_t, binder_aa_idx=aa_t,
71
- receptor_label='holo').item()
72
- q_apo = dq.score(coords_t, mask_t, binder_aa_idx=aa_t,
73
- receptor_label='apo').item()
74
- S = q_holo - q_apo
75
- results.append({"design": name, "Q_holo": q_holo, "Q_apo": q_apo, "S": S})
76
- except Exception as e:
77
- logger.warning(f" Skip {name}: {e}")
78
- return results
79
-
80
-
81
- def summarize(results, label):
82
- if not results:
83
- return {}
84
- S = [r["S"] for r in results]
85
- return {
86
- "method": label, "n": len(S),
87
- "S_mean": float(np.mean(S)), "S_std": float(np.std(S)),
88
- "S_pos_pct": float(np.mean([s > 0 for s in S]) * 100),
89
- "Q_holo_mean": float(np.mean([r["Q_holo"] for r in results])),
90
- "Q_apo_mean": float(np.mean([r["Q_apo"] for r in results])),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
 
 
 
92
 
 
 
 
 
93
 
94
- def main():
95
- parser = argparse.ArgumentParser()
96
- parser.add_argument("--gpu", type=int, default=7)
97
- parser.add_argument("--checkpoint", default="checkpoints/Q_theta_phase2.pt")
98
- args = parser.parse_args()
99
-
100
- os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
101
- device = "cuda:0"
102
-
103
- logger.info(f"Loading Q_theta from {args.checkpoint}")
104
- dq = DifferentiableQTheta(checkpoint_path=args.checkpoint, device=device,
105
- esm_dir=os.path.join(BASE, "data/esm2_embeddings"))
106
- dq.load_receptor(HOLO_PDB, chain='A', label='holo', esm_target='cam')
107
- dq.load_receptor(APO_PDB, chain='A', label='apo', esm_target='cam')
108
-
109
- ref_model = load_structure(HOLO_PDB)
110
- ref_res = get_residues(ref_model['A'])
111
- ref_coords, _ = get_backbone_coords(ref_res)
112
- ref_resnums = {r.get_id()[1]: i for i, r in enumerate(ref_res)}
113
-
114
- output_dir = os.path.join(BASE, "results/v2_strict_holdout/scoring")
115
- os.makedirs(output_dir, exist_ok=True)
116
-
117
- # Define design directories
118
- design_sets = {
119
- "vanilla": os.path.join(BASE, "results/independent_validation/vanilla/holo_pdbs"),
120
- "langevin": os.path.join(BASE, "results/langevin_refinement/refined_pdbs"),
121
- "classifier": os.path.join(BASE, "results/guided_diffusion/guided"),
122
- "smc_r3": os.path.join(BASE, "results/smc_guidance/cam/round_3"),
123
- }
124
 
125
- # Also check for TDS and PXDesign
126
- tds_dirs = glob.glob(os.path.join(BASE, "results/tds_guidance/cam/designs"))
127
- if tds_dirs:
128
- design_sets["tds"] = tds_dirs[0]
129
-
130
- # PXDesign directories
131
- for px_method in ["pxdesign_scoring", "pxdesign_classifier", "pxdesign_tds",
132
- "pxdesign_smc", "pxdesign_langevin"]:
133
- px_dir = os.path.join(BASE, f"results_familysplit/design_bd30/{px_method}")
134
- if not os.path.exists(px_dir):
135
- px_dir = os.path.join(BASE, f"results/{px_method}")
136
- if os.path.exists(px_dir):
137
- pdbs = glob.glob(os.path.join(px_dir, "*.pdb"))
138
- if pdbs:
139
- design_sets[px_method] = px_dir
140
-
141
- all_results = {}
142
- summaries = []
143
-
144
- for method, pdb_dir in design_sets.items():
145
- if not os.path.exists(pdb_dir):
146
- logger.warning(f" {method}: directory not found ({pdb_dir})")
147
- continue
148
- pdbs = sorted(glob.glob(os.path.join(pdb_dir, "*.pdb")))
149
- if not pdbs:
150
- logger.warning(f" {method}: no PDB files")
151
- continue
152
-
153
- logger.info(f"\n=== {method} ({len(pdbs)} designs) ===")
154
- results = score_pdb_list(dq, pdbs, ref_resnums, ref_coords, device)
155
- s = summarize(results, method)
156
- if s:
157
- summaries.append(s)
158
- logger.info(f" {method}: n={s['n']}, S̄={s['S_mean']:.3f}±{s['S_std']:.3f}, "
159
- f"S>0={s['S_pos_pct']:.0f}%, Q+={s['Q_holo_mean']:.3f}, Q-={s['Q_apo_mean']:.3f}")
160
- all_results[method] = {"results": results, "summary": s}
161
-
162
- # Save
163
- with open(os.path.join(output_dir, "rescore_v2_all.json"), "w") as f:
164
- json.dump(all_results, f, indent=2)
165
-
166
- # Print summary table
167
- print("\n" + "=" * 70)
168
- print("V2 RESCORING SUMMARY (strict holdout, CaM OOD)")
169
- print("=" * 70)
170
- print(f"{'Method':20s} {'n':>4s} {'S̄':>8s} {'±σ':>6s} {'S>0%':>6s} {'Q+':>6s} {'Q-':>6s}")
171
- print("-" * 70)
172
- for s in sorted(summaries, key=lambda x: x['S_mean'], reverse=True):
173
- print(f"{s['method']:20s} {s['n']:4d} {s['S_mean']:8.3f} {s['S_std']:6.3f} "
174
- f"{s['S_pos_pct']:5.1f}% {s['Q_holo_mean']:6.3f} {s['Q_apo_mean']:6.3f}")
175
 
176
 
177
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python3
2
+ """Re-score binder designs with the paper's canonical Q_theta scoring pipeline
3
+ (v5-S2 TS-S2 3-seed ensemble: seeds 1024 / 5555 / 789).
4
+
5
+ Fixes vs the older `rescore_v2.py`:
6
+
7
+ (1) Sequence-anchored ordinal Kabsch — residue-ID intersection fails when a
8
+ generator (PXDesign, Protenix, Proteina) renumbers chain A from 1 while
9
+ the canonical holo PDB is numbered from its crystallographic start.
10
+ (2) Second Kabsch to move the binder from holo frame into apo frame before
11
+ scoring against the apo receptor — otherwise the binder sits 30+ Å from
12
+ the apo receptor slot and Q_apo collapses to 0 for every design.
13
+ (3) HETATM-safe receptor extraction — PXDesign writes chain A as HETATM;
14
+ `get_residues(..., only_standard=True)` filters HETATM and drops every
15
+ chain-A residue, so the whole design gets skipped.
16
+ (4) 3-seed ensemble — a single checkpoint under-reports; paper averages
17
+ seeds 1024 / 5555 / 789.
18
+ (5) `.cif` inputs supported alongside `.pdb`.
19
+ (6) `--design_dir` argument instead of hardcoded internal directory tree.
20
 
21
  Usage:
22
+ python code/scripts/rescore.py \
23
+ --design_dir path/to/designs \
24
+ --target cam \
25
+ --gpu 0 \
26
+ --out results/rescore_out.json
27
  """
28
+ import os
29
+ import sys
30
+ import json
31
+ import argparse
32
+ import logging
33
+ from pathlib import Path
34
+
35
  import numpy as np
36
  import torch
 
37
 
38
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
39
  logger = logging.getLogger(__name__)
40
 
41
+ BASE = Path(__file__).resolve().parent.parent.parent
42
+ sys.path.insert(0, str(BASE / "code"))
43
+ sys.path.insert(0, str(BASE))
44
 
45
  from models.differentiable_features import DifferentiableQTheta
46
+ from utils.pdb_utils import (
47
+ load_structure, get_backbone_coords, get_aa_indices, align_structures,
48
+ )
49
+
50
+ # --------------------------------------------------------------------------
51
+ # 3-seed ensemble (paper: v5-S2 TS-S2, seeds 1024 / 5555 / 789)
52
+ # --------------------------------------------------------------------------
53
+ SEEDS = [1024, 5555, 789]
54
+ CKPT_TEMPLATE = str(BASE / "results/v5_training/10seed/seed_{seed}/best_phase2.pt")
55
+ ESM_DIR = str(BASE / "data/esm2_embeddings")
56
+
57
+ # Per-target holo/apo canonical PDB pair (extend as needed).
58
+ TARGETS = {
59
+ "cam": {"holo": "data/pdbs/cam_holo/3CLN.pdb", "apo": "data/pdbs/cam_apo/1CFD.pdb", "chain": "A", "esm_target": "cam"},
60
+ "bcl2": {"holo": "data/pdbs/bcl2_holo/2XA0.pdb", "apo": "data/pdbs/bcl2_apo/1G5M.pdb", "chain": "A", "esm_target": "bcl2"},
61
+ "era": {"holo": "data/pdbs/era_complex/1GWR.pdb","apo": "data/pdbs/era_apo/3ERT.pdb", "chain": "A", "esm_target": "era"},
62
+ "mdm2": {"holo": "data/pdbs/mdm2_holo/1T4E.pdb", "apo": "data/pdbs/mdm2_apo/1Z1M.pdb", "chain": "A", "esm_target": "mdm2"},
63
+ "a2a": {"holo": "data/pdbs/a2a_holo/6GDG.pdb", "apo": "data/pdbs/a2a_apo/4EIY.pdb", "chain": "A", "esm_target": "a2a"},
64
+ "pai1": {"holo": "data/pdbs/pai1_holo/1LJ5.pdb", "apo": "data/pdbs/pai1_apo/1B3K.pdb", "chain": "A", "esm_target": "pai1"},
65
+ "ran": {"holo": "data/pdbs/ran_holo/1RRP.pdb", "apo": "data/pdbs/ran_apo/1BYT.pdb", "chain": "A", "esm_target": "ran"},
66
+ "integrin": {"holo": "data/pdbs/integrin_holo/2VDO.pdb","apo": "data/pdbs/integrin_apo/2VDK.pdb","chain": "A", "esm_target": "integrin"},
67
+ }
68
+
69
+ _THREE2ONE = {
70
+ 'ALA':'A','ARG':'R','ASN':'N','ASP':'D','CYS':'C','GLU':'E','GLN':'Q',
71
+ 'GLY':'G','HIS':'H','ILE':'I','LEU':'L','LYS':'K','MET':'M','PHE':'F',
72
+ 'PRO':'P','SER':'S','THR':'T','TRP':'W','TYR':'Y','VAL':'V',
73
+ }
74
+
75
+
76
+ def _seq_one(residues):
77
+ return ''.join(_THREE2ONE.get(r.resname.strip(), 'X') for r in residues)
78
+
79
+
80
+ def _load_ref(pdb_path, chain):
81
+ """Canonical holo/apo reference. Standard chain, ATOM-only."""
82
+ m = load_structure(pdb_path)
83
+ rs = [r for r in m[chain] if 'CA' in r and r.get_id()[0] == ' ']
84
+ coords, _ = get_backbone_coords(rs)
85
+ return rs, coords
86
+
87
+
88
+ def _get_design_chains(model):
89
+ """Return (receptor_residues, binder_residues) for a design PDB/CIF.
90
+
91
+ HETATM-safe: PXDesign writes receptor as HETATM in chain A. We include any
92
+ residue with a CA atom, regardless of het-flag. The receptor is the
93
+ longest chain (>= 50 residues); the binder is the 5–120 residue chain.
94
+ """
95
+ rec_res, binder_res = None, None
96
+ for chain in model.get_chains():
97
+ residues = [r for r in chain if 'CA' in r]
98
+ if not residues:
99
+ continue
100
+ if 5 <= len(residues) <= 120:
101
+ if binder_res is None or len(residues) < len(binder_res):
102
+ binder_res = residues
103
+ if len(residues) >= 50:
104
+ if rec_res is None or len(residues) > len(rec_res):
105
+ rec_res = residues
106
+ return rec_res, binder_res
107
+
108
+
109
+ def _seq_offset(design_res, ref_res):
110
+ """Locate where the design's receptor sequence starts within the reference
111
+ sequence — handles receptor cropping and residue renumbering-from-1.
112
+ """
113
+ d_seq = _seq_one(design_res)
114
+ r_seq = _seq_one(ref_res)
115
+ probe = d_seq[:30] if len(d_seq) >= 30 else d_seq
116
+ idx = r_seq.find(probe)
117
+ return max(0, idx)
118
+
119
+
120
+ def _score_one(design_path, holo_res, holo_bb, apo_res, apo_bb):
121
+ """Extract binder coords in holo AND apo frames. Return None if the design
122
+ can't be parsed."""
123
+ try:
124
+ model = load_structure(str(design_path))
125
+ except Exception as e:
126
+ logger.warning(f" Skip {design_path.name}: load failed ({e})")
127
+ return None
128
+
129
+ rec_res, binder_res = _get_design_chains(model)
130
+ if binder_res is None:
131
+ logger.warning(f" Skip {design_path.name}: no binder chain (5–120 residues) found")
132
+ return None
133
+ if rec_res is None:
134
+ # No receptor in the design — binder assumed to already be in holo frame
135
+ b_bb, b_mask = get_backbone_coords(binder_res)
136
+ b_aa = get_aa_indices(binder_res)
137
+ return b_bb, b_bb, b_mask, b_aa
138
+
139
+ d_bb, _ = get_backbone_coords(rec_res) # design chain A backbone
140
+
141
+ # Sequence-anchored ordinal alignment: i-th design residue corresponds to
142
+ # (offset + i)-th reference residue. Robust to residue renumbering-from-1.
143
+ offset_h = _seq_offset(rec_res, holo_res)
144
+ n_align_h = min(len(rec_res), len(holo_res) - offset_h)
145
+ if n_align_h < 10:
146
+ logger.warning(f" Skip {design_path.name}: sequence anchor could not align to holo")
147
+ return None
148
+ d_ca_h = d_bb[:n_align_h, 1]
149
+ h_ca = holo_bb[offset_h:offset_h + n_align_h, 1]
150
+ mc_h = d_ca_h.mean(0); rc_h = h_ca.mean(0)
151
+ _, R_h = align_structures(d_ca_h, h_ca)
152
+
153
+ b_bb, b_mask = get_backbone_coords(binder_res)
154
+ b_aa = get_aa_indices(binder_res)
155
+ flat = b_bb.reshape(-1, 3) - mc_h
156
+ binder_holo = (flat @ R_h.T + rc_h).reshape(-1, 4, 3)
157
+
158
+ # Holo -> apo transfer via residue-ID intersection on the CANONICAL PDBs
159
+ # (their numbering semantics match: e.g. 3CLN 5–147, 1CFD 5–147 overlap).
160
+ holo_rn = {r.get_id()[1]: i for i, r in enumerate(holo_res)}
161
+ apo_rn = {r.get_id()[1]: i for i, r in enumerate(apo_res)}
162
+ common = sorted(set(holo_rn) & set(apo_rn))
163
+ if len(common) >= 10:
164
+ h_ca2 = holo_bb[[holo_rn[k] for k in common], 1]
165
+ a_ca2 = apo_bb[[apo_rn[k] for k in common], 1]
166
+ mh2 = h_ca2.mean(0); ma2 = a_ca2.mean(0)
167
+ _, R_ha = align_structures(h_ca2, a_ca2)
168
+ flat2 = binder_holo.reshape(-1, 3) - mh2
169
+ binder_apo = (flat2 @ R_ha.T + ma2).reshape(-1, 4, 3)
170
+ else:
171
+ binder_apo = binder_holo # last resort: same frame
172
+ return binder_holo, binder_apo, b_mask, b_aa
173
+
174
+
175
+ def rescore(target, design_dir, gpu, out_path):
176
+ cfg = TARGETS[target]
177
+ device = f"cuda:{gpu}"
178
+ holo_pdb = str(BASE / cfg["holo"])
179
+ apo_pdb = str(BASE / cfg["apo"])
180
+
181
+ holo_res, holo_bb = _load_ref(holo_pdb, cfg["chain"])
182
+ apo_res, apo_bb = _load_ref(apo_pdb, cfg["chain"])
183
+ logger.info(f"[{target}] holo={cfg['holo']} n={len(holo_res)} apo={cfg['apo']} n={len(apo_res)}")
184
+
185
+ designs = sorted([p for p in Path(design_dir).iterdir() if p.suffix.lower() in (".pdb", ".cif", ".mmcif")])
186
+ logger.info(f"Found {len(designs)} design files in {design_dir}")
187
+ extracted = []
188
+ for p in designs:
189
+ r = _score_one(p, holo_res, holo_bb, apo_res, apo_bb)
190
+ if r is None:
191
+ continue
192
+ b_h, b_a, m, aa = r
193
+ extracted.append({"id": p.stem, "coords_holo": b_h, "coords_apo": b_a, "mask": m, "aa_idx": aa})
194
+ logger.info(f"Extracted {len(extracted)} usable designs")
195
+ if not extracted:
196
+ raise RuntimeError("No designs successfully extracted")
197
+
198
+ results = {d["id"]: {} for d in extracted}
199
+ for seed in SEEDS:
200
+ ckpt = CKPT_TEMPLATE.format(seed=seed)
201
+ if not Path(ckpt).exists():
202
+ logger.warning(f"Missing checkpoint {ckpt}; skip seed {seed}")
203
+ continue
204
+ logger.info(f"Scoring seed {seed}...")
205
+ dq = DifferentiableQTheta(checkpoint_path=ckpt, device=device, esm_dir=ESM_DIR)
206
+ dq.load_receptor(holo_pdb, chain=cfg["chain"], label="holo", esm_target=cfg["esm_target"])
207
+ dq.load_receptor(apo_pdb, chain=cfg["chain"], label="apo", esm_target=cfg["esm_target"])
208
+ for d in extracted:
209
+ try:
210
+ mt = torch.from_numpy(d["mask"]).bool().to(device)
211
+ at = torch.from_numpy(d["aa_idx"]).long().to(device)
212
+ with torch.no_grad():
213
+ ct_h = torch.from_numpy(d["coords_holo"]).float().to(device)
214
+ qh = dq.score(ct_h, mt, binder_aa_idx=at, receptor_label="holo").item()
215
+ ct_a = torch.from_numpy(d["coords_apo"]).float().to(device)
216
+ qa = dq.score(ct_a, mt, binder_aa_idx=at, receptor_label="apo").item()
217
+ results[d["id"]][str(seed)] = {"Q_holo": round(qh, 6), "Q_apo": round(qa, 6), "S": round(qh - qa, 6)}
218
+ except Exception as e:
219
+ results[d["id"]][str(seed)] = {"error": repr(e)}
220
+ del dq
221
+ torch.cuda.empty_cache()
222
+
223
+ # Mean over available seeds
224
+ for d in extracted:
225
+ by_seed = results[d["id"]]
226
+ qh = [v["Q_holo"] for v in by_seed.values() if "S" in v]
227
+ qa = [v["Q_apo"] for v in by_seed.values() if "S" in v]
228
+ ss = [v["S"] for v in by_seed.values() if "S" in v]
229
+ if ss:
230
+ by_seed["mean"] = {
231
+ "Q_holo": round(float(np.mean(qh)), 6),
232
+ "Q_apo": round(float(np.mean(qa)), 6),
233
+ "S": round(float(np.mean(ss)), 6),
234
+ "S_std": round(float(np.std(ss)), 6),
235
+ "n_seeds": len(ss),
236
+ }
237
+
238
+ out = {
239
+ "target": target, "holo_pdb": holo_pdb, "apo_pdb": apo_pdb,
240
+ "seeds": SEEDS, "checkpoints": {str(s): CKPT_TEMPLATE.format(seed=s) for s in SEEDS},
241
+ "n_designs": len(extracted), "per_design": results,
242
  }
243
+ Path(out_path).parent.mkdir(parents=True, exist_ok=True)
244
+ Path(out_path).write_text(json.dumps(out, indent=2))
245
+ logger.info(f"Saved {out_path}")
246
 
247
+ s_all = [results[d["id"]]["mean"]["S"] for d in extracted if "mean" in results[d["id"]]]
248
+ if s_all:
249
+ s = np.array(s_all)
250
+ logger.info(f"[{target}] n={len(s)} mean S={s.mean():+.4f} S>0: {100*(s>0).mean():.0f}% ({int((s>0).sum())}/{len(s)})")
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ def main():
254
+ ap = argparse.ArgumentParser()
255
+ ap.add_argument("--design_dir", required=True, help="dir with .pdb / .cif designs (receptor chain A + binder chain B)")
256
+ ap.add_argument("--target", required=True, choices=list(TARGETS.keys()))
257
+ ap.add_argument("--gpu", type=int, default=0)
258
+ ap.add_argument("--out", default=None)
259
+ args = ap.parse_args()
260
+ if args.out is None:
261
+ args.out = str(BASE / f"results/rescore/{args.target}_qtheta_v5s2.json")
262
+ rescore(args.target, args.design_dir, args.gpu, args.out)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
 
265
  if __name__ == "__main__":