anzhi2710gmailcom's picture
Upload folder using huggingface_hub
d766458 verified
Raw
History Blame Contribute Delete
7.83 kB
# Copyright 2025 ByteDance and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
import pandas as pd
from pxdbench.tasks.base import BaseTask
from pxdbench.tools.protmpnn.main_mpnn import get_gt_sequence
from pxdbench.tools.protmpnn.mpnn_predictor import MPNNPredictor
from pxdbench.utils import save_eval_results
from .registry import register_task
@register_task("binder")
class BinderTask(BaseTask):
def __init__(self, input_data, cfg, device_id: int, seed: int):
"""
Initialize a BinderTask instance.
Args:
input_data (dict): Task input parameters including PDB paths and chain specifications.
cfg (dict): Configuration dictionary with task settings.
device_id (int): GPU device ID (-1 for CPU).
seed (int): Random seed for reproducibility.
Validates:
- Exactly one binder chain is specified (multiple binder chains not supported).
"""
self.task_type = "binder"
self.task_name = input_data.get("name", "binder")
assert "cond_chains" in input_data
assert "binder_chains" in input_data
self.cond_chains = input_data["cond_chains"]
self.binder_chains = input_data["binder_chains"]
self.pdb_name_to_binder_seq_list = input_data.get(
"pdb_name_to_binder_seq_list", None
)
if input_data.get("orig_seqs_json", None) is not None:
with open(input_data["orig_seqs_json"], "r") as f:
self.orig_seqs = json.load(f)
elif input_data.get("orig_seqs", None) is not None:
self.orig_seqs = input_data["orig_seqs"]
else:
self.orig_seqs = None
# Default values
self.use_binder_seq_list = cfg.get("use_binder_seq_list", False)
self.eval_diversity = cfg.get("eval_diversity", False)
self.eval_binder_monomer = cfg.get("eval_binder_monomer", True)
self.eval_complex = cfg.get("eval_complex", True)
self.eval_protenix_mini = cfg.get("eval_protenix_mini", True)
self.eval_protenix = cfg.get("eval_protenix", False)
# Check values
assert (
len(self.binder_chains) == 1
), f"Get {len(self.binder_chains)} binder chains, but only 1 is allowed."
super().__init__(input_data, cfg, device_id, seed)
def prepare_data_from_seq_list(self):
datas = []
for name in self.pdb_names:
binder_seq_list = self.pdb_name_to_binder_seq_list[name]
for i, seq in enumerate(binder_seq_list):
data = {"name": name, "seq_idx": i, "sequence": seq}
datas.append(data)
return datas
def design_sequence(self, verbose=True):
"""
Generates binder sequences based on task configuration.
Supports three modes:
1. Use pre-provided sequence lists (self.use_binder_seq_list)
2. Use ground truth sequences from PDB files (self.use_gt_seq)
3. De novo design using MPNN (default)
Args:
verbose (bool, optional): Whether to print detailed progress. Defaults to True.
Returns:
list[dict]: List of design results with keys "name", "seq_idx", and "sequence".
"""
if self.use_binder_seq_list:
results = self.prepare_data_from_seq_list()
elif self.use_gt_seq:
results = get_gt_sequence(
self.pdb_dir, self.pdb_names, self.binder_chains[0]
)
else:
mpnn_predictor = MPNNPredictor(
self.cfg.tools.mpnn,
device_id=self.device_id,
verbose=verbose,
seed=self.seed,
)
results = mpnn_predictor.design_binder(
self.pdb_dir,
self.pdb_names,
self.num_seqs,
binder_chains=self.binder_chains,
cond_chains=self.cond_chains,
)
return results
def run(self):
"""
Executes the complete binder design evaluation workflow.
Workflow steps:
1. Designs sequences via design_sequence()
2. Runs structure predictions (AF2 complex/monomer, Protenix) based on config
3. Calculates secondary structure and diversity metrics
4. Saves sample-level results to CSV and summary metrics to JSON
Returns:
dict: Dictionary with task metadata and output file paths.
"""
results = self.design_sequence()
self.check_results(results)
binder_chain = self.binder_chains[0]
af2_pred_path = os.path.join(self.out_dir, "af2_pred")
if self.eval_complex:
self.af2_complex_predict(results, af2_pred_path)
if self.eval_binder_monomer:
self.af2_monomer_predict(results, af2_pred_path)
if self.eval_protenix_mini:
self.protenix_predict(results, orig_seqs=self.orig_seqs)
if self.eval_protenix:
self.protenix_predict(results, orig_seqs=self.orig_seqs, is_large=True)
self.cal_secondary(results, binder_chain)
div = self.cal_diversity()
sample_df = pd.DataFrame(results)
sample_df = sample_df.sort_values(by=["name", "seq_idx"])
self.compute_success_rate(self.cfg.filters, sample_df)
summary_dict = {"task": self.task_type, "name": self.task_name}
summary_dict.update(
self.summary_from_df(sample_df, other_metrics={"diversity": div})
)
sample_save_path, summary_save_path = save_eval_results(
sample_df, summary_dict, self.out_dir, self.sample_fn, self.summary_fn
)
print(
f"Eval done! Results are saved in {sample_save_path} and {summary_save_path}"
)
return {
"task": self.task_type,
"name": self.task_name,
"sample_save_path": sample_save_path,
"summary_save_path": summary_save_path,
}
def check_results(self, results):
"""
Validates design results for consistency and correctness.
Checks:
1. No duplicate entries (by structure name + sequence index)
2. Correct number of sequences per structure (when not using pre-provided lists)
Args:
results (list[dict]): List of design results from design_sequence()
Raises:
ValueError: If duplicates are found or sequence count is incorrect.
"""
result_names = [
result["name"] + f"_seq{result['seq_idx']}" for result in results
]
if len(result_names) != len(set(result_names)):
raise ValueError(f"Found duplicate names in results: {result_names}.")
if self.use_binder_seq_list or self.use_gt_seq:
pass
elif len(result_names) != len(self.pdb_names) * self.num_seqs:
raise ValueError(
f"Found {len(result_names)} results, but {len(self.pdb_names)} pdb_names, each with {self.num_seqs} seqs are provided."
)
return