Download scripts/get_predictions.py from OneScience-Group/Chainsaw: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/OneScience-Group/Chainsaw/resolve/main/scripts/get_predictions.py
- Command line
-
hf download hf://OneScience-Group/Chainsaw/scripts/get_predictions.py
-
curl -L -o get_predictions.py https://huggingface.co/OneScience-Group/Chainsaw/resolve/main/scripts/get_predictions.py
15.3 kB
| """ | |
| Script for running Chainsaw | |
| Created by: Jude Wells 2023-04-19 | |
| User can provide any of the following as an input to get predictions: | |
| - a single uniprot id (alphafold model will be downloaded and parsed) | |
| - a list of uniprot ids (alphafold model will be downloaded and parsed) | |
| - a list of pdb ids (alphafold model will be downloaded and parsed) | |
| - a path to a directory with PDBs or MMCIF files | |
| """ | |
| import argparse | |
| import csv | |
| import hashlib | |
| import logging | |
| import os | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from typing import List | |
| REPO_ROOT = Path(__file__).resolve().parents[1] | |
| if str(REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(REPO_ROOT)) | |
| from torch import compile as torch_compile | |
| from model import constants, featurisers | |
| from model.domain_assignment.util import convert_domain_dict_strings | |
| from model.factories import pairwise_predictor | |
| from model.models.results import PredictionResult | |
| from model.prediction_result_file import PredictionResultsFile | |
| from model.utils import common as common_utils | |
| from model.utils.pymol_3d_visuals import generate_pymol_image | |
| LOG = logging.getLogger(__name__) | |
| OUTPUT_COLNAMES = ['chain_id', 'sequence_md5', 'nres', 'ndom', 'chopping', 'confidence', 'time_sec'] | |
| ACCEPTED_STRUCTURE_FILE_SUFFIXES = ['.pdb', '.cif'] | |
| def setup_logging(): | |
| loglevel = os.environ.get('LOGLEVEL', 'INFO').upper() | |
| # log all messages to stderr so results can be sent to stdout | |
| logging.basicConfig(level=loglevel, | |
| stream=sys.stderr, | |
| format='%(asctime)s | %(levelname)s | %(message)s', | |
| datefmt='%m/%d/%Y %I:%M:%S %p') | |
| def get_input_method(args): | |
| number_of_input_methods = sum([ args.uniprot_id is not None, | |
| args.uniprot_id_list_file is not None, | |
| args.structure_directory is not None, | |
| args.structure_file is not None, | |
| args.pdb_id_list_file is not None, | |
| args.pdb_id is not None]) | |
| if number_of_input_methods != 1: | |
| raise ValueError('Exactly one input method must be provided') | |
| if args.uniprot_id is not None: | |
| return 'uniprot_id' | |
| elif args.uniprot_id_list_file is not None: | |
| return 'uniprot_id_list_file' | |
| elif args.structure_directory is not None: | |
| return 'structure_directory' | |
| elif args.structure_file is not None: | |
| return 'structure_file' | |
| else: | |
| raise ValueError('No input method provided') | |
| def load_model(*, | |
| model_dir: str, | |
| config_dir: str = None, | |
| remove_disordered_domain_threshold: float = 0.35, | |
| min_ss_components: int = 2, | |
| min_domain_length: int = 30, | |
| post_process_domains: bool = True,): | |
| model_dir = Path(model_dir) | |
| if config_dir is None: | |
| packaged_config_dir = constants.REPO_ROOT / "conf" / model_dir.name | |
| config_dir = packaged_config_dir if packaged_config_dir.is_dir() else model_dir | |
| config_dir = Path(config_dir) | |
| config = common_utils.load_json(config_dir / "config.json") | |
| feature_config = common_utils.load_json(config_dir / "feature_config.json") | |
| config["learner"]["remove_disordered_domain_threshold"] = remove_disordered_domain_threshold | |
| config["learner"]["post_process_domains"] = post_process_domains | |
| config["learner"]["min_ss_components"] = min_ss_components | |
| config["learner"]["min_domain_length"] = min_domain_length | |
| config["learner"]["dist_transform_type"] = config["data"].get("dist_transform", 'min_replace_inverse') | |
| config["learner"]["distance_denominator"] = config["data"].get("distance_denominator", None) | |
| learner = pairwise_predictor(config["learner"], output_dir=str(model_dir)) | |
| learner.feature_config = feature_config | |
| learner.load_checkpoints() | |
| learner.eval() | |
| try: | |
| learner = torch_compile(learner) | |
| except: | |
| pass | |
| return learner | |
| def predict(model, pdb_path, renumber_pdbs=True, pdbchain=None) -> List[PredictionResult]: | |
| """ | |
| Makes the prediction and returns a list of PredictionResult objects | |
| """ | |
| start = time.time() | |
| # get model structure metadata | |
| model_structure = featurisers.get_model_structure(pdb_path) | |
| if pdbchain is None: | |
| LOG.warning(f"No chain specified for {pdb_path}, using first chain") | |
| # get all the chain ids from the model structure | |
| all_chain_ids = [c.id for c in model_structure.get_chains()] | |
| # take the first chain id | |
| pdbchain = all_chain_ids[0] | |
| model_residues = featurisers.get_model_structure_residues(model_structure, chain=pdbchain) | |
| model_res_label_by_index = { int(r.index): str(r.res_label) for r in model_residues} | |
| model_structure_seq = "".join([r.aa for r in model_residues]) | |
| model_structure_md5 = hashlib.md5(model_structure_seq.encode('utf-8')).hexdigest() | |
| x = featurisers.inference_time_create_features(pdb_path, | |
| feature_config=model.feature_config, | |
| chain=pdbchain, | |
| renumber_pdbs=renumber_pdbs, | |
| model_structure=model_structure, | |
| ) | |
| A_hat, domain_dict, confidence = model.predict(x) | |
| # Convert 0-indexed to 1-indexed to match AlphaFold indexing: | |
| domain_dict = [{k: [r + 1 for r in v] for k, v in d.items()} for d in domain_dict] | |
| names_str, bounds_str = convert_domain_dict_strings(domain_dict[0]) | |
| confidence = confidence[0] | |
| if names_str == "": | |
| names = bounds = () | |
| else: | |
| names = names_str.split('|') | |
| bounds = bounds_str.split('|') | |
| assert len(names) == len(bounds) | |
| class Seg: | |
| def __init__(self, domain_id: str, start_index: int, end_index: int): | |
| self.domain_id = domain_id | |
| self.start_index = int(start_index) | |
| self.end_index = int(end_index) | |
| def res_label_of_index(self, index: int): | |
| if index not in model_res_label_by_index: | |
| raise ValueError(f"Index {index} not in model_res_label_by_index ({model_res_label_by_index})") | |
| return model_res_label_by_index[int(index)] | |
| def start_label(self): | |
| return self.res_label_of_index(self.start_index) | |
| def end_label(self): | |
| return self.res_label_of_index(self.end_index) | |
| class Dom: | |
| def __init__(self, domain_id, segs: List[Seg] = None): | |
| self.domain_id = domain_id | |
| if segs is None: | |
| segs = [] | |
| self.segs = segs | |
| def add_seg(self, seg: Seg): | |
| self.segs.append(seg) | |
| # gather choppings into segments in domains | |
| domains_by_domain_id = {} | |
| for domain_id, chopping_by_index in zip(names, bounds): | |
| if domain_id not in domains_by_domain_id: | |
| domains_by_domain_id[domain_id] = Dom(domain_id) | |
| start_index, end_index = chopping_by_index.split('-') | |
| seg = Seg(domain_id, start_index, end_index) | |
| domains_by_domain_id[domain_id].add_seg(seg) | |
| # sort domain choppings by the start residue in first segment | |
| domains = sorted(domains_by_domain_id.values(), key=lambda dom: dom.segs[0].start_index) | |
| # collect domain choppings as strings | |
| domain_choppings = [] | |
| for dom in domains: | |
| # convert segments to strings | |
| segs_str = [f"{seg.start_label}-{seg.end_label}" for seg in dom.segs] | |
| segs_index_str = [f"{seg.start_index}-{seg.end_index}" for seg in dom.segs] | |
| LOG.info(f"Segments (index to label): {segs_index_str} -> {segs_str}") | |
| # join discontinuous segs with '_' | |
| domain_choppings.append('_'.join(segs_str)) | |
| # join domains with ',' | |
| chopping_str = ','.join(domain_choppings) | |
| num_domains = len(domain_choppings) | |
| if num_domains == 0: | |
| chopping_str = None | |
| runtime = round(time.time() - start, 3) | |
| result = PredictionResult( | |
| pdb_path=pdb_path, | |
| sequence_md5=model_structure_md5, | |
| nres=len(model_structure_seq), | |
| ndom=num_domains, | |
| chopping=chopping_str, | |
| confidence=confidence, | |
| time_sec=runtime, | |
| ) | |
| LOG.info(f"Runtime: {round(runtime, 3)}s") | |
| return result | |
| def write_csv_results(csv_writer, prediction_results: List[PredictionResult]): | |
| """ | |
| Render list of PredictionResult results to file pointer | |
| """ | |
| for res in prediction_results: | |
| row = { | |
| 'chain_id': res.chain_id, | |
| 'sequence_md5': res.sequence_md5, | |
| 'nres': res.nres, | |
| 'ndom': res.ndom, | |
| 'chopping': res.chopping if res.chopping is not None else 'NULL', | |
| 'confidence': f'{res.confidence:.3g}' if res.confidence is not None else 'NULL', | |
| 'time_sec': f'{res.time_sec}' if res.time_sec is not None else 'NULL', | |
| } | |
| csv_writer.writerow(row) | |
| def get_csv_writer(file_pointer): | |
| csv_writer = csv.DictWriter(file_pointer, | |
| fieldnames=OUTPUT_COLNAMES, | |
| delimiter='\t') | |
| return csv_writer | |
| def main(args): | |
| outer_save_dir = args.save_dir | |
| if args.use_first_chain: | |
| # use the first chain in the PDB file | |
| pdb_chain_id = None | |
| else: | |
| pdb_chain_id = 'A' | |
| input_method = get_input_method(args) | |
| model = load_model( | |
| model_dir=args.model_dir, | |
| config_dir=args.config_dir, | |
| remove_disordered_domain_threshold=args.remove_disordered_domain_threshold, | |
| min_ss_components=args.min_ss_components, | |
| min_domain_length=args.min_domain_length, | |
| post_process_domains=args.post_process_domains, | |
| ) | |
| os.makedirs(outer_save_dir, exist_ok=True) | |
| output_path = Path(args.output).absolute() | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| prediction_results_file = PredictionResultsFile( | |
| csv_path=output_path, | |
| # use args.allow_append to mean allow_skip and allow_append | |
| allow_append=args.allow_append, | |
| allow_skip=args.allow_append, | |
| ) | |
| if input_method == 'structure_directory': | |
| structure_dir = args.structure_directory | |
| for idx, fname in enumerate(os.listdir(structure_dir)): | |
| suffix = Path(fname).suffix | |
| LOG.debug(f"Checking file {fname} (suffix: {suffix}) ..") | |
| if suffix not in ACCEPTED_STRUCTURE_FILE_SUFFIXES: | |
| continue | |
| chain_id = Path(fname).stem | |
| result_exists = prediction_results_file.has_result_for_chain_id(chain_id) | |
| if result_exists: | |
| LOG.info(f"Skipping file {fname} (result for '{chain_id}' already exists)") | |
| continue | |
| pdb_path = os.path.join(structure_dir, fname) | |
| LOG.info(f"Making prediction for file {fname} (chain '{chain_id}')") | |
| result = predict(model, pdb_path, pdbchain=pdb_chain_id, renumber_pdbs=args.renumber_pdbs) | |
| prediction_results_file.add_result(result) | |
| if args.pymol_visual: | |
| generate_pymol_image( | |
| pdb_path=str(result.pdb_path), | |
| chopping=result.chopping or '', | |
| image_out_path=os.path.join(str(outer_save_dir), f'{result.pdb_path.name.replace(".pdb", "")}.png'), | |
| path_to_script=os.path.join(str(outer_save_dir), 'image_gen.pml'), | |
| pymol_executable=constants.PYMOL_EXE, | |
| ) | |
| elif input_method == 'structure_file': | |
| result = predict(model, args.structure_file, pdbchain=pdb_chain_id) | |
| prediction_results_file.add_result(result) | |
| if args.pymol_visual: | |
| generate_pymol_image( | |
| pdb_path=str(result.pdb_path), | |
| chopping=result.chopping or '', | |
| image_out_path=os.path.join(str(outer_save_dir), f'{result.pdb_path.name.replace(".pdb", "")}.png'), | |
| path_to_script=os.path.join(str(outer_save_dir), 'image_gen.pml'), | |
| pymol_executable=constants.PYMOL_EXE, | |
| ) | |
| else: | |
| raise NotImplementedError('Not implemented yet') | |
| prediction_results_file.flush() | |
| LOG.info("DONE") | |
| def parse_args(): | |
| """ | |
| Parse command line arguments | |
| """ | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--model_dir', type=str, | |
| default=str(constants.REPO_ROOT / 'weight' / 'model_v3'), | |
| help='path to the model weight directory containing weights.pt') | |
| parser.add_argument('--config_dir', type=str, default=None, | |
| help='configuration directory; defaults to conf/<model directory name>') | |
| parser.add_argument('--output', '-o', type=str, required=True, | |
| help='write results to this file') | |
| parser.add_argument('--uniprot_id', type=str, default=None, help='single uniprot id') | |
| parser.add_argument('--uniprot_id_list_file', type=str, default=None, | |
| help='path to file containing uniprot ids') | |
| parser.add_argument('--structure_directory', type=str, default=None, | |
| help='path to directory containing PDB or MMCIF files') | |
| parser.add_argument('--structure_file', type=str, default=None, | |
| help='path to PDB or MMCIF files') | |
| parser.add_argument('--append', '-a', dest='allow_append', action='store_true', default=False, | |
| help='allow results to be appended to an existing file') | |
| parser.add_argument('--pdb_id', type=str, default=None, help='single pdb id') | |
| parser.add_argument('--pdb_id_list_file', type=str, default=None, help='path to file containing uniprot ids') | |
| parser.add_argument('--save_dir', type=str, default='results', help='path where results and images will be saved') | |
| parser.add_argument('--no_post_processing', dest='post_process_domains', action='store_false') | |
| parser.add_argument('--remove_disordered_domain_threshold', type=float, default=0.35, | |
| help='if the domain is less than this proportion secondary structure, it will be removed') | |
| parser.add_argument('--min_domain_length', type=int, default=30, | |
| help='if the domain has fewer residues than this it will be removed') | |
| parser.add_argument('--min_ss_components', type=int, default=2, | |
| help='if the domain has fewer than this number of distinct secondary structure components,' | |
| 'it will be removed') | |
| parser.add_argument('--pymol_visual', dest='pymol_visual', action='store_true', | |
| help='whether to generate pymol images') | |
| parser.add_argument('--use_first_chain', default=True, action="store_true", help='use the first chain in the structure (rather than "A")') | |
| parser.add_argument('--renumber_pdbs', default=False, action="store_true", help='renumber pdb files') | |
| args = parser.parse_args() | |
| return args | |
| if __name__ == "__main__": | |
| setup_logging() | |
| main(parse_args()) | |