anzhi2710gmailcom commited on
Commit
df36b14
·
verified ·
1 Parent(s): 4d20b62

Upload folder using huggingface_hub (part 3)

Browse files
.gitattributes CHANGED
@@ -53,3 +53,4 @@ model/Protenix-pxd/assets/mini_performance.png filter=lfs diff=lfs merge=lfs -te
53
  model/Protenix-pxd/assets/protenix_predictions.gif filter=lfs diff=lfs merge=lfs -text
54
  model/Protenix-pxd/assets/v0.5.0_metrics.png filter=lfs diff=lfs merge=lfs -text
55
  model/pxdesign/pxd_server/TimesNewRoman.ttf filter=lfs diff=lfs merge=lfs -text
 
 
53
  model/Protenix-pxd/assets/protenix_predictions.gif filter=lfs diff=lfs merge=lfs -text
54
  model/Protenix-pxd/assets/v0.5.0_metrics.png filter=lfs diff=lfs merge=lfs -text
55
  model/pxdesign/pxd_server/TimesNewRoman.ttf filter=lfs diff=lfs merge=lfs -text
56
+ weight/release_data/ccd_cache/clusters-by-entity-40.txt filter=lfs diff=lfs merge=lfs -text
model/pxdesign/utils/infer.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 ByteDance and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import hashlib
16
+ import logging
17
+ import os
18
+ import sys
19
+ import urllib
20
+ from collections import defaultdict
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from typing import Iterable
24
+
25
+ import torch
26
+ from ml_collections.config_dict import ConfigDict
27
+ from protenix.config import parse_configs
28
+ from protenix.data.parser import DistillationMMCIFParser
29
+ from protenix.utils.file_io import dump_gzip_pickle
30
+ from pxdbench.pxd_configs.eval import eval_configs
31
+
32
+ from pxdesign.configs.configs_base import configs as configs_base
33
+ from pxdesign.configs.configs_data import data_configs
34
+ from pxdesign.configs.configs_infer import inference_configs
35
+ from pxdesign.data.utils import pdb_to_cif
36
+
37
+ URL = {
38
+ "pxdesign_v0.1.0": "https://pxdesign.tos-cn-beijing.volces.com/release_model/pxdesign_v0.1.0.pt",
39
+ "protenix_base_default_v0.5.0": "https://pxdesign.tos-cn-beijing.volces.com/release_model/protenix_base_default_v0.5.0.pt",
40
+ "protenix_mini_default_v0.5.0": "https://pxdesign.tos-cn-beijing.volces.com/release_model/protenix_mini_default_v0.5.0.pt",
41
+ "protenix_mini_tmpl_v0.5.0": "https://pxdesign.tos-cn-beijing.volces.com/release_model/protenix_mini_tmpl_v0.5.0.pt",
42
+ "ccd_components_file": "https://pxdesign.tos-cn-beijing.volces.com/release_data/components.v20240608.cif",
43
+ "ccd_components_rdkit_mol_file": "https://pxdesign.tos-cn-beijing.volces.com/release_data/components.v20240608.cif.rdkit_mol.pkl",
44
+ "pdb_cluster_file": "https://pxdesign.tos-cn-beijing.volces.com/release_data/clusters-by-entity-40.txt",
45
+ }
46
+
47
+ ALIASES = {
48
+ "N_sample": "sample_diffusion.N_sample",
49
+ "N_step": "sample_diffusion.N_step",
50
+ "eta_type": "sample_diffusion.eta_schedule.type",
51
+ "eta_min": "sample_diffusion.eta_schedule.min",
52
+ "eta_max": "sample_diffusion.eta_schedule.max",
53
+ "gamma0": "sample_diffusion.gamma0",
54
+ "gamma_min": "sample_diffusion.gamma_min",
55
+ "sample_diffusion_chunk_size": "infer_setting.sample_diffusion_chunk_size",
56
+ }
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+
61
+ def download_inference_cache(configs) -> None:
62
+ def progress_callback(block_num, block_size, total_size):
63
+ downloaded = block_num * block_size
64
+ percent = min(100, downloaded * 100 / total_size)
65
+ bar_length = 30
66
+ filled_length = int(bar_length * percent // 100)
67
+ bar = "=" * filled_length + "-" * (bar_length - filled_length)
68
+
69
+ status = f"\r[{bar}] {percent:.1f}%"
70
+ print(status, end="", flush=True)
71
+
72
+ if downloaded >= total_size:
73
+ print()
74
+
75
+ def download_from_url(tos_url, checkpoint_path, check_weight=True):
76
+ urllib.request.urlretrieve(
77
+ tos_url, checkpoint_path, reporthook=progress_callback
78
+ )
79
+ if check_weight:
80
+ try:
81
+ ckpt = torch.load(checkpoint_path)
82
+ del ckpt
83
+ except:
84
+ os.remove(checkpoint_path)
85
+ raise RuntimeError(
86
+ "Download model checkpoint failed, please download by yourself with "
87
+ f"wget {tos_url} -O {checkpoint_path}"
88
+ )
89
+
90
+ for cache_name in (
91
+ "ccd_components_file",
92
+ "ccd_components_rdkit_mol_file",
93
+ "pdb_cluster_file",
94
+ ):
95
+ cur_cache_fpath = configs["data"][cache_name]
96
+ if not os.path.exists(cur_cache_fpath):
97
+ os.makedirs(os.path.dirname(cur_cache_fpath), exist_ok=True)
98
+ tos_url = URL[cache_name]
99
+ assert os.path.basename(tos_url) == os.path.basename(cur_cache_fpath), (
100
+ f"{cache_name} file name is incorrect, `{tos_url}` and "
101
+ f"`{cur_cache_fpath}`. Please check and try again."
102
+ )
103
+ logger.info(
104
+ f"Downloading data cache from\n {tos_url}... to {cur_cache_fpath}"
105
+ )
106
+ download_from_url(tos_url, cur_cache_fpath, check_weight=False)
107
+
108
+ checkpoint_path = os.path.join(
109
+ configs.load_checkpoint_dir, f"{configs.model_name}.pt"
110
+ )
111
+ if not os.path.exists(checkpoint_path):
112
+ os.makedirs(configs.load_checkpoint_dir, exist_ok=True)
113
+ tos_url = URL[configs.model_name]
114
+ logger.info(
115
+ f"Downloading model checkpoint from\n {tos_url}... to {checkpoint_path}"
116
+ )
117
+ download_from_url(tos_url, checkpoint_path)
118
+
119
+ # download protenix checkpoints
120
+ for model_name in [
121
+ "protenix_base_default_v0.5.0",
122
+ "protenix_mini_default_v0.5.0",
123
+ "protenix_mini_tmpl_v0.5.0",
124
+ ]:
125
+ checkpoint_path = os.path.join(configs.load_checkpoint_dir, f"{model_name}.pt")
126
+ if not os.path.exists(checkpoint_path):
127
+ tos_url = URL[model_name]
128
+ logger.info(
129
+ f"Downloading model checkpoint from\n {tos_url}... to {checkpoint_path}"
130
+ )
131
+ download_from_url(tos_url, checkpoint_path)
132
+
133
+ # set checkpoint dir for ptx tools in PXDesignBench
134
+ if hasattr(configs, "eval"):
135
+ configs.eval.binder.tools.ptx.load_checkpoint_dir = configs.load_checkpoint_dir
136
+ configs.eval.binder.tools.ptx_mini.load_checkpoint_dir = (
137
+ configs.load_checkpoint_dir
138
+ )
139
+
140
+
141
+ def remap_arg_key(key: str) -> str:
142
+ if key.startswith("--"):
143
+ name = key[2:]
144
+ mapped = ALIASES.get(name, name)
145
+ return "--" + mapped
146
+ return key
147
+
148
+
149
+ def parse_sys_args(argv=None):
150
+ if argv is None:
151
+ argv = sys.argv[1:]
152
+ remapped = []
153
+
154
+ i = 0
155
+ while i < len(argv):
156
+ k = argv[i]
157
+ # if k starts with "--", check whether it matches alias
158
+ if k.startswith("--") and i + 1 < len(argv):
159
+ remapped.append(remap_arg_key(k))
160
+ remapped.append(argv[i + 1])
161
+ i += 2
162
+ else:
163
+ remapped.append(k)
164
+ i += 1
165
+
166
+ return " ".join(remapped)
167
+
168
+
169
+ def get_configs(argv=None) -> ConfigDict:
170
+ configs = {
171
+ **configs_base,
172
+ **{"data": data_configs},
173
+ **inference_configs,
174
+ **{"eval": eval_configs},
175
+ }
176
+ configs = parse_configs(
177
+ configs=configs,
178
+ arg_str=parse_sys_args(argv),
179
+ fill_required_with_null=True,
180
+ )
181
+ return configs
182
+
183
+
184
+ class DisableLogging:
185
+ def __enter__(self):
186
+ logging.disable(logging.WARNING)
187
+
188
+ def __exit__(self, exc_type, exc, tb):
189
+ logging.disable(logging.NOTSET)
190
+
191
+
192
+ # -------------------------
193
+ # Handling PDB input
194
+ # -------------------------
195
+
196
+
197
+ def parse_ranges(range_str: str) -> list[tuple[int, int]]:
198
+ """
199
+ Parse "1-30,40-50,66" -> [(1,30),(40,50),(66,66)]
200
+ """
201
+ ranges: list[tuple[int, int]] = []
202
+ for part in range_str.split(","):
203
+ part = part.strip()
204
+ if not part:
205
+ continue
206
+ if "-" in part:
207
+ a, b = part.split("-")
208
+ ranges.append((int(a), int(b)))
209
+ else:
210
+ x = int(part)
211
+ ranges.append((x, x))
212
+ return ranges
213
+
214
+
215
+ def format_ranges(ints: Iterable[int]) -> str:
216
+ """
217
+ Compress sorted integers into "a-b,c,d-e".
218
+ """
219
+ xs = sorted(set(int(x) for x in ints))
220
+ if not xs:
221
+ return ""
222
+ out: list[str] = []
223
+ s = e = xs[0]
224
+ for x in xs[1:]:
225
+ if x == e + 1:
226
+ e = x
227
+ else:
228
+ out.append(f"{s}-{e}" if s != e else f"{s}")
229
+ s = e = x
230
+ out.append(f"{s}-{e}" if s != e else f"{s}")
231
+ return ",".join(out)
232
+
233
+
234
+ # -------------------------
235
+ # Chain mapping
236
+ # -------------------------
237
+
238
+
239
+ def build_chain_mapping(
240
+ old_ids: Iterable[str],
241
+ new_ids: Iterable[str],
242
+ *,
243
+ keep_chains: Iterable[str] | None = None,
244
+ err_hint: str = "Please consider using a CIF structure file in your JSON file.",
245
+ ) -> dict[str, str]:
246
+ """
247
+ Build mapping old_id -> new_id with consistency check.
248
+
249
+ keep_chains:
250
+ - None: keep all chains
251
+ - otherwise: only build mapping for chains in keep_chains
252
+ """
253
+ old_ids = list(old_ids)
254
+ new_ids = list(new_ids)
255
+ if len(old_ids) != len(new_ids):
256
+ raise ValueError("old_ids and new_ids must have the same length.")
257
+
258
+ keep = set(keep_chains) if keep_chains is not None else None
259
+
260
+ mapping: dict[str, str] = {}
261
+ for old, new in zip(old_ids, new_ids):
262
+ if keep is not None and old not in keep:
263
+ continue
264
+ if old not in mapping:
265
+ mapping[old] = new
266
+ elif mapping[old] != new:
267
+ raise ValueError(
268
+ f"Inconsistent mapping: chain '{old}' maps to both "
269
+ f"'{mapping[old]}' and '{new}'. It will raise ambiguity. {err_hint}"
270
+ )
271
+ return mapping
272
+
273
+
274
+ # -------------------------
275
+ # Residue mapping (res_id <-> auth_res_id)
276
+ # -------------------------
277
+
278
+
279
+ @dataclass(frozen=True)
280
+ class ResidueMaps:
281
+ # (chain_id, res_id) -> (auth_asym_id, auth_res_id)
282
+ resid2auth: dict[tuple[str, int], tuple[str, int]]
283
+ # (auth_asym_id, auth_res_id) -> (chain_id, res_id)
284
+ auth2resid: dict[tuple[str, int], tuple[str, int]]
285
+
286
+
287
+ def build_residue_maps(
288
+ atom_array,
289
+ *,
290
+ strict_bijective: bool = True,
291
+ err_hint: str = "Please consider using a CIF structure file in your JSON file.",
292
+ ) -> ResidueMaps:
293
+ """
294
+ Build residue-level mapping with uniqueness checks.
295
+
296
+ Ensures:
297
+ - each (chain_id, res_id) maps to a single (auth_asym_id, auth_res_id)
298
+ - (optional) bijection: each (auth_asym_id, auth_res_id) maps back to a single (chain_id, res_id)
299
+ """
300
+ chain_id = atom_array.chain_id
301
+ res_id = atom_array.res_id
302
+ auth_asym_id = atom_array.auth_asym_id
303
+ auth_res_id = atom_array.auth_res_id
304
+
305
+ resid2auth: dict[tuple[str, int], tuple[str, int]] = {}
306
+ auth2resid: dict[tuple[str, int], tuple[str, int]] = {}
307
+
308
+ for c, r, ac, ar in zip(chain_id, res_id, auth_asym_id, auth_res_id):
309
+ key = (str(c), int(r))
310
+ val = (str(ac), int(ar))
311
+
312
+ if key in resid2auth and resid2auth[key] != val:
313
+ raise ValueError(
314
+ "Non-unique mapping detected: same (chain_id, res_id) maps to multiple "
315
+ f"(auth_asym_id, auth_res_id).\n key={key}\n first={resid2auth[key]}\n new={val}\n"
316
+ f"{err_hint}"
317
+ )
318
+ resid2auth.setdefault(key, val)
319
+
320
+ if strict_bijective:
321
+ if val in auth2resid and auth2resid[val] != key:
322
+ raise ValueError(
323
+ "Non-unique mapping detected: same (auth_asym_id, auth_res_id) maps to multiple "
324
+ f"(chain_id, res_id).\n val={val}\n first={auth2resid[val]}\n new={key}\n"
325
+ f"{err_hint}"
326
+ )
327
+ auth2resid.setdefault(val, key)
328
+
329
+ return ResidueMaps(resid2auth=resid2auth, auth2resid=auth2resid)
330
+
331
+
332
+ # -------------------------
333
+ # Converters: crop / hotspot
334
+ # -------------------------
335
+
336
+
337
+ def convert_crop_auth_to_new(
338
+ crop_dict: dict[str, str],
339
+ residue_maps: ResidueMaps,
340
+ *,
341
+ strict_mapping: bool = True,
342
+ ) -> dict[str, str]:
343
+ result: dict[str, list[int]] = defaultdict(list)
344
+
345
+ for auth_chain, range_str in crop_dict.items():
346
+ for start, end in parse_ranges(range_str):
347
+ for auth_r in range(start, end + 1):
348
+ key = (auth_chain, int(auth_r))
349
+ if key not in residue_maps.auth2resid:
350
+ if strict_mapping:
351
+ raise KeyError(
352
+ f"Requested auth residue not found in atom_array: {key}"
353
+ )
354
+ else:
355
+ continue
356
+ new_c, new_r = residue_maps.auth2resid[key]
357
+ result[new_c].append(new_r)
358
+
359
+ return {new_c: format_ranges(rs) for new_c, rs in result.items() if rs}
360
+
361
+
362
+ def convert_hotspot_auth_to_new(
363
+ hotspot_dict: dict[str, list[int]],
364
+ residue_maps: ResidueMaps,
365
+ *,
366
+ strict_mapping: bool = True,
367
+ ) -> dict[str, list[int]]:
368
+ """
369
+ {chain_id: [11,22]} -> {auth_asym_id: [auth_res_id,...]} (sorted unique)
370
+ """
371
+ result: dict[str, list[int]] = defaultdict(list)
372
+
373
+ for chain, res_list in hotspot_dict.items():
374
+ for r in res_list:
375
+ key = (chain, int(r))
376
+ if key not in residue_maps.auth2resid:
377
+ if strict_mapping:
378
+ raise KeyError(f"Hotspot residue not found in atom_array: {key}")
379
+ else:
380
+ continue
381
+ ac, ar = residue_maps.auth2resid[key]
382
+ result[ac].append(ar)
383
+
384
+ return {ac: sorted(set(ars)) for ac, ars in result.items() if ars}
385
+
386
+
387
+ # -------------------------
388
+ # Apply filter rewrite (chain_id / crop / msa / hotspot)
389
+ # -------------------------
390
+
391
+
392
+ def rewrite_input_dict_inplace(
393
+ input_dict: dict,
394
+ *,
395
+ chain_mapping: dict[str, str], # old chain_id -> new chain_id (e.g. PDB->CIF)
396
+ residue_maps: ResidueMaps | None,
397
+ ) -> None:
398
+ """
399
+ Rewrite cond_dict['filter'] in-place using chain_mapping and residue_maps.
400
+ """
401
+ cond_dict = input_dict["condition"]
402
+ filt = cond_dict.get("filter", {})
403
+ if filt:
404
+ # chain_id list
405
+ if "chain_id" in filt and filt["chain_id"]:
406
+ filt["chain_id"] = [chain_mapping[c] for c in filt["chain_id"]]
407
+
408
+ # crop dict: {chain_id: "ranges"}
409
+ if "crop" in filt and filt["crop"]:
410
+ if residue_maps is None:
411
+ raise ValueError(
412
+ "filter.crop requires residue_maps (atom_array) to convert res_id -> auth_res_id."
413
+ )
414
+ filt["crop"] = convert_crop_auth_to_new(filt["crop"], residue_maps)
415
+ cond_dict["filter"] = filt
416
+
417
+ # msa dict: {chain_id: ...}
418
+ if "msa" in cond_dict and cond_dict["msa"]:
419
+ cond_dict["msa"] = {chain_mapping[k]: v for k, v in cond_dict["msa"].items()}
420
+
421
+ input_dict["condition"] = cond_dict
422
+ # hotspot dict: {chain_id: [res_ids]}
423
+ if "hotspot" in input_dict and input_dict["hotspot"]:
424
+ if residue_maps is None:
425
+ raise ValueError(
426
+ "filter.hotspot requires residue_maps (atom_array) to convert res_id -> auth_res_id."
427
+ )
428
+ input_dict["hotspot"] = convert_hotspot_auth_to_new(
429
+ input_dict["hotspot"], residue_maps
430
+ )
431
+
432
+
433
+ # -------------------------
434
+ # Main entry: convert_to_bioassembly_dict
435
+ # -------------------------
436
+
437
+
438
+ def convert_to_bioassembly_dict(input_dict: dict, out_dir: str | None = None):
439
+ """
440
+ Returns:
441
+ - if input is already .pkl.gz: (str_file) (kept as your original behavior)
442
+ - else: (out_path, chain_mapping)
443
+ """
444
+ assert "condition" in input_dict, "input_dict must have 'condition' key"
445
+ cond_dict = input_dict["condition"]
446
+ str_file = cond_dict["structure_file"]
447
+ if out_dir is None:
448
+ out_dir = os.path.dirname(str_file)
449
+
450
+ if str_file.endswith(".pkl.gz"):
451
+ return str_file
452
+
453
+ chain_mapping: dict[str, str] = {}
454
+ residue_maps: ResidueMaps | None = None
455
+ atom_array = None
456
+
457
+ if str_file.endswith(".cif"):
458
+ parser = DistillationMMCIFParser(str_file)
459
+ d = parser.get_structure_dict()
460
+
461
+ elif str_file.endswith(".pdb"):
462
+ cif_file = os.path.join(out_dir, os.path.basename(str_file)[:-4] + ".cif")
463
+ atom_array = pdb_to_cif(str_file, cif_file)
464
+
465
+ filter_chains = cond_dict.get("filter", {}).get("chain_id", [])
466
+ chain_mapping = build_chain_mapping(
467
+ atom_array.auth_asym_id,
468
+ atom_array.chain_id,
469
+ keep_chains=filter_chains if filter_chains else None,
470
+ )
471
+ residue_maps = build_residue_maps(atom_array)
472
+
473
+ rewrite_input_dict_inplace(
474
+ input_dict,
475
+ chain_mapping=chain_mapping,
476
+ residue_maps=residue_maps,
477
+ )
478
+ parser = DistillationMMCIFParser(cif_file)
479
+ d = parser.get_structure_dict()
480
+
481
+ else:
482
+ raise ValueError(f"Unsupported structure file! {str_file}")
483
+
484
+ out_path = Path(out_dir) / f"{Path(str_file).stem}.pkl.gz"
485
+ assert str(out_path).endswith(".pkl.gz"), "Bioassembly dict should end with .pkl.gz"
486
+ dump_gzip_pickle(d, out_path)
487
+ input_dict["condition"]["structure_file"] = str(out_path)
488
+
489
+ return d
490
+
491
+
492
+ def configure_runtime_env(
493
+ use_fast_ln: bool = False, use_deepspeed_evo: bool = False
494
+ ) -> None:
495
+ """
496
+ Independent runtime knobs:
497
+ - use_fast_ln -> LAYERNORM_TYPE
498
+ - use_deepspeed_evo -> DEEPSPEED_EVO (+ CUTLASS dependency)
499
+ """
500
+
501
+ # LayerNorm
502
+ if use_fast_ln:
503
+ os.environ["LAYERNORM_TYPE"] = "fast_layernorm"
504
+
505
+ # DeepSpeed Evo: fully independent
506
+ os.environ["DEEPSPEED_EVO"] = "true" if use_deepspeed_evo else "false"
507
+
508
+ if not use_deepspeed_evo:
509
+ return
510
+
511
+ if "CUTLASS_PATH" in os.environ and os.environ["CUTLASS_PATH"]:
512
+ cutlass_path = Path(os.environ["CUTLASS_PATH"]).expanduser()
513
+ else:
514
+ cutlass_path = Path.home() / "cutlass"
515
+ os.environ["CUTLASS_PATH"] = str(cutlass_path)
516
+
517
+ if not cutlass_path.is_dir():
518
+ print("")
519
+ print(f"[WARNING] CUTLASS not found at: {cutlass_path}")
520
+ print(
521
+ " PXDesign uses DeepSpeed Evo kernels which require NVIDIA CUTLASS v3.5.1."
522
+ )
523
+ print(" To install:")
524
+ print(
525
+ ' git clone -b v3.5.1 https://github.com/NVIDIA/cutlass.git "$HOME/cutlass"'
526
+ )
527
+ print(' export CUTLASS_PATH="$HOME/cutlass"')
528
+ print("")
529
+
530
+
531
+ def derive_seed(base_seed: int, rank: int = 0, digits: int = 6) -> int:
532
+ mod = 10**digits
533
+ msg = f"pxdesign|{base_seed}|{rank}".encode()
534
+ h = hashlib.blake2b(msg, digest_size=8).digest()
535
+ return int.from_bytes(h, "little") % mod
model/pxdesign/utils/inputs.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Union
7
+
8
+ import numpy as np
9
+ import yaml
10
+ from protenix.utils.file_io import load_gzip_pickle
11
+
12
+ from pxdesign.data.utils import CIFWriter
13
+ from pxdesign.utils.infer import convert_to_bioassembly_dict
14
+
15
+
16
+ class NpEncoder(json.JSONEncoder):
17
+ def default(self, obj):
18
+ if isinstance(obj, (np.integer,)):
19
+ return int(obj)
20
+ elif isinstance(obj, (np.floating,)):
21
+ return float(obj)
22
+ elif isinstance(obj, (np.ndarray,)):
23
+ return obj.tolist()
24
+ return super(NpEncoder, self).default(obj)
25
+
26
+
27
+ def parse_yaml_to_json(yaml_path, json_path=None):
28
+ """
29
+ Parses the YAML config and converts it to the
30
+ JSON structure required by PXDesign model.
31
+ """
32
+ yaml_path = os.path.abspath(yaml_path)
33
+ if not os.path.exists(yaml_path):
34
+ raise FileNotFoundError(f"YAML config file not found: {yaml_path}")
35
+
36
+ with open(yaml_path, "r") as f:
37
+ try:
38
+ cfg = yaml.safe_load(f)
39
+ except yaml.YAMLError as e:
40
+ raise ValueError(f"Error parsing YAML file: {e}")
41
+
42
+ # --- 1. Top Level Fields ---
43
+ # Default task name uses filename if not provided
44
+ default_name = os.path.splitext(os.path.basename(yaml_path))[0]
45
+ task_name = cfg.get("task_name", default_name)
46
+
47
+ # Binder length (Required)
48
+ if "binder_length" not in cfg:
49
+ raise ValueError("Missing required field: 'binder_length'")
50
+ binder_length = int(cfg["binder_length"])
51
+
52
+ # --- 2. Target Parsing ---
53
+ target_cfg = cfg.get("target", {})
54
+ if "file" not in target_cfg:
55
+ raise ValueError("Missing required field: 'target.file'")
56
+
57
+ target_file_path = target_cfg["file"]
58
+ if not os.path.exists(target_file_path):
59
+ raise FileNotFoundError(f"Target structure file not found: {target_file_path}")
60
+
61
+ # Initialize containers
62
+ chain_ids = []
63
+ crop_dict = {}
64
+ hotspot_dict = {}
65
+ msa_dict_per_chain = {}
66
+
67
+ # --- 3. Chains Parsing ---
68
+ chains_cfg = target_cfg.get("chains", {})
69
+ if not chains_cfg:
70
+ raise ValueError("Missing required field: 'target.chains'")
71
+
72
+ for chain_id, props in chains_cfg.items():
73
+ chain_id = str(chain_id)
74
+ chain_ids.append(chain_id)
75
+
76
+ # Handle "A: all" or "A: null" shorthand
77
+ if props is None or (
78
+ isinstance(props, str) and props.lower() in ["all", "full"]
79
+ ):
80
+ props = {}
81
+
82
+ # --- Crop Logic ---
83
+ # User YAML: ["1-50", "80-100"] OR "1-100" OR "all"
84
+ # Internal JSON: "1-50,80-100" OR None
85
+ if "crop" in props:
86
+ raw_crop = props["crop"]
87
+ crop_val = None
88
+
89
+ if isinstance(raw_crop, list):
90
+ # Join list into comma-separated string
91
+ crop_val = ",".join(str(x) for x in raw_crop)
92
+ elif isinstance(raw_crop, str):
93
+ if raw_crop.lower() in ["all", "full"]:
94
+ crop_val = None
95
+ else:
96
+ crop_val = raw_crop
97
+
98
+ if crop_val:
99
+ crop_dict[chain_id] = crop_val
100
+
101
+ # --- Hotspot Logic ---
102
+ if "hotspots" in props:
103
+ # YAML list is already a Python list
104
+ hotspot_dict[chain_id] = props["hotspots"]
105
+
106
+ # --- MSA Logic ---
107
+ if "msa" in props and props["msa"]:
108
+ msa_path = props["msa"]
109
+ for fname in ["pairing.a3m", "non_pairing.a3m"]:
110
+ if not os.path.exists(os.path.join(msa_path, fname)):
111
+ raise FileNotFoundError(
112
+ f"MSA file not found: {os.path.join(msa_path, fname)}"
113
+ )
114
+ msa_config = {
115
+ "precomputed_msa_dir": msa_path, # Default to None (Auto)
116
+ "pairing_db": "uniref100",
117
+ }
118
+
119
+ msa_dict_per_chain[chain_id] = msa_config
120
+
121
+ # --- 4. Construct Internal JSON Structure ---
122
+ json_task = {
123
+ "name": task_name,
124
+ "condition": {
125
+ "structure_file": target_file_path,
126
+ "filter": {
127
+ "chain_id": chain_ids,
128
+ "crop": crop_dict,
129
+ },
130
+ "msa": msa_dict_per_chain,
131
+ },
132
+ "hotspot": hotspot_dict,
133
+ "generation": [
134
+ {
135
+ "type": "protein",
136
+ "length": binder_length,
137
+ "count": 1,
138
+ }
139
+ ],
140
+ }
141
+
142
+ if json_path is not None:
143
+ os.makedirs(os.path.dirname(json_path), exist_ok=True)
144
+ with open(json_path, "w") as f:
145
+ json.dump([json_task], f, cls=NpEncoder)
146
+
147
+ return [json_task]
148
+
149
+
150
+ def check_yaml_file(yaml_path: str):
151
+ print(f"Checking YAML file: {yaml_path}...")
152
+ result = parse_yaml_to_json(yaml_path, None)
153
+ print("✅ YAML file is valid.")
154
+
155
+
156
+ def process_input_file(input_path: str, out_dir: str = None) -> str:
157
+ """
158
+ Process the input file path to ensure it has the correct extension.
159
+ """
160
+ input_path = os.path.abspath(input_path)
161
+ if not os.path.exists(input_path):
162
+ raise FileNotFoundError(f"Input file not found: {input_path}")
163
+
164
+ # Check file extension
165
+ ext = os.path.splitext(input_path)[1].lower()
166
+ if ext not in [".json", ".yaml"]:
167
+ raise ValueError(
168
+ f"Unsupported input file format: {ext}. "
169
+ "Supported formats are: JSON, YAML."
170
+ )
171
+
172
+ # Convert YAML to JSON if necessary
173
+ if ext == ".yaml":
174
+ base, _ = os.path.splitext(os.path.basename(input_path))
175
+ out_dir = out_dir or os.path.dirname(input_path)
176
+ json_path = os.path.join(out_dir, f"{base}.json")
177
+
178
+ parse_yaml_to_json(input_path, json_path)
179
+ input_path = json_path
180
+
181
+ return input_path
182
+
183
+
184
+ def dump_bioassembly_to_cif(
185
+ bio_dict: Union[str, Path, dict],
186
+ output_cif: Union[str, Path],
187
+ dump_unresolved_atoms: bool = False,
188
+ ):
189
+ """
190
+ Dump a bioassembly dict to CIF.
191
+ """
192
+ if isinstance(bio_dict, str) or isinstance(bio_dict, Path):
193
+ bio_dict = load_gzip_pickle(bio_dict)
194
+ atom_array = bio_dict["atom_array"]
195
+
196
+ if not dump_unresolved_atoms:
197
+ mask = atom_array.is_resolved
198
+ else:
199
+ mask = None
200
+
201
+ entity_poly_type = bio_dict["entity_poly_type"]
202
+ writer = CIFWriter(
203
+ atom_array=atom_array,
204
+ entity_poly_type=entity_poly_type,
205
+ atom_array_output_mask=mask,
206
+ )
207
+ writer.save_to_cif(
208
+ output_cif,
209
+ entry_id=Path(output_cif).stem.split(".")[0],
210
+ include_bonds=False,
211
+ )
212
+
213
+ return
214
+
215
+
216
+ def generate_pml_from_json_input(cif_file_path: str, json_file_path: str) -> dict:
217
+ """
218
+ generate PML script from input_dict. for visualizing in pymol.
219
+ cif_file_path: path to the cif file of the target structure.
220
+ json_file_path: path to the json file of the input dict.
221
+ """
222
+ cif_fname = os.path.basename(cif_file_path)
223
+ cif_file_dir = os.path.dirname(cif_file_path)
224
+ pymol_cmds = [
225
+ "load " + cif_fname,
226
+ "hide",
227
+ "show cartoon",
228
+ 'cmd.util.cbc(selection="(elem C)")',
229
+ ]
230
+ json_task_dict = json.load(open(json_file_path, "r"))[0]
231
+
232
+ cond_dict = json_task_dict.get("condition", {})
233
+ if "filter" in cond_dict:
234
+ crop_region = cond_dict["filter"].get("crop", {})
235
+ crop_sele_list = []
236
+ for chain_id, chain_crop in crop_region.items():
237
+ chain_crop_sele = (
238
+ f"(chain {chain_id} and resi " + chain_crop.replace(",", "+") + ")"
239
+ )
240
+ crop_sele_list.append(chain_crop_sele)
241
+ if len(crop_sele_list) > 0:
242
+ crop_sele_str = "select crop, " + " OR ".join(crop_sele_list)
243
+ pymol_cmds.append(crop_sele_str)
244
+ pymol_cmds.append("color marine, crop and elem C")
245
+
246
+ hotspot_residues = json_task_dict.get("hotspot", {})
247
+ hotspot_sele_list = []
248
+ for chain_id, residues in hotspot_residues.items():
249
+ hotspot_sele = (
250
+ f"(chain {chain_id} and resi " + "+".join([str(x) for x in residues]) + ")"
251
+ )
252
+ hotspot_sele_list.append(hotspot_sele)
253
+ if len(hotspot_sele_list) > 0:
254
+ hotspot_sele_str = "select hotspot, " + " OR ".join(hotspot_sele_list)
255
+ pymol_cmds.append(hotspot_sele_str)
256
+ pymol_cmds.extend(["color pink, hotspot and elem C", "show sticks, hotspot"])
257
+
258
+ pymol_cmds.append("color grey70, not (hotspot OR crop)")
259
+ pml_script_path = os.path.join(cif_file_dir, f"{Path(cif_file_path).stem}.pml")
260
+ with open(pml_script_path, "w") as f:
261
+ f.write("\n".join(pymol_cmds))
262
+ return
263
+
264
+
265
+ def dump_target_cif_from_input_file(file_path: str, out_dir: str) -> dict:
266
+ """
267
+ Parse target structure from input_dict.
268
+ """
269
+ if os.path.splitext(file_path)[1].lower() == ".json":
270
+ json_path = file_path
271
+ else:
272
+ assert (
273
+ os.path.splitext(file_path)[1].lower() == ".yaml"
274
+ ), f"Input file must be JSON or YAML, but got {os.path.splitext(file_path)[1]}"
275
+ json_path = os.path.join(out_dir, "tmp", f"{Path(file_path).stem}.json")
276
+ parse_yaml_to_json(file_path, json_path)
277
+ with open(json_path, "r") as f:
278
+ json_task_dict = json.load(f)[0]
279
+ bioassembly_dict = convert_to_bioassembly_dict(
280
+ json_task_dict, os.path.join(out_dir, "tmp")
281
+ )
282
+ if isinstance(bioassembly_dict, str):
283
+ bioassembly_dict = load_gzip_pickle(bioassembly_dict)
284
+
285
+ output_cif = os.path.join(out_dir, f"{Path(file_path).stem}_parsed_target.cif")
286
+ dump_bioassembly_to_cif(bioassembly_dict, output_cif)
287
+ generate_pml_from_json_input(output_cif, json_path)
288
+ return
289
+
290
+
291
+ # --- CLI Wrapper for Debugging ---
292
+ if __name__ == "__main__":
293
+ parser = argparse.ArgumentParser()
294
+ parser.add_argument("--yaml_file", help="Path to input YAML file")
295
+ parser.add_argument(
296
+ "--output_json_file", help="Path to output JSON file", default=None
297
+ )
298
+ args = parser.parse_args()
299
+
300
+ try:
301
+ result = parse_yaml_to_json(args.yaml_file, args.output_json_file)
302
+ except Exception as e:
303
+ print(f"Error: {e}")
304
+ sys.exit(1)
model/pxdesign/utils/pipeline.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 ByteDance and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -*- coding: utf-8 -*-
16
+ """
17
+ Utility helpers & constants for the design pipeline.
18
+ """
19
+
20
+ import ast
21
+ import os
22
+ import sys
23
+ from glob import glob
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+ import pandas as pd
28
+ from biotite.structure import get_residues
29
+ from protenix.data.constants import PROT_STD_RESIDUES_ONE_TO_THREE
30
+ from protenix.data.parser import MMCIFParser
31
+
32
+ from pxdesign.data.utils import CIFWriter, pdb_to_cif
33
+
34
+ # -------- Global subdir constants (used everywhere) --------
35
+ ORIG_SUBDIR = os.path.join("orig_designed")
36
+ AF2_SUBDIR = os.path.join("passing-AF2-IG-easy")
37
+ PTX_SUBDIR = os.path.join("passing-Protenix-basic")
38
+
39
+
40
+ # -------- Small general helpers --------
41
+
42
+
43
+ def convert_strlist_col(df: pd.DataFrame) -> pd.DataFrame:
44
+ """Convert stringified lists like '[0.3, 0.6]' to their numeric mean."""
45
+
46
+ def _safe_mean(v):
47
+ if isinstance(v, list):
48
+ return float(np.mean(v)) if len(v) > 0 else np.nan
49
+ if isinstance(v, str):
50
+ try:
51
+ parsed = ast.literal_eval(v)
52
+ if isinstance(parsed, list) and len(parsed) > 0:
53
+ return float(np.mean(parsed))
54
+ except Exception:
55
+ pass
56
+ return v
57
+
58
+ out = df.copy()
59
+ for c in out.columns:
60
+ out[c] = out[c].apply(_safe_mean)
61
+ return out
62
+
63
+
64
+ # -------- File post-processing --------
65
+
66
+
67
+ def add_terms_to_cif(input_cif: str, output_cif: str) -> None:
68
+ """Insert license/disclaimer blocks into a CIF file."""
69
+ terms1 = [
70
+ "# By using this file you agree to the legally binding terms of use found at https://protenix-server.com/terms-of-service\n",
71
+ "# Version: 0.1\n",
72
+ ]
73
+ terms2 = [
74
+ "#\n",
75
+ "loop_\n",
76
+ "_audit_author.name\n",
77
+ "_audit_author.pdbx_ordinal\n",
78
+ '"ByteDance Seed" 1\n',
79
+ "#\n",
80
+ "loop_\n",
81
+ "_pdbx_data_usage.details\n",
82
+ "_pdbx_data_usage.id\n",
83
+ "_pdbx_data_usage.type\n",
84
+ "_pdbx_data_usage.url\n",
85
+ ";NON-COMMERCIAL USE ONLY, BY USING THIS FILE YOU AGREE TO THE TERMS OF USE FOUND\n",
86
+ "AT https://protenix-server.com/terms-of-service.\n",
87
+ ";\n",
88
+ "1 license ?\n",
89
+ ";THE INFORMATION IS NOT INTENDED FOR, HAS NOT BEEN VALIDATED FOR, AND IS NOT\n",
90
+ "APPROVED FOR CLINICAL USE. IT SHOULD NOT BE USED FOR CLINICAL PURPOSE OR RELIED\n",
91
+ "ON FOR MEDICAL OR OTHER PROFESSIONAL ADVICE. IT IS THEORETICAL MODELLING ONLY\n",
92
+ 'AND CAUTION SHOULD BE EXERCISED IN ITS USE. IT IS PROVIDED "AS-IS" WITHOUT ANY\n',
93
+ "WARRANTY OF ANY KIND, WHETHER EXPRESSED OR IMPLIED. NO WARRANTY IS GIVEN THAT\n",
94
+ "USE OF THE INFORMATION SHALL NOT INFRINGE THE RIGHTS OF ANY THIRD PARTY.\n",
95
+ ";\n",
96
+ "2 disclaimer ?\n",
97
+ ]
98
+ with open(input_cif, "r") as f:
99
+ lines = f.readlines()
100
+ out, inserted = [], False
101
+ out.extend(terms1)
102
+ for line in lines:
103
+ out.append(line)
104
+ if not inserted and line.strip().startswith("_entry.id"):
105
+ out.extend(terms2)
106
+ inserted = True
107
+ with open(output_cif, "w") as f:
108
+ f.writelines(out)
109
+
110
+
111
+ def replace_last_xpb_chain_sequence(
112
+ input_cif_path: str, out_cif_path: str, new_sequence: str
113
+ ) -> str:
114
+ """Replace the sequence on the LAST chain where all residues are 'xpb'."""
115
+ parser = MMCIFParser(input_cif_path)
116
+ atom_array = parser.get_structure(
117
+ altloc="first", model=1, bond_lenth_threshold=None
118
+ )
119
+
120
+ uniq_chain_ids, first_idx = np.unique(atom_array.chain_id, return_index=True)
121
+ ordered_chain_ids = uniq_chain_ids[np.argsort(first_idx)]
122
+
123
+ candidates = []
124
+ for ch in ordered_chain_ids:
125
+ ch_mask = atom_array.chain_id == ch
126
+ if np.any(ch_mask) and np.all(atom_array.res_name[ch_mask] == "xpb"):
127
+ candidates.append(ch)
128
+ if not candidates:
129
+ raise ValueError("No chain found where all residues are 'xpb'.")
130
+
131
+ target_chain = candidates[-1]
132
+ ch_mask = atom_array.chain_id == target_chain
133
+ res_ids, _ = get_residues(atom_array[ch_mask])
134
+ if len(res_ids) != len(new_sequence):
135
+ raise ValueError(
136
+ f"Length mismatch on chain {target_chain}: structure={len(res_ids)} vs seq={len(new_sequence)}"
137
+ )
138
+
139
+ for resid, one in zip(res_ids, new_sequence):
140
+ if one not in PROT_STD_RESIDUES_ONE_TO_THREE:
141
+ raise ValueError(f"Invalid amino acid letter '{one}' in new_sequence.")
142
+ res3 = PROT_STD_RESIDUES_ONE_TO_THREE[one]
143
+ atom_array.res_name[
144
+ (atom_array.chain_id == target_chain) & (atom_array.res_id == resid)
145
+ ] = res3
146
+
147
+ writer = CIFWriter(atom_array=atom_array, entity_poly_type=parser.entity_poly_type)
148
+ writer.save_to_cif(out_cif_path, include_bonds=True)
149
+ return out_cif_path
150
+
151
+
152
+ # -------- Structure writers (project-specific) --------
153
+
154
+
155
+ def save_design_cif(
156
+ task,
157
+ base_dir: str,
158
+ output_dir: str,
159
+ output_subdir: str = "",
160
+ rank_col: str = "rank",
161
+ ) -> str:
162
+ """Write original designed CIF and inject license blocks."""
163
+ task_name = task["task_name"]
164
+ src = os.path.join(
165
+ base_dir,
166
+ f"global_run_{task['run_idx']}",
167
+ task_name,
168
+ f"seed_{task['seed']}",
169
+ "predictions",
170
+ task["name"] + ".cif",
171
+ )
172
+ dst = os.path.join(output_dir, output_subdir, f"rank_{task[rank_col]}.cif")
173
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
174
+ replace_last_xpb_chain_sequence(src, dst, task["sequence"])
175
+ add_terms_to_cif(dst, dst)
176
+ return dst
177
+
178
+
179
+ def save_af2_docked(
180
+ task,
181
+ base_dir: str,
182
+ output_dir: str,
183
+ output_subdir: str = "",
184
+ ) -> str:
185
+ """Copy AF2-docked PDB to <output>/<task>/<output_subdir>/rank_<k>.pdb"""
186
+ task_name, sample_name, rank = task["task_name"], task["name"], task["rank"]
187
+ matches = glob(
188
+ os.path.join(
189
+ base_dir,
190
+ f"global_run_{task['run_idx']}",
191
+ task_name,
192
+ f"seed_{task['seed']}",
193
+ "predictions",
194
+ "af2_pred",
195
+ f"*{sample_name}_seq{task['seq_idx']}_model*.pdb",
196
+ )
197
+ )
198
+ if len(matches) != 1:
199
+ raise FileNotFoundError(
200
+ f"Expect 1 AF2 PDB for {sample_name}, got {len(matches)}"
201
+ )
202
+ src = matches[0]
203
+ dst = os.path.join(output_dir, output_subdir, f"rank_{rank}.cif")
204
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
205
+ pdb_to_cif(src, dst, reset_res_id=False, pad_chain_id=True)
206
+ add_terms_to_cif(dst, dst)
207
+ return dst
208
+
209
+
210
+ def save_ptx_docked(
211
+ task,
212
+ base_dir: str,
213
+ output_dir: str,
214
+ output_subdir: str = "",
215
+ is_large: bool = True,
216
+ ) -> str:
217
+ """Write Protenix-docked CIF (with hotspot-based chain permutation)."""
218
+ sample_name, rank = task["name"], task["rank"]
219
+ ptx_redocked_name = f"run_{task['run_idx']}_{task['name']}_seq{task['seq_idx']}"
220
+ matches = glob(
221
+ os.path.join(
222
+ base_dir,
223
+ "ptx_pred" if is_large else "ptx_mini_pred",
224
+ ptx_redocked_name + "_seq0",
225
+ "seed_*",
226
+ "predictions",
227
+ f"{ptx_redocked_name}*.cif",
228
+ )
229
+ )
230
+ if len(matches) != 1:
231
+ raise FileNotFoundError(
232
+ f"Expect 1 PTX CIF for {sample_name}, got {len(matches)}"
233
+ )
234
+ src = matches[0]
235
+ dst = os.path.join(output_dir, output_subdir, f"rank_{rank}.cif")
236
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
237
+ add_terms_to_cif(src, dst)
238
+ return dst
239
+
240
+
241
+ # -------- Misc helpers --------
242
+
243
+
244
+ def trim_summary_df(df: pd.DataFrame) -> pd.DataFrame:
245
+ """Keep a curated subset of columns and rename AF2 metrics if present."""
246
+ base_metrics = [
247
+ "plddt",
248
+ "ptm_binder",
249
+ "ptm_target",
250
+ "iptm",
251
+ "ptm",
252
+ "iptm_binder",
253
+ "pred_design_rmsd",
254
+ ]
255
+ ptx_cols = ["ptx_" + m for m in base_metrics]
256
+ ptx_mini_cols = ["ptx_mini_" + m for m in base_metrics]
257
+ columns_to_keep = (
258
+ [
259
+ "rank",
260
+ "task_name",
261
+ "sequence",
262
+ "af2_easy_success",
263
+ "af2_opt_success",
264
+ "ptx_success",
265
+ "ptx_basic_success",
266
+ "pLDDT",
267
+ "pTM",
268
+ "i_pTM",
269
+ "pAE",
270
+ "unscaled_i_pAE",
271
+ "pLDDT_MONOMER",
272
+ "pTM_MONOMER",
273
+ "pAE_MONOMER",
274
+ "bound_unbound_RMSD",
275
+ "af2_binder_pred_design_rmsd",
276
+ "af2_complex_pred_design_rmsd",
277
+ ]
278
+ + ptx_cols
279
+ + ptx_mini_cols
280
+ + ["alpha", "beta", "loop", "Rg", "chosen_struct_type", "chosen_struct_path"]
281
+ )
282
+ existing_cols = [c for c in columns_to_keep if c in df.columns]
283
+ df = df[existing_cols].copy()
284
+
285
+ # ---- Rename AF2 columns to canonical names ----
286
+ df = df.rename(
287
+ columns={
288
+ "pLDDT": "af2_plddt",
289
+ "pTM": "af2_ptm",
290
+ "i_pTM": "af2_iptm",
291
+ "pAE": "af2_pAE",
292
+ "unscaled_i_pAE": "af2_ipAE",
293
+ "pLDDT_MONOMER": "af2_monomer_plddt",
294
+ "pTM_MONOMER": "af2_monomer_ptm",
295
+ "pAE_MONOMER": "af2_monomer_pAE",
296
+ "bound_unbound_RMSD": "af2_bound_unbound_RMSD",
297
+ "af2_easy_success": "AF2-IG-easy-success",
298
+ "af2_opt_success": "AF2-IG-success",
299
+ "ptx_success": "Protenix-success",
300
+ "ptx_basic_success": "Protenix-basic-success",
301
+ }
302
+ )
303
+ return df
304
+
305
+
306
+ def parse_hotspot_json(json_list):
307
+ """Parse hotspots from the input design JSON for downstream PTX permutation."""
308
+ result = {}
309
+ for entry in json_list:
310
+ name = entry["name"]
311
+ if "condition" in entry: # structure input
312
+ chain_ids = entry["condition"]["filter"]["chain_id"]
313
+ hotspot = entry.get("hotspot", {})
314
+ chain_mapping = {
315
+ chain_id: f"{chr(65 + i)}0" for i, chain_id in enumerate(chain_ids)
316
+ }
317
+ new_hotspot = {
318
+ chain_mapping[chain_id]: residues
319
+ for chain_id, residues in hotspot.items()
320
+ if chain_id in chain_mapping
321
+ }
322
+ else: # sequence input
323
+ assert "sequences" in entry
324
+ new_hotspot = {}
325
+ for i, seq_entity in enumerate(entry["sequences"]):
326
+ entity_type = list(seq_entity.keys())[0]
327
+ new_hotspot[f"{chr(65 + i)}0"] = seq_entity[entity_type].get(
328
+ "hotspot", {}
329
+ )
330
+ result[name] = new_hotspot
331
+ return result
332
+
333
+
334
+ def check_tool_weights() -> None:
335
+ """
336
+ Sanity check for required tool weights.
337
+ Equivalent to the original shell script.
338
+ """
339
+ root = os.environ.get("TOOL_WEIGHTS_ROOT")
340
+ if not root:
341
+ raise RuntimeError(
342
+ "Environment variable TOOL_WEIGHTS_ROOT is not set.\n"
343
+ "Please set TOOL_WEIGHTS_ROOT or run download_tool_weights.sh."
344
+ )
345
+
346
+ root = Path(root)
347
+
348
+ required_files = [
349
+ # ---- AF2 ----
350
+ root / "af2" / "params_model_1.npz",
351
+ root / "af2" / "params_model_1_ptm.npz",
352
+ ]
353
+
354
+ print(f"Checking tool weights in: {root}")
355
+
356
+ missing = [p for p in required_files if not p.is_file()]
357
+ if missing:
358
+ print()
359
+ print("Missing required tool weights:")
360
+ for p in missing:
361
+ print(f" {p}")
362
+ print()
363
+ print("Please run:")
364
+ print(" bash download_tool_weights.sh")
365
+ sys.exit(1)
model/setup.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 ByteDance and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from setuptools import find_packages, setup
19
+
20
+ this_directory = Path(__file__).parent
21
+ long_description = (this_directory / "README.md").read_text()
22
+ # Read requirements from the file
23
+ # with open("requirements.txt") as f:
24
+ # install_requires = f.read().splitlines()
25
+
26
+ # Check if the user specified the CPU option
27
+ # if "--cpu" in sys.argv:
28
+ # # Remove the gpu packages
29
+ # try:
30
+ # to_drop = [x for x in install_requires if "nvidia" in x or "cuda" in x]
31
+ # for x in to_drop:
32
+ # install_requires.remove(x)
33
+ # except ValueError:
34
+ # pass
35
+ # # Remove the --cpu option from sys.argv so setuptools doesn't get confused
36
+ # sys.argv.remove("--cpu")
37
+
38
+ setup(
39
+ name="pxdesign",
40
+ python_requires=">=3.10",
41
+ version="0.1.0",
42
+ description="PXDesign: a Fast, Modular, and Accurate Model For De Novo Protein Binder Design",
43
+ long_description=long_description,
44
+ long_description_content_type="text/markdown",
45
+ author="Bytedance Inc.",
46
+ url="https://github.com/bytedance/PXDesign",
47
+ author_email="ai4s-bio@bytedance.com",
48
+ packages=find_packages(
49
+ exclude=(
50
+ "assets",
51
+ "benchmark",
52
+ "*.egg-info",
53
+ )
54
+ ),
55
+ # install_requires=install_requires,
56
+ license="Apache 2.0 License",
57
+ platforms="manylinux1",
58
+ entry_points={
59
+ "console_scripts": [
60
+ "pxdesign = pxdesign.runner.cli:cli",
61
+ ],
62
+ },
63
+ )
requirements.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PyYaml
2
+ scipy
3
+ ml_collections
4
+ tqdm
5
+ pandas
6
+ optree
7
+ rdkit
8
+ biopython==1.83
9
+ modelcif==0.7
10
+ biotite==1.0.1
11
+ scikit-learn
12
+ scikit-learn-extra
13
+ deepspeed>=0.15.1
14
+ protobuf==3.20.2
15
+ icecream
16
+ ipdb
17
+ wandb
18
+ numpy==1.26.3
19
+ matplotlib==3.9.2
20
+ ipywidgets
21
+ py3Dmol
22
+ nvidia-cublas-cu12
23
+ torch==2.3.1
24
+ fair-esm
25
+ posix_ipc
26
+ einops
27
+ transformers==4.51.3
28
+ optax==0.2.5
29
+ dm-haiku==0.0.13
30
+ natsort
31
+ dm-tree
32
+ protenix>=0.1.0
33
+ pxdbench>=0.1.0
scripts/Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ai4s-cn-beijing.cr.volces.com/infra/protenix:v0.0.3
2
+
3
+ # Install Protenix
4
+ RUN pip --no-cache-dir install git+https://github.com/bytedance/Protenix.git@v0.5.0+pxd
5
+
6
+ # Install PXDesignBench
7
+ RUN pip install git+https://github.com/sokrypton/ColabDesign.git --no-deps
8
+ RUN pip install posix_ipc einops transformers==4.51.3 optax==0.2.5 dm-haiku==0.0.13
9
+ RUN pip install "jax[cuda]==0.4.29" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html
10
+ RUN pip install numpy==1.26.3 natsort dm-tree
11
+ RUN pip install git+https://github.com/bytedance/PXDesignBench.git@v0.1.2 --no-deps
12
+
13
+ # CUTLASS (for DeepSpeed Evo attention)
14
+ RUN git clone -b v3.5.1 https://github.com/NVIDIA/cutlass.git /opt/cutlass
15
+ ENV CUTLASS_PATH=/opt/cutlass
scripts/download_tool_weights.sh ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # download_model_weights.sh
3
+ #
4
+ # Usage:
5
+ # bash download_model_weights.sh # Download into ./tool_weights
6
+ # bash download_model_weights.sh /path/to/dir # Custom download root directory
7
+ #
8
+ # The final directory structure will look like:
9
+ # MODELS_ROOT/
10
+ # af2/
11
+ # params_model_{1..5}.npz
12
+ # params_model_{1..5}_ptm.npz
13
+ # params_model_{1..5}_multimer_v3.npz
14
+ # LICENSE
15
+ # mpnn/
16
+ # ca_model_weights/
17
+ # soluble_model_weights/
18
+ # vanilla_model_weights/
19
+ #
20
+ # After downloading, update pxdbench/globals.py to point to these locations.
21
+
22
+ set -euo pipefail
23
+
24
+ # Set root directory for storing all model weights
25
+ MODELS_ROOT="${1:-$(pwd)/tool_weights}"
26
+
27
+ AF2_DIR="${MODELS_ROOT}/af2"
28
+ MPNN_DIR="${MODELS_ROOT}/mpnn"
29
+
30
+ echo "Model root directory: ${MODELS_ROOT}"
31
+ mkdir -p "${AF2_DIR}" "${MPNN_DIR}"
32
+ ########################################
33
+ # 1. AlphaFold2 parameters
34
+ ########################################
35
+ echo "==> Downloading AlphaFold2 parameters ..."
36
+
37
+ AF2_TAR="alphafold_params_2022-12-06.tar"
38
+ AF2_URL="https://storage.googleapis.com/alphafold/${AF2_TAR}"
39
+
40
+ # If AF2 parameters already exist, skip download
41
+ if compgen -G "${AF2_DIR}/params_model_1*.npz" > /dev/null; then
42
+ echo " AlphaFold2 params appear to already exist — skipping download."
43
+ else
44
+ tmp_tar="${MODELS_ROOT}/${AF2_TAR}"
45
+ echo " Downloading from: ${AF2_URL}"
46
+ curl -L "${AF2_URL}" -o "${tmp_tar}"
47
+
48
+ echo " Extracting to: ${AF2_DIR}"
49
+ tar -xf "${tmp_tar}" -C "${AF2_DIR}"
50
+ rm -f "${tmp_tar}"
51
+
52
+ echo " AlphaFold2 parameters downloaded successfully."
53
+ fi
54
+
55
+
56
+ ########################################
57
+ # 2. ProteinMPNN weights
58
+ ########################################
59
+ echo "==> Downloading ProteinMPNN weights ..."
60
+
61
+ TMP_DIR="$(mktemp -d)"
62
+ cleanup() {
63
+ rm -rf "${TMP_DIR}"
64
+ }
65
+ trap cleanup EXIT
66
+
67
+ echo " Cloning dauparas/ProteinMPNN (shallow clone)..."
68
+ git clone --depth 1 https://github.com/dauparas/ProteinMPNN.git "${TMP_DIR}"
69
+
70
+ # Copy each weight directory
71
+ for subdir in ca_model_weights soluble_model_weights vanilla_model_weights; do
72
+ src="${TMP_DIR}/${subdir}"
73
+ dst="${MPNN_DIR}/${subdir}"
74
+
75
+ if [ -d "${dst}" ]; then
76
+ echo " ${subdir} already exists — skipping."
77
+ else
78
+ echo " Copying ${subdir} → ${dst}"
79
+ mkdir -p "${MPNN_DIR}"
80
+ cp -r "${src}" "${dst}"
81
+ fi
82
+ done
83
+
84
+ echo " ProteinMPNN weights are ready in: ${MPNN_DIR}"
85
+
86
+ ########################################
87
+ # 3. CCD cache (PXDesign release_data)
88
+ ########################################
89
+ echo "==> Downloading CCD cache ..."
90
+
91
+ CCD_DIR="${1:-$(pwd)/release_data/ccd_cache}"
92
+ mkdir -p "${CCD_DIR}"
93
+
94
+ CCD_COMPONENTS_URL="https://pxdesign.tos-cn-beijing.volces.com/release_data/components.v20240608.cif"
95
+ CCD_RDKIT_URL="https://pxdesign.tos-cn-beijing.volces.com/release_data/components.v20240608.cif.rdkit_mol.pkl"
96
+ PDB_CLUSTER_URL="https://pxdesign.tos-cn-beijing.volces.com/release_data/clusters-by-entity-40.txt"
97
+
98
+ CCD_COMPONENTS_FILE="${CCD_DIR}/components.v20240608.cif"
99
+ CCD_RDKIT_FILE="${CCD_DIR}/components.v20240608.cif.rdkit_mol.pkl"
100
+ PDB_CLUSTER_FILE="${CCD_DIR}/clusters-by-entity-40.txt"
101
+
102
+ download_if_missing() {
103
+ local url="$1"
104
+ local out="$2"
105
+ if [ -f "$out" ]; then
106
+ echo " $(basename "$out") already exists — skipping."
107
+ else
108
+ echo " Downloading $(basename "$out")"
109
+ curl -L -C - "$url" -o "$out"
110
+ fi
111
+ }
112
+
113
+ download_if_missing "${CCD_COMPONENTS_URL}" "${CCD_COMPONENTS_FILE}"
114
+ download_if_missing "${CCD_RDKIT_URL}" "${CCD_RDKIT_FILE}"
115
+ download_if_missing "${PDB_CLUSTER_URL}" "${PDB_CLUSTER_FILE}"
116
+
117
+ echo " CCD cache is ready in: ${CCD_DIR}"
118
+
119
+ ########################################
120
+
121
+ echo "==> All downloads completed."
122
+ echo "Model weight directories:"
123
+ echo " AF2: ${AF2_DIR}"
124
+ echo " MPNN: ${MPNN_DIR}"
125
+ echo "CCD cache: ${CCD_DIR}"
scripts/install.sh ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ############################################################
5
+ # PXDesign One-Click Installation Script
6
+ #
7
+ # This script will:
8
+ # 1. Create a dedicated conda/mamba/micromamba environment
9
+ # 2. Install GPU PyTorch matching a specified CUDA version
10
+ # 3. Install Protenix
11
+ # 4. Install PXDesignBench dependencies
12
+ # 5. Clone PXDesign repo and install
13
+ # 6. Run basic import sanity checks
14
+ #
15
+ # Supported options:
16
+ # --env <name> Conda/mamba environment name (default: pxdesign)
17
+ # --pkg_manager <tool> conda | mamba | micromamba (default: conda)
18
+ # --cuda-version <ver> CUDA version string, e.g. 12.1, 12.2, 12.4
19
+ # Required. Must be >= 12.1.
20
+ ############################################################
21
+
22
+ # Default configuration
23
+ env_name="pxdesign"
24
+ pkg_manager="conda" # conda | mamba | micromamba
25
+ cuda_version="" # e.g. 12.1, 12.2, 12.4
26
+
27
+ # ----------------------------------------------------------
28
+ # Parse command-line options
29
+ # ----------------------------------------------------------
30
+ OPTIONS=e:p:c:
31
+ LONGOPTIONS=env:,pkg_manager:,cuda-version:
32
+
33
+ PARSED=$(getopt --options="${OPTIONS}" --longoptions="${LONGOPTIONS}" --name "$0" -- "$@") || {
34
+ echo "Error: failed to parse command line options."
35
+ exit 1
36
+ }
37
+ eval set -- "${PARSED}"
38
+
39
+ while true; do
40
+ case "$1" in
41
+ -e|--env)
42
+ env_name="$2"
43
+ shift 2
44
+ ;;
45
+ -p|--pkg_manager)
46
+ pkg_manager="$2"
47
+ shift 2
48
+ ;;
49
+ -c|--cuda-version)
50
+ cuda_version="$2"
51
+ shift 2
52
+ ;;
53
+ --)
54
+ shift
55
+ break
56
+ ;;
57
+ *)
58
+ echo "Invalid option: $1" >&2
59
+ exit 1
60
+ ;;
61
+ esac
62
+ done
63
+
64
+ echo "=================================================="
65
+ echo " PXDesign Installation"
66
+ echo " Environment name : ${env_name}"
67
+ echo " Package manager : ${pkg_manager}"
68
+ echo " CUDA version : ${cuda_version:-<not specified>}"
69
+ echo "=================================================="
70
+
71
+ SECONDS=0
72
+
73
+ ############################################################
74
+ # CUDA version checks & PyTorch CUDA tag selection
75
+ ############################################################
76
+
77
+ # Helper: check if version >= 12.1
78
+ check_cuda_ge_12_1() {
79
+ local ver="$1"
80
+ local major="${ver%%.*}"
81
+ local rest="${ver#*.}"
82
+ local minor="${rest%%.*}"
83
+
84
+ if (( major > 12 )); then
85
+ return 0
86
+ elif (( major == 12 && minor >= 1 )); then
87
+ return 0
88
+ else
89
+ return 1
90
+ fi
91
+ }
92
+
93
+ if [ -z "${cuda_version}" ]; then
94
+ echo "Error: --cuda-version must be specified (e.g., --cuda-version 12.1)."
95
+ exit 1
96
+ fi
97
+
98
+ if ! check_cuda_ge_12_1 "${cuda_version}"; then
99
+ echo "Error: CUDA version must be >= 12.1, but got '${cuda_version}'."
100
+ exit 1
101
+ fi
102
+
103
+ # Decide PyTorch CUDA tag from CUDA version
104
+ # Extend this mapping as needed.
105
+ torch_tag=""
106
+ torch_version="2.3.1" # adjust if needed
107
+
108
+ if [[ "${cuda_version}" == 12.1* || "${cuda_version}" == 12.2* ]]; then
109
+ torch_tag="cu121"
110
+ elif [[ "${cuda_version}" == 12.4* || "${cuda_version}" == 12.5* ]]; then
111
+ torch_tag="cu124"
112
+ else
113
+ echo "Error: unsupported CUDA version '${cuda_version}' for this installer."
114
+ echo " Currently supported: 12.1, 12.2 (cu121) and 12.4+ (cu124)."
115
+ exit 1
116
+ fi
117
+
118
+ ############################################################
119
+ # Package manager detection and initialization
120
+ ############################################################
121
+
122
+ case "${pkg_manager}" in
123
+ conda)
124
+ if ! command -v conda >/dev/null 2>&1; then
125
+ echo "Error: conda is not installed or not in PATH."
126
+ exit 1
127
+ fi
128
+ env_tool="conda"
129
+ ;;
130
+ mamba)
131
+ if ! command -v mamba >/dev/null 2>&1; then
132
+ echo "Error: mamba is not installed or not in PATH."
133
+ exit 1
134
+ fi
135
+ if ! command -v conda >/dev/null 2>&1; then
136
+ echo "Error: mamba is installed but conda is not available."
137
+ exit 1
138
+ fi
139
+ env_tool="mamba"
140
+ ;;
141
+ micromamba)
142
+ if ! command -v micromamba >/dev/null 2>&1; then
143
+ echo "Error: micromamba is not installed or not in PATH."
144
+ exit 1
145
+ fi
146
+ env_tool="micromamba"
147
+ ;;
148
+ *)
149
+ echo "Error: unsupported pkg_manager '${pkg_manager}'. Use 'conda', 'mamba', or 'micromamba'."
150
+ exit 1
151
+ ;;
152
+ esac
153
+
154
+ install_dir=$(pwd)
155
+ echo "Install root : ${install_dir}"
156
+
157
+ ############################################################
158
+ # Create and activate environment
159
+ ############################################################
160
+
161
+ if [ "${env_tool}" = "micromamba" ]; then
162
+ echo ">>> Using micromamba to manage environments"
163
+
164
+ # Ensure MAMBA_ROOT_PREFIX is defined to avoid "unbound variable" under `set -u`
165
+ export MAMBA_ROOT_PREFIX="${MAMBA_ROOT_PREFIX:-$HOME/micromamba}"
166
+
167
+ # Initialize micromamba shell hook for bash
168
+ eval "$(micromamba shell hook -s bash)"
169
+
170
+ echo ">>> Creating environment '${env_name}' (Python 3.11) with micromamba"
171
+ micromamba create -y -n "${env_name}" python=3.11 || {
172
+ echo "Error: failed to create environment ${env_name} with micromamba"
173
+ exit 1
174
+ }
175
+
176
+ echo ">>> Activating environment '${env_name}' (micromamba)"
177
+ micromamba activate "${env_name}" || {
178
+ echo "Error: failed to activate environment ${env_name} with micromamba"
179
+ exit 1
180
+ }
181
+
182
+ else
183
+ echo ">>> Using ${env_tool} to manage environments"
184
+
185
+ CONDA_BASE=$(conda info --base 2>/dev/null) || {
186
+ echo "Error: conda is not installed or cannot be initialized."
187
+ exit 1
188
+ }
189
+
190
+ echo "Conda base : ${CONDA_BASE}"
191
+
192
+ echo ">>> Creating environment '${env_name}' (Python 3.11) with ${env_tool}"
193
+ "${env_tool}" create -y -n "${env_name}" python=3.11 || {
194
+ echo "Error: failed to create environment ${env_name} with ${env_tool}"
195
+ exit 1
196
+ }
197
+
198
+ echo ">>> Activating environment '${env_name}' (${env_tool})"
199
+ # shellcheck disable=SC1090
200
+ source "${CONDA_BASE}/etc/profile.d/conda.sh"
201
+ conda activate "${env_name}" || {
202
+ echo "Error: failed to activate environment ${env_name}"
203
+ exit 1
204
+ }
205
+
206
+ if [ "${CONDA_DEFAULT_ENV:-}" != "${env_name}" ]; then
207
+ echo "Error: expected environment '${env_name}' to be active, but got '${CONDA_DEFAULT_ENV:-}'."
208
+ exit 1
209
+ fi
210
+ fi
211
+
212
+ echo "Environment '${env_name}' successfully activated."
213
+
214
+ ############################################################
215
+ # Python package installation
216
+ ############################################################
217
+
218
+ echo ">>> Upgrading pip"
219
+ python -m pip install --upgrade pip
220
+
221
+ # ----------------------------------------------------------
222
+ # 1) Install GPU PyTorch first (matching CUDA version)
223
+ # ----------------------------------------------------------
224
+ echo ">>> Installing PyTorch (GPU, CUDA ${cuda_version}, tag ${torch_tag})"
225
+ pip install --no-cache-dir \
226
+ "torch==${torch_version}" \
227
+ --index-url "https://download.pytorch.org/whl/${torch_tag}" \
228
+ || { echo "Error: failed to install PyTorch ${torch_version} with ${torch_tag} wheels."; exit 1; }
229
+
230
+ python - << 'PYTORCH_CHECK'
231
+ import torch
232
+ print("PyTorch version:", torch.__version__)
233
+ print("CUDA available :", torch.cuda.is_available())
234
+ print("Torch CUDA :", torch.version.cuda)
235
+ if torch.cuda.is_available():
236
+ print("CUDA devices :", torch.cuda.device_count())
237
+ PYTORCH_CHECK
238
+
239
+ # ----------------------------------------------------------
240
+ # 2) Install Protenix & PXDesignBench
241
+ # ----------------------------------------------------------
242
+
243
+ echo ">>> Installing Protenix"
244
+ pip install --no-cache-dir "git+https://github.com/bytedance/Protenix.git@v0.5.0+pxd" \
245
+ || { echo "Error: failed to install Protenix."; exit 1; }
246
+
247
+ echo ">>> Installing PXDesignBench base dependencies"
248
+ pip install --no-cache-dir \
249
+ einops \
250
+ natsort \
251
+ dm-tree \
252
+ posix_ipc \
253
+ "transformers==4.51.3" \
254
+ "dm-haiku==0.0.13" \
255
+ "optax==0.2.5" \
256
+ || { echo "Error: failed to install base Python dependencies."; exit 1; }
257
+
258
+ echo ">>> Installing ColabDesign (without dependencies)"
259
+ pip install --no-cache-dir git+https://github.com/sokrypton/ColabDesign.git --no-deps \
260
+ || { echo "Error: failed to install ColabDesign."; exit 1; }
261
+
262
+ echo ">>> Installing JAX with CUDA support"
263
+ pip install --no-cache-dir \
264
+ "jax[cuda]==0.4.29" \
265
+ -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html \
266
+ || { echo "Error: failed to install JAX (CUDA build)."; exit 1; }
267
+
268
+ # downgrade numpy
269
+ pip install --no-cache-dir \
270
+ "numpy==1.26.3" \
271
+ || { echo "Error: failed to install numpy 1.26.3."; exit 1; }
272
+
273
+ echo ">>> Installing PXDesignBench"
274
+ pip install --no-cache-dir git+https://github.com/bytedance/PXDesignBench.git@v0.1.2 --no-deps \
275
+ || { echo "Error: failed to install PXDesignBench."; exit 1; }
276
+
277
+ echo ">>> Installing PXDesign"
278
+ pip install -e .
279
+
280
+ if [ "${env_tool}" = "micromamba" ]; then
281
+ micromamba install -c conda-forge cudnn -y || { echo "Error: failed to install cudnn with micromamba."; exit 1; }
282
+ else
283
+ conda install -c conda-forge cudnn -y || { echo "Error: failed to install cudnn with conda."; exit 1; }
284
+ fi
285
+
286
+
287
+ # -------------------------------
288
+ # 3) CUTLASS (for DeepSpeed Evo attention)
289
+ # -------------------------------
290
+
291
+ # Default to $HOME/cutlass if CUTLASS_PATH is not set by the user
292
+ export CUTLASS_PATH="${CUTLASS_PATH:-$HOME/cutlass}"
293
+
294
+ echo "[CUTLASS] Using CUTLASS_PATH=${CUTLASS_PATH}"
295
+
296
+ if [ ! -d "${CUTLASS_PATH}" ]; then
297
+ echo "[CUTLASS] CUTLASS not found, cloning NVIDIA/cutlass v3.5.1 ..."
298
+ git clone -b v3.5.1 https://github.com/NVIDIA/cutlass.git "${CUTLASS_PATH}"
299
+ else
300
+ echo "[CUTLASS] Existing CUTLASS directory detected, skipping clone."
301
+ fi
302
+
303
+
304
+ ############################################################
305
+ # Sanity checks
306
+ ############################################################
307
+
308
+ echo ">>> Running sanity checks (import tests)"
309
+
310
+ python - << 'PYCODE'
311
+ import sys
312
+
313
+ def check(mod):
314
+ try:
315
+ __import__(mod)
316
+ print(f"[OK] import {mod}")
317
+ except Exception as e:
318
+ print(f"[FAIL] import {mod}: {e}", file=sys.stderr)
319
+ raise
320
+
321
+ modules = [
322
+ "torch",
323
+ "jax",
324
+ "jax.numpy",
325
+ "colabdesign",
326
+ "protenix",
327
+ "pxdbench",
328
+ "pxdesign"
329
+ ]
330
+
331
+ for m in modules:
332
+ check(m)
333
+
334
+ import jax
335
+ print("JAX devices:", jax.devices())
336
+ PYCODE
337
+
338
+ echo "Sanity checks completed."
339
+
340
+ ############################################################
341
+ # Cleanup and final message
342
+ ############################################################
343
+
344
+ echo ">>> Cleaning up package manager caches"
345
+
346
+ if [ "${env_tool}" = "micromamba" ]; then
347
+ micromamba clean -a -y || echo "Warning: failed to clean micromamba caches."
348
+ micromamba deactivate || true
349
+ else
350
+ "${env_tool}" clean -a -y || echo "Warning: failed to clean ${env_tool} caches."
351
+ conda deactivate || true
352
+ fi
353
+
354
+ t=${SECONDS}
355
+ echo "=================================================="
356
+ echo " PXDesign environment setup done!"
357
+ echo " Environment name : ${env_name}"
358
+ echo " Package manager : ${pkg_manager}"
359
+ echo " CUDA version : ${cuda_version} (torch tag: ${torch_tag})"
360
+ echo
361
+ echo " Activate with:"
362
+ if [ "${env_tool}" = "micromamba" ]; then
363
+ echo " micromamba activate ${env_name}"
364
+ else
365
+ echo " conda activate ${env_name}"
366
+ fi
367
+ echo
368
+ echo " Installation time: $((t / 3600))h $(((t / 60) % 60))m $((t % 60))s"
369
+ echo "=================================================="
weight/release_data/ccd_cache/clusters-by-entity-40.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ab4af905e75b382eda8dec59917dc3608bee0729e36b9e71baf860bbe86850c
3
+ size 21699572
weight/release_data/ccd_cache/components.v20240608.cif ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7240b17369ccfbbcc86e2d02dc8c9db59f46c32e0420f58889c6c121c60bfef0
3
+ size 427767290
weight/release_data/ccd_cache/components.v20240608.cif.rdkit_mol.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2d6caced2d26c62015115a1d0a50f4106755a300e0c2a2d2b5c101c9038dcbcd
3
+ size 125903802
weight/release_data/checkpoint/protenix_base_default_v0.5.0.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9ea20b0aba42f2256711da1d0cd081510a4b291e64375bff6b70ced70b87a5f1
3
+ size 1474265486
weight/release_data/checkpoint/protenix_mini_default_v0.5.0.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3803340c5d9958c038e799ddd2b53b532db21855f261592ad455a5f003791f81
3
+ size 537049294
weight/release_data/checkpoint/protenix_mini_tmpl_v0.5.0.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:221ca4da769e36ea0e2fa1fa82c46f6ca3a00bc7a1eff8ec0e3ddc02ed830474
3
+ size 537487513
weight/release_data/checkpoint/pxdesign_v0.1.0.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b075867bae942dc0c6487173736922b0e2913308c1ba542d227418b6e176478d
3
+ size 556554618