| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import argparse |
| import logging |
| import math |
| import numpy as np |
| import os |
| import pickle |
| import random |
| import time |
| import json |
| from pathlib import Path |
| import sys |
| root_path = Path(__file__).parent.parent |
| sys.path.append(str(root_path)) |
|
|
| logging.basicConfig() |
| logger = logging.getLogger(__file__) |
| logger.setLevel(level=logging.INFO) |
|
|
| import torch |
| torch_versions = torch.__version__.split(".") |
| torch_major_version = int(torch_versions[0]) |
| torch_minor_version = int(torch_versions[1]) |
| if ( |
| torch_major_version > 1 or |
| (torch_major_version == 1 and torch_minor_version >= 12) |
| ): |
| |
| torch.set_float32_matmul_precision("high") |
|
|
| torch.set_grad_enabled(False) |
|
|
| from onescience.configs.bio.openfold.config import model_config |
| from onescience.datapipes.openfold import templates, feature_pipeline, data_pipeline |
| from onescience.datapipes.openfold.tools import hhsearch, hmmsearch |
| from onescience.utils.openfold.np import protein |
| from onescience.utils.openfold.script_utils import (load_models_from_command_line, parse_fasta, run_model, |
| prep_output, relax_protein) |
| from onescience.utils.openfold.tensor_utils import tensor_tree_map |
| from onescience.utils.openfold.trace_utils import ( |
| pad_feature_dict_seq, |
| trace_model_, |
| ) |
|
|
| |
| |
| from precompute_embeddings import EmbeddingGenerator |
| from utils import add_data_args |
|
|
|
|
| TRACING_INTERVAL = 50 |
|
|
|
|
| def precompute_alignments(tags, seqs, alignment_dir, args): |
| for tag, seq in zip(tags, seqs): |
| tmp_fasta_path = os.path.join(args.output_dir, f"tmp_{os.getpid()}.fasta") |
| with open(tmp_fasta_path, "w") as fp: |
| fp.write(f">{tag}\n{seq}") |
|
|
| local_alignment_dir = os.path.join(alignment_dir, tag) |
|
|
| if args.use_precomputed_alignments is None: |
| logger.info(f"Generating alignments for {tag}...") |
|
|
| os.makedirs(local_alignment_dir, exist_ok=True) |
|
|
| if "multimer" in args.config_preset: |
| template_searcher = hmmsearch.Hmmsearch( |
| binary_path=args.hmmsearch_binary_path, |
| hmmbuild_binary_path=args.hmmbuild_binary_path, |
| database_path=args.pdb_seqres_database_path, |
| ) |
| else: |
| template_searcher = hhsearch.HHSearch( |
| binary_path=args.hhsearch_binary_path, |
| databases=[args.pdb70_database_path], |
| ) |
|
|
| |
| if args.use_single_seq_mode: |
| alignment_runner = data_pipeline.AlignmentRunner( |
| jackhmmer_binary_path=args.jackhmmer_binary_path, |
| uniref90_database_path=args.uniref90_database_path, |
| template_searcher=template_searcher, |
| no_cpus=args.cpus, |
| ) |
| embedding_generator = EmbeddingGenerator() |
| embedding_generator.run(tmp_fasta_path, alignment_dir) |
| else: |
| alignment_runner = data_pipeline.AlignmentRunner( |
| jackhmmer_binary_path=args.jackhmmer_binary_path, |
| hhblits_binary_path=args.hhblits_binary_path, |
| uniref90_database_path=args.uniref90_database_path, |
| mgnify_database_path=args.mgnify_database_path, |
| bfd_database_path=args.bfd_database_path, |
| uniref30_database_path=args.uniref30_database_path, |
| uniclust30_database_path=args.uniclust30_database_path, |
| uniprot_database_path=args.uniprot_database_path, |
| template_searcher=template_searcher, |
| use_small_bfd=args.bfd_database_path is None, |
| no_cpus=args.cpus |
| ) |
|
|
| alignment_runner.run( |
| tmp_fasta_path, local_alignment_dir |
| ) |
| else: |
| logger.info( |
| f"Using precomputed alignments for {tag} at {alignment_dir}..." |
| ) |
|
|
| |
| os.remove(tmp_fasta_path) |
|
|
|
|
| def round_up_seqlen(seqlen): |
| return int(math.ceil(seqlen / TRACING_INTERVAL)) * TRACING_INTERVAL |
|
|
|
|
| def generate_feature_dict( |
| tags, |
| seqs, |
| alignment_dir, |
| data_processor, |
| args, |
| ): |
| tmp_fasta_path = os.path.join(args.output_dir, f"tmp_{os.getpid()}.fasta") |
|
|
| if "multimer" in args.config_preset: |
| with open(tmp_fasta_path, "w") as fp: |
| fp.write( |
| '\n'.join([f">{tag}\n{seq}" for tag, seq in zip(tags, seqs)]) |
| ) |
| feature_dict = data_processor.process_fasta( |
| fasta_path=tmp_fasta_path, alignment_dir=alignment_dir, |
| ) |
| elif len(seqs) == 1: |
| tag = tags[0] |
| seq = seqs[0] |
| with open(tmp_fasta_path, "w") as fp: |
| fp.write(f">{tag}\n{seq}") |
|
|
| local_alignment_dir = os.path.join(alignment_dir, tag) |
| feature_dict = data_processor.process_fasta( |
| fasta_path=tmp_fasta_path, |
| alignment_dir=local_alignment_dir, |
| seqemb_mode=args.use_single_seq_mode, |
| ) |
| else: |
| with open(tmp_fasta_path, "w") as fp: |
| fp.write( |
| '\n'.join([f">{tag}\n{seq}" for tag, seq in zip(tags, seqs)]) |
| ) |
| feature_dict = data_processor.process_multiseq_fasta( |
| fasta_path=tmp_fasta_path, super_alignment_dir=alignment_dir, |
| ) |
|
|
| |
| os.remove(tmp_fasta_path) |
|
|
| return feature_dict |
|
|
|
|
| def list_files_with_extensions(dir, extensions): |
| return [f for f in os.listdir(dir) if f.endswith(extensions)] |
|
|
|
|
| def main(args): |
| |
| os.makedirs(args.output_dir, exist_ok=True) |
|
|
| if args.config_preset.startswith("seq"): |
| args.use_single_seq_mode = True |
|
|
| config = model_config( |
| args.config_preset, |
| long_sequence_inference=args.long_sequence_inference, |
| use_deepspeed_evoformer_attention=args.use_deepspeed_evoformer_attention, |
| ) |
|
|
| if args.experiment_config_json: |
| with open(args.experiment_config_json, 'r') as f: |
| custom_config_dict = json.load(f) |
| config.update_from_flattened_dict(custom_config_dict) |
|
|
| if args.trace_model: |
| if not config.data.predict.fixed_size: |
| raise ValueError( |
| "Tracing requires that fixed_size mode be enabled in the config" |
| ) |
|
|
| is_multimer = "multimer" in args.config_preset |
| is_custom_template = "use_custom_template" in args and args.use_custom_template |
| if is_custom_template: |
| template_featurizer = templates.CustomHitFeaturizer( |
| mmcif_dir=args.template_mmcif_dir, |
| max_template_date="9999-12-31", |
| max_hits=-1, |
| kalign_binary_path=args.kalign_binary_path |
| ) |
| elif is_multimer: |
| template_featurizer = templates.HmmsearchHitFeaturizer( |
| mmcif_dir=args.template_mmcif_dir, |
| max_template_date=args.max_template_date, |
| max_hits=config.data.predict.max_templates, |
| kalign_binary_path=args.kalign_binary_path, |
| release_dates_path=args.release_dates_path, |
| obsolete_pdbs_path=args.obsolete_pdbs_path |
| ) |
| else: |
| template_featurizer = templates.HhsearchHitFeaturizer( |
| mmcif_dir=args.template_mmcif_dir, |
| max_template_date=args.max_template_date, |
| max_hits=config.data.predict.max_templates, |
| kalign_binary_path=args.kalign_binary_path, |
| release_dates_path=args.release_dates_path, |
| obsolete_pdbs_path=args.obsolete_pdbs_path |
| ) |
| data_processor = data_pipeline.DataPipeline( |
| template_featurizer=template_featurizer, |
| ) |
| if is_multimer: |
| data_processor = data_pipeline.DataPipelineMultimer( |
| monomer_data_pipeline=data_processor, |
| ) |
|
|
| output_dir_base = args.output_dir |
| random_seed = args.data_random_seed |
| if random_seed is None: |
| random_seed = random.randrange(2 ** 32) |
|
|
| np.random.seed(random_seed) |
| torch.manual_seed(random_seed + 1) |
| feature_processor = feature_pipeline.FeaturePipeline(config.data) |
| if not os.path.exists(output_dir_base): |
| os.makedirs(output_dir_base) |
| if args.use_precomputed_alignments is None: |
| alignment_dir = os.path.join(output_dir_base, "alignments") |
| else: |
| alignment_dir = args.use_precomputed_alignments |
|
|
| tag_list = [] |
| seq_list = [] |
| for fasta_file in list_files_with_extensions(args.fasta_dir, (".fasta", ".fa")): |
| |
| fasta_path = os.path.join(args.fasta_dir, fasta_file) |
| with open(fasta_path, "r") as fp: |
| data = fp.read() |
|
|
| tags, seqs = parse_fasta(data) |
|
|
| if not is_multimer and len(tags) != 1: |
| print( |
| f"{fasta_path} contains more than one sequence but " |
| f"multimer mode is not enabled. Skipping..." |
| ) |
| continue |
|
|
| |
| tag = '-'.join(tags) |
|
|
| tag_list.append((tag, tags)) |
| seq_list.append(seqs) |
|
|
| seq_sort_fn = lambda target: sum([len(s) for s in target[1]]) |
| sorted_targets = sorted(zip(tag_list, seq_list), key=seq_sort_fn) |
| feature_dicts = {} |
|
|
| if is_multimer and args.openfold_checkpoint_path: |
| raise ValueError( |
| '`openfold_checkpoint_path` was specified, but no OpenFold checkpoints are available for multimer mode') |
|
|
| model_generator = load_models_from_command_line( |
| config, |
| args.model_device, |
| args.openfold_checkpoint_path, |
| args.jax_param_path, |
| args.output_dir) |
|
|
| for model, output_directory in model_generator: |
| cur_tracing_interval = 0 |
| for (tag, tags), seqs in sorted_targets: |
| output_name = f'{tag}_{args.config_preset}' |
| if args.output_postfix is not None: |
| output_name = f'{output_name}_{args.output_postfix}' |
|
|
| |
| precompute_alignments(tags, seqs, alignment_dir, args) |
|
|
| feature_dict = feature_dicts.get(tag, None) |
| if feature_dict is None: |
| feature_dict = generate_feature_dict( |
| tags, |
| seqs, |
| alignment_dir, |
| data_processor, |
| args, |
| ) |
|
|
| if args.trace_model: |
| n = feature_dict["aatype"].shape[-2] |
| rounded_seqlen = round_up_seqlen(n) |
| feature_dict = pad_feature_dict_seq( |
| feature_dict, rounded_seqlen, |
| ) |
|
|
| feature_dicts[tag] = feature_dict |
| processed_feature_dict = feature_processor.process_features( |
| feature_dict, mode='predict', is_multimer=is_multimer |
| ) |
|
|
| processed_feature_dict = { |
| k: torch.as_tensor(v, device=args.model_device) |
| for k, v in processed_feature_dict.items() |
| } |
|
|
| if args.trace_model: |
| if rounded_seqlen > cur_tracing_interval: |
| logger.info( |
| f"Tracing model at {rounded_seqlen} residues..." |
| ) |
| t = time.perf_counter() |
| trace_model_(model, processed_feature_dict) |
| tracing_time = time.perf_counter() - t |
| logger.info( |
| f"Tracing time: {tracing_time}" |
| ) |
| cur_tracing_interval = rounded_seqlen |
|
|
| out = run_model(model, processed_feature_dict, tag, args.output_dir) |
|
|
| |
| processed_feature_dict = tensor_tree_map( |
| lambda x: np.array(x[..., -1].cpu()), |
| processed_feature_dict |
| ) |
| out = tensor_tree_map(lambda x: np.array(x.cpu()), out) |
|
|
| unrelaxed_protein = prep_output( |
| out, |
| processed_feature_dict, |
| feature_dict, |
| feature_processor, |
| args.config_preset, |
| args.multimer_ri_gap, |
| args.subtract_plddt |
| ) |
|
|
| unrelaxed_file_suffix = "_unrelaxed.pdb" |
| if args.cif_output: |
| unrelaxed_file_suffix = "_unrelaxed.cif" |
| unrelaxed_output_path = os.path.join( |
| output_directory, f'{output_name}{unrelaxed_file_suffix}' |
| ) |
|
|
| with open(unrelaxed_output_path, 'w') as fp: |
| if args.cif_output: |
| fp.write(protein.to_modelcif(unrelaxed_protein)) |
| else: |
| fp.write(protein.to_pdb(unrelaxed_protein)) |
|
|
| logger.info(f"Output written to {unrelaxed_output_path}...") |
|
|
| if not args.skip_relaxation: |
| |
| logger.info(f"Running relaxation on {unrelaxed_output_path}...") |
| relax_protein(config, args.model_device, unrelaxed_protein, output_directory, output_name, |
| args.cif_output) |
|
|
| if args.save_outputs: |
| output_dict_path = os.path.join( |
| output_directory, f'{output_name}_output_dict.pkl' |
| ) |
| with open(output_dict_path, "wb") as fp: |
| pickle.dump(out, fp, protocol=pickle.HIGHEST_PROTOCOL) |
|
|
| logger.info(f"Model output written to {output_dict_path}...") |
|
|
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "fasta_dir", type=str, |
| help="Path to directory containing FASTA files, one sequence per file" |
| ) |
| parser.add_argument( |
| "template_mmcif_dir", type=str, |
| ) |
| parser.add_argument( |
| "--use_precomputed_alignments", type=str, default=None, |
| help="""Path to alignment directory. If provided, alignment computation |
| is skipped and database path arguments are ignored.""" |
| ) |
| parser.add_argument( |
| "--use_custom_template", action="store_true", default=False, |
| help="""Use mmcif given with "template_mmcif_dir" argument as template input.""" |
| ) |
| parser.add_argument( |
| "--use_single_seq_mode", action="store_true", default=False, |
| help="""Use single sequence embeddings instead of MSAs.""" |
| ) |
| parser.add_argument( |
| "--output_dir", type=str, default=os.getcwd(), |
| help="""Name of the directory in which to output the prediction""", |
| ) |
| parser.add_argument( |
| "--model_device", type=str, default="cpu", |
| help="""Name of the device on which to run the model. Any valid torch |
| device name is accepted (e.g. "cpu", "cuda:0")""" |
| ) |
| parser.add_argument( |
| "--config_preset", type=str, default="model_1", |
| help="""Name of a model config preset defined in openfold/config.py""" |
| ) |
| parser.add_argument( |
| "--jax_param_path", type=str, default=None, |
| help="""Path to JAX model parameters. If None, and openfold_checkpoint_path |
| is also None, parameters are selected automatically according to |
| the model name from openfold/resources/params""" |
| ) |
| parser.add_argument( |
| "--openfold_checkpoint_path", type=str, default=None, |
| help="""Path to OpenFold checkpoint. Can be either a DeepSpeed |
| checkpoint directory or a .pt file""" |
| ) |
| parser.add_argument( |
| "--save_outputs", action="store_true", default=False, |
| help="Whether to save all model outputs, including embeddings, etc." |
| ) |
| parser.add_argument( |
| "--cpus", type=int, default=4, |
| help="""Number of CPUs with which to run alignment tools""" |
| ) |
| parser.add_argument( |
| "--preset", type=str, default='full_dbs', |
| choices=('reduced_dbs', 'full_dbs') |
| ) |
| parser.add_argument( |
| "--output_postfix", type=str, default=None, |
| help="""Postfix for output prediction filenames""" |
| ) |
| parser.add_argument( |
| "--data_random_seed", type=int, default=None |
| ) |
| parser.add_argument( |
| "--skip_relaxation", action="store_true", default=False, |
| ) |
| parser.add_argument( |
| "--multimer_ri_gap", type=int, default=200, |
| help="""Residue index offset between multiple sequences, if provided""" |
| ) |
| parser.add_argument( |
| "--trace_model", action="store_true", default=False, |
| help="""Whether to convert parts of each model to TorchScript. |
| Significantly improves runtime at the cost of lengthy |
| 'compilation.' Useful for large batch jobs.""" |
| ) |
| parser.add_argument( |
| "--subtract_plddt", action="store_true", default=False, |
| help=""""Whether to output (100 - pLDDT) in the B-factor column instead |
| of the pLDDT itself""" |
| ) |
| parser.add_argument( |
| "--long_sequence_inference", action="store_true", default=False, |
| help="""enable options to reduce memory usage at the cost of speed, helps longer sequences fit into GPU memory, see the README for details""" |
| ) |
| parser.add_argument( |
| "--cif_output", action="store_true", default=False, |
| help="Output predicted models in ModelCIF format instead of PDB format (default)" |
| ) |
| parser.add_argument( |
| "--experiment_config_json", default="", help="Path to a json file with custom config values to overwrite config setting", |
| ) |
| parser.add_argument( |
| "--use_deepspeed_evoformer_attention", action="store_true", default=False, |
| help="Whether to use the DeepSpeed evoformer attention layer. Must have deepspeed installed in the environment.", |
| ) |
| add_data_args(parser) |
| args = parser.parse_args() |
|
|
| if args.jax_param_path is None and args.openfold_checkpoint_path is None: |
| args.jax_param_path = os.path.join( |
| "openfold", "resources", "params", |
| "params_" + args.config_preset + ".npz" |
| ) |
|
|
| if args.model_device == "cpu" and torch.cuda.is_available(): |
| logging.warning( |
| """The model is being run on CPU. Consider specifying |
| --model_device for better performance""" |
| ) |
| main(args) |
|
|