Chainsaw / scripts /get_predictions.py
wuxing0105's picture
Upload folder using huggingface_hub
80a72c3 verified
Raw
History Blame Contribute Delete
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)]
@property
def start_label(self):
return self.res_label_of_index(self.start_index)
@property
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())