Upload folder using huggingface_hub (part 3)
Browse files- .gitattributes +1 -0
- model/pxdesign/utils/infer.py +535 -0
- model/pxdesign/utils/inputs.py +304 -0
- model/pxdesign/utils/pipeline.py +365 -0
- model/setup.py +63 -0
- requirements.txt +33 -0
- scripts/Dockerfile +15 -0
- scripts/download_tool_weights.sh +125 -0
- scripts/install.sh +369 -0
- weight/release_data/ccd_cache/clusters-by-entity-40.txt +3 -0
- weight/release_data/ccd_cache/components.v20240608.cif +3 -0
- weight/release_data/ccd_cache/components.v20240608.cif.rdkit_mol.pkl +3 -0
- weight/release_data/checkpoint/protenix_base_default_v0.5.0.pt +3 -0
- weight/release_data/checkpoint/protenix_mini_default_v0.5.0.pt +3 -0
- weight/release_data/checkpoint/protenix_mini_tmpl_v0.5.0.pt +3 -0
- weight/release_data/checkpoint/pxdesign_v0.1.0.pt +3 -0
.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
|