Download model/PXDesignBench/pxdbench/scripts/postprocess_binder.py from OneScience-Group/PXDesign: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/OneScience-Group/PXDesign/resolve/4d20b627a1ce9f2dceb4e542f169fbbfd298d458/model/PXDesignBench/pxdbench/scripts/postprocess_binder.py
- Command line
-
hf download hf://OneScience-Group/PXDesign@4d20b627a1ce9f2dceb4e542f169fbbfd298d458/model/PXDesignBench/pxdbench/scripts/postprocess_binder.py
-
curl -L -o postprocess_binder.py https://huggingface.co/OneScience-Group/PXDesign/resolve/4d20b627a1ce9f2dceb4e542f169fbbfd298d458/model/PXDesignBench/pxdbench/scripts/postprocess_binder.py
10.4 kB
| import argparse | |
| import multiprocessing as mp | |
| import os | |
| import subprocess | |
| import warnings | |
| from glob import glob | |
| from itertools import combinations | |
| import numpy as np | |
| from biotite.structure.io import load_structure | |
| from biotite.structure.io.pdb import PDBFile | |
| from pxdbench.globals import TMALIGN_PATH | |
| from pxdbench.utils import convert_cifs_to_pdbs, str2bool | |
| warnings.filterwarnings("ignore", module="biotite") | |
| def run_tmalign(pdb1, pdb2, tmalign_path="TMalign"): | |
| """ | |
| Run TM-align on two PDB files and return the output. | |
| Args: | |
| pdb1 (str): Path to the first PDB file | |
| pdb2 (str): Path to the second PDB file | |
| tmalign_path (str): Path to the TM-align executable | |
| Returns: | |
| str: The output from TM-align | |
| """ | |
| cmd = [tmalign_path, pdb1, pdb2] | |
| try: | |
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) | |
| return result.stdout | |
| except subprocess.CalledProcessError as e: | |
| print(f"Error running TMalign: {e}") | |
| return None | |
| def extract_tmscore(tmalign_output): | |
| """ | |
| Extract the TM-score from the TM-align output. | |
| Args: | |
| tmalign_output (str): The output from TM-align | |
| Returns: | |
| float: The TM-score (average of the two normalized scores), or None if not found | |
| """ | |
| if tmalign_output is None: | |
| return None | |
| # TM-align returns two TM-scores (normalized by different lengths) | |
| tm_scores = [] | |
| for line in tmalign_output.split("\n"): | |
| if "TM-score=" in line: | |
| try: | |
| tm_score = float(line.split("=")[1].split("(")[0].strip()) | |
| tm_scores.append(tm_score) | |
| except (IndexError, ValueError): | |
| continue | |
| if tm_scores: | |
| return sum(tm_scores) / len(tm_scores) | |
| return None | |
| def cluster_worker(args): | |
| pdb1, pdb2, i, j, tmalign_path = args | |
| tmalign_output = run_tmalign(pdb1, pdb2, tmalign_path) | |
| tm_score = extract_tmscore(tmalign_output) | |
| return (i, j, tm_score) | |
| def calculate_pairwise_tmscores(pdb_files, tmalign_path): | |
| """ | |
| Calculate pairwise TM-scores for a list of PDB files, in parallel. | |
| Args: | |
| pdb_files (list): List of paths to PDB files | |
| tmalign_path (str): Path to the TM-align executable | |
| Returns: | |
| numpy.ndarray: Matrix of pairwise TM-scores | |
| """ | |
| n = len(pdb_files) | |
| tm_matrix = np.zeros((n, n)) | |
| # Set diagonal to 1.0 | |
| np.fill_diagonal(tm_matrix, 1.0) | |
| # Create list of pairs (i, j) | |
| pairs = list(combinations(range(n), 2)) | |
| # Prepare arguments for multiprocessing | |
| args_list = [(pdb_files[i], pdb_files[j], i, j, tmalign_path) for i, j in pairs] | |
| num_workers = min(40, mp.cpu_count()) | |
| with mp.Pool(processes=num_workers) as pool: | |
| results = pool.map(cluster_worker, args_list) | |
| for i, j, tm_score in results: | |
| if tm_score is not None: | |
| tm_matrix[i, j] = tm_score | |
| tm_matrix[j, i] = tm_score | |
| return tm_matrix | |
| def greedy_clustering(pdb_files, tm_matrix, threshold): | |
| """ | |
| Perform greedy clustering based on TM-scores. | |
| This implementation selects the structure with the most | |
| unassigned neighbors above the threshold as the next cluster center. | |
| Args: | |
| pdb_files (list): List of paths to PDB files | |
| tm_matrix (numpy.ndarray): Matrix of pairwise TM-scores | |
| threshold (float): TM-score threshold for clustering | |
| Returns: | |
| list: List of clusters, where each cluster is a list of PDB file indices | |
| """ | |
| n = len(pdb_files) | |
| assigned = [False] * n | |
| clusters = [] | |
| print(f"Performing greedy clustering with TM-score threshold {threshold}...") | |
| while not all(assigned): | |
| # Find unassigned structure with most unassigned neighbors | |
| max_neighbors = -1 | |
| center_idx = -1 | |
| for i in range(n): | |
| if assigned[i]: | |
| continue | |
| # Count unassigned neighbors (including self) | |
| count = sum( | |
| 1 for j in range(n) if not assigned[j] and tm_matrix[i, j] >= threshold | |
| ) | |
| if count > max_neighbors: | |
| max_neighbors = count | |
| center_idx = i | |
| if center_idx == -1: | |
| break # No unassigned structures left | |
| # Create new cluster | |
| current_cluster = [center_idx] | |
| assigned[center_idx] = True | |
| # Add all similar unassigned structures to the cluster | |
| for j in range(n): | |
| if not assigned[j] and tm_matrix[center_idx, j] >= threshold: | |
| current_cluster.append(j) | |
| assigned[j] = True | |
| clusters.append(current_cluster) | |
| print(f"Created cluster {len(clusters)} with {len(current_cluster)} structures") | |
| return clusters | |
| def save_tm_matrix(tm_matrix, pdb_files, output_file): | |
| """ | |
| Save the TM-score matrix to a file. | |
| Args: | |
| tm_matrix (numpy.ndarray): Matrix of pairwise TM-scores | |
| pdb_files (list): List of paths to PDB files | |
| output_file (str): Path to the output file | |
| """ | |
| with open(output_file, "w") as f: | |
| # Write header | |
| f.write("# TM-score matrix\n") | |
| f.write("# Format: <pdb_i> <pdb_j> <tm_score>\n\n") | |
| n = len(pdb_files) | |
| for i in range(n): | |
| for j in range(i, n): # Only upper triangle including diagonal | |
| pdb_i = os.path.basename(pdb_files[i]) | |
| pdb_j = os.path.basename(pdb_files[j]) | |
| tm_score = tm_matrix[i, j] | |
| f.write(f"{pdb_i} {pdb_j} {tm_score:.4f}\n") | |
| def extract_chain_from_pdb( | |
| pdb_dir: str, | |
| pdb_files: list[str], | |
| chain_id: str, | |
| ): | |
| new_file_list = [] | |
| folder_path = f"{pdb_dir}/tmp" | |
| os.makedirs(folder_path, exist_ok=True) | |
| for input_file_path in pdb_files: | |
| name = input_file_path.split("/")[-1].split(".")[0] | |
| pdb_file_path = f"{pdb_dir}/tmp/{name}.pdb" | |
| # we only consider input pdb file | |
| structure = load_structure(input_file_path) | |
| # make sure chain id is in structure | |
| chain_structure = structure[structure.chain_id == chain_id] | |
| if len(chain_structure) == 0: | |
| raise ValueError(f"chain {chain_id} not in {input_file_path}") | |
| pdb_file = PDBFile() | |
| pdb_file.set_structure(chain_structure) | |
| pdb_file.write(pdb_file_path) | |
| new_file_list.append(pdb_file_path) | |
| print(f"finish extract chain") | |
| return folder_path, new_file_list | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Perform pairwise TM-align and greedy clustering of PDB files" | |
| ) | |
| parser.add_argument( | |
| "--input_dir", required=True, help="Directory containing PDB/CIF files" | |
| ) | |
| parser.add_argument("--output_dir", type=str, default=None) | |
| parser.add_argument( | |
| "--threshold", | |
| type=float, | |
| default=0.5, | |
| help="TM-score threshold for clustering (default: 0.5)", | |
| ) | |
| parser.add_argument( | |
| "--clusters_output", | |
| default="clusters.txt", | |
| help="Output file for clustering results (default: clusters.txt)", | |
| ) | |
| parser.add_argument( | |
| "--matrix_output", | |
| default="tm_matrix.txt", | |
| help="Output file for TM-score matrix (default: tm_matrix.txt)", | |
| ) | |
| parser.add_argument( | |
| "--tmalign_path", | |
| default=TMALIGN_PATH, | |
| help="Path to the TM-align executable (default: TMalign in PATH)", | |
| ) | |
| parser.add_argument( | |
| "--is_mmcif", default=False, type=str2bool, help="input file type, mmcif or pdb" | |
| ) | |
| parser.add_argument("--binder_chain", default=None, help="only calculate one chain") | |
| args = parser.parse_args() | |
| # Get all PDB files in the directory | |
| if args.is_mmcif: | |
| pdb_dir, pdb_names, _, _ = convert_cifs_to_pdbs( | |
| args.input_dir, | |
| out_pdb_dir=os.path.join(args.input_dir, "converted_pdbs"), | |
| ) | |
| pdb_files = sorted([os.path.join(pdb_dir, fn + ".pdb") for fn in pdb_names]) | |
| else: | |
| pdb_dir = args.input_dir | |
| pdb_files = sorted(glob(os.path.join(args.input_dir, "*.pdb"))) | |
| if not pdb_files: | |
| print(f"No PDB files found in {args.input_dir}") | |
| return | |
| if args.binder_chain is not None: | |
| if len(args.binder_chain) > 1: | |
| args.binder_chain = args.binder_chain[0] | |
| print( | |
| f"Use the chain ID in the PDB file -- trim it to one char: {args.binder_chain}" | |
| ) | |
| pdb_dir, pdb_files = extract_chain_from_pdb( | |
| pdb_dir, pdb_files, args.binder_chain | |
| ) | |
| print(f"Found {len(pdb_files)} PDB files") | |
| if args.output_dir is None: | |
| output_dir = os.path.join(args.input_dir, "postprocess") | |
| else: | |
| output_dir = args.output_dir | |
| os.makedirs(output_dir, exist_ok=True) | |
| # Calculate pairwise TM-scores | |
| tm_matrix = calculate_pairwise_tmscores(pdb_files, args.tmalign_path) | |
| # Save the TM-score matrix | |
| save_tm_matrix(tm_matrix, pdb_files, os.path.join(output_dir, args.matrix_output)) | |
| print(f"TM-score matrix saved to {os.path.join(output_dir, args.matrix_output)}") | |
| # Perform greedy clustering | |
| clusters = greedy_clustering(pdb_files, tm_matrix, args.threshold) | |
| # Write clustering results | |
| with open(os.path.join(output_dir, args.clusters_output), "w") as f: | |
| f.write(f"# Clustering with TM-score threshold: {args.threshold}\n") | |
| f.write(f"# Number of clusters: {len(clusters)}\n\n") | |
| for i, cluster in enumerate(clusters): | |
| f.write(f"Cluster {i+1} (size: {len(cluster)}):\n") | |
| # Write the representative (center) first | |
| f.write(f" {os.path.basename(pdb_files[cluster[0]])} (center)\n") | |
| # Write the rest of the cluster members | |
| for idx in cluster[1:]: | |
| f.write(f" {os.path.basename(pdb_files[idx])}\n") | |
| f.write("\n") | |
| print(f"Clustering completed. Found {len(clusters)} clusters.") | |
| print(f"Results written to {os.path.join(output_dir, args.clusters_output)}") | |
| if __name__ == "__main__": | |
| main() | |