| from typing import Any |
| import torch |
| import time |
| import os |
| import subprocess |
| import shutil |
| import random |
| import wandb |
| import numpy as np |
| import pandas as pd |
| import logging |
| from pytorch_lightning import LightningModule |
| from Bio import PDB |
|
|
| |
| from biotite.sequence.io import fasta |
|
|
| from utils.experiments import write_prot_to_pdb, save_traj, create_full_prot |
| from utils import metrics |
| from models.proteinflow import ProteinFlow |
| from models.classifier import ProtClassifier |
| from models.classifier_wrapper import ClasfModule |
| from utils import all_atom |
| from utils import so3Utils as su |
| from utils import residue_constants as rc |
| from utils import pdbUtils as du |
| from utils.flows import Interpolant |
|
|
| from utils.modelUtils import t_stratified_loss, to_numpy |
| from pytorch_lightning.loggers.wandb import WandbLogger |
|
|
| from dataset import protein |
|
|
|
|
| class ProteinFlowModulev2(LightningModule): |
|
|
| def __init__(self, cfg, classifier_cfg=None): |
| super().__init__() |
| self._print_logger = logging.getLogger(__name__) |
| self._exp_cfg = cfg.experiment |
| self._model_cfg = cfg.model |
| self._data_cfg = cfg.data |
| self._interpolant_cfg = cfg.interpolant |
| |
|
|
| |
| self.model = ProteinFlow(cfg.model) |
|
|
| |
| self.interpolant = Interpolant(cfg.interpolant) |
| |
| |
| self.loaded_classifier = False |
| |
|
|
| self._sample_write_dir = self._exp_cfg.checkpointer.dirpath |
| os.makedirs(self._sample_write_dir, exist_ok=True) |
|
|
| self.validation_epoch_metrics = [] |
| self.validation_epoch_samples = [] |
| self.save_hyperparameters() |
| print(f"Model is initiated on GPU: {torch.cuda.current_device()}") |
|
|
| def on_train_start(self): |
| self._epoch_start_time = time.time() |
|
|
| def on_train_epoch_end(self): |
| epoch_time = (time.time() - self._epoch_start_time) / 60.0 |
| self.log( |
| 'train/epoch_time_minutes', |
| epoch_time, |
| on_step=False, |
| on_epoch=True, |
| prog_bar=False |
| ) |
| self._epoch_start_time = time.time() |
|
|
| def model_step(self, noisy_batch: Any): |
| training_cfg = self._exp_cfg.training |
| loss_mask = noisy_batch['res_mask'] |
| if training_cfg.min_plddt_mask is not None: |
| plddt_mask = noisy_batch['res_plddt'] > training_cfg.min_plddt_mask |
| loss_mask *= plddt_mask |
| num_batch, num_res = loss_mask.shape |
|
|
| |
| gt_trans_1 = noisy_batch['trans_1'] |
| gt_rotmats_1 = noisy_batch['rotmats_1'] |
| rotmats_t = noisy_batch['rotmats_t'] |
| gt_rot_vf = su.calc_rot_vf( |
| rotmats_t, gt_rotmats_1.type(torch.float32)) |
| gt_bb_atoms = all_atom.to_atom37(gt_trans_1, gt_rotmats_1)[:, :, :3] |
|
|
| |
| t = noisy_batch['t'] |
| norm_scale = 1 - torch.min( |
| t[..., None], torch.tensor(training_cfg.t_normalize_clip)) |
| |
| |
| model_output = self.model(noisy_batch) |
| pred_trans_1 = model_output['pred_trans'] |
| pred_rotmats_1 = model_output['pred_rotmats'] |
| pred_rots_vf = su.calc_rot_vf(rotmats_t, pred_rotmats_1) |
|
|
| |
| pred_bb_atoms = all_atom.to_atom37(pred_trans_1, pred_rotmats_1)[:, :, :3] |
| gt_bb_atoms *= training_cfg.bb_atom_scale / norm_scale[..., None] |
| pred_bb_atoms *= training_cfg.bb_atom_scale / norm_scale[..., None] |
| loss_denom = torch.sum(loss_mask, dim=-1) * 3 |
| bb_atom_loss = torch.sum( |
| (gt_bb_atoms - pred_bb_atoms) ** 2 * loss_mask[..., None, None], |
| dim=(-1, -2, -3) |
| ) / loss_denom |
|
|
| |
| trans_error = (gt_trans_1 - pred_trans_1) / norm_scale * training_cfg.trans_scale |
| trans_loss = training_cfg.translation_loss_weight * torch.sum( |
| trans_error ** 2 * loss_mask[..., None], |
| dim=(-1, -2) |
| ) / loss_denom |
|
|
| |
| rots_vf_error = (gt_rot_vf - pred_rots_vf) / norm_scale |
| rots_vf_loss = training_cfg.rotation_loss_weights * torch.sum( |
| rots_vf_error ** 2 * loss_mask[..., None], |
| dim=(-1, -2) |
| ) / loss_denom |
|
|
| |
| gt_flat_atoms = gt_bb_atoms.reshape([num_batch, num_res * 3, 3]) |
| gt_pair_dists = torch.linalg.norm( |
| gt_flat_atoms[:, :, None, :] - gt_flat_atoms[:, None, :, :], dim=-1) |
| pred_flat_atoms = pred_bb_atoms.reshape([num_batch, num_res * 3, 3]) |
| pred_pair_dists = torch.linalg.norm( |
| pred_flat_atoms[:, :, None, :] - pred_flat_atoms[:, None, :, :], dim=-1) |
|
|
| flat_loss_mask = torch.tile(loss_mask[:, :, None], (1, 1, 3)) |
| flat_loss_mask = flat_loss_mask.reshape([num_batch, num_res * 3]) |
| flat_res_mask = torch.tile(loss_mask[:, :, None], (1, 1, 3)) |
| flat_res_mask = flat_res_mask.reshape([num_batch, num_res * 3]) |
|
|
| gt_pair_dists = gt_pair_dists * flat_loss_mask[..., None] |
| pred_pair_dists = pred_pair_dists * flat_loss_mask[..., None] |
| pair_dist_mask = flat_loss_mask[..., None] * flat_res_mask[:, None, :] |
|
|
| dist_mat_loss = torch.sum( |
| (gt_pair_dists - pred_pair_dists) ** 2 * pair_dist_mask, |
| dim=(1, 2)) |
| dist_mat_loss /= (torch.sum(pair_dist_mask, dim=(1, 2)) - num_res) |
|
|
| se3_vf_loss = trans_loss + rots_vf_loss |
| auxiliary_loss = (bb_atom_loss + dist_mat_loss) * ( |
| t[:, 0] > training_cfg.aux_loss_t_pass |
| ) |
| auxiliary_loss *= self._exp_cfg.training.aux_loss_weight |
| se3_vf_loss += auxiliary_loss |
| if torch.isnan(se3_vf_loss).any(): |
| raise ValueError('NaN loss encountered') |
| return { |
| "bb_atom_loss": bb_atom_loss, |
| "trans_loss": trans_loss, |
| "dist_mat_loss": dist_mat_loss, |
| "auxiliary_loss": auxiliary_loss, |
| "rots_vf_loss": rots_vf_loss, |
| "se3_vf_loss": se3_vf_loss |
| } |
|
|
| def validation_step(self, batch: Any, batch_idx: int): |
| res_mask = batch['res_mask'] |
| self.interpolant.set_device(res_mask.device) |
| num_batch, num_res = res_mask.shape |
|
|
| samples = self.interpolant.sample( |
| num_batch, |
| num_res, |
| self.model, |
| )[0][-1].numpy() |
|
|
| batch_metrics = [] |
| for i in range(num_batch): |
|
|
| |
| final_pos = samples[i] |
| saved_path = write_prot_to_pdb( |
| final_pos, |
| os.path.join( |
| self._sample_write_dir, |
| f'sample_{i}_idx_{batch_idx}_len_{num_res}.pdb'), |
| no_indexing=True |
| ) |
| if isinstance(self.logger, WandbLogger): |
| self.validation_epoch_samples.append( |
| [saved_path, self.global_step, wandb.Molecule(saved_path)] |
| ) |
|
|
| mdtraj_metrics = metrics.calc_mdtraj_metrics(saved_path) |
| ca_idx = rc.atom_order['CA'] |
| ca_ca_metrics = metrics.calc_ca_ca_metrics(final_pos[:, ca_idx]) |
| batch_metrics.append((mdtraj_metrics | ca_ca_metrics)) |
|
|
| batch_metrics = pd.DataFrame(batch_metrics) |
| self.validation_epoch_metrics.append(batch_metrics) |
|
|
| def on_validation_epoch_end(self): |
| if len(self.validation_epoch_samples) > 0: |
| self.logger.log_table( |
| key='valid/samples', |
| columns=["sample_path", "global_step", "Protein"], |
| data=self.validation_epoch_samples) |
| self.validation_epoch_samples.clear() |
| val_epoch_metrics = pd.concat(self.validation_epoch_metrics) |
| for metric_name, metric_val in val_epoch_metrics.mean().to_dict().items(): |
| self._log_scalar( |
| f'valid/{metric_name}', |
| metric_val, |
| on_step=False, |
| on_epoch=True, |
| prog_bar=False, |
| batch_size=len(val_epoch_metrics), |
| ) |
| self.validation_epoch_metrics.clear() |
|
|
| def _log_scalar( |
| self, |
| key, |
| value, |
| on_step=True, |
| on_epoch=False, |
| prog_bar=True, |
| batch_size=None, |
| sync_dist=False, |
| rank_zero_only=True |
| ): |
| if sync_dist and rank_zero_only: |
| raise ValueError('Unable to sync dist when rank_zero_only=True') |
| self.log( |
| key, |
| value, |
| on_step=on_step, |
| on_epoch=on_epoch, |
| prog_bar=prog_bar, |
| batch_size=batch_size, |
| sync_dist=sync_dist, |
| rank_zero_only=rank_zero_only |
| ) |
|
|
| def training_step(self, batch: Any, stage: int): |
| self.stage = 'train' |
| step_start_time = time.time() |
| self.interpolant.set_device(batch['res_mask'].device) |
| noisy_batch = self.interpolant.corrupt_batch(batch) |
| if self._interpolant_cfg.self_condition and random.random() > 0.5: |
| with torch.no_grad(): |
| model_sc = self.model(noisy_batch) |
| noisy_batch['trans_sc'] = model_sc['pred_trans'] |
| batch_losses = self.model_step(noisy_batch) |
| num_batch = batch_losses['bb_atom_loss'].shape[0] |
| total_losses = { |
| k: torch.mean(v) for k, v in batch_losses.items() |
| } |
| for k, v in total_losses.items(): |
| self._log_scalar( |
| f"train/{k}", v, prog_bar=False, batch_size=num_batch) |
|
|
| |
| t = torch.squeeze(noisy_batch['t']) |
| self._log_scalar( |
| "train/t", |
| np.mean(to_numpy(t)), |
| prog_bar=False, batch_size=num_batch) |
| for loss_name, loss_dict in batch_losses.items(): |
| stratified_losses = t_stratified_loss( |
| t, loss_dict, loss_name=loss_name) |
| for k, v in stratified_losses.items(): |
| self._log_scalar( |
| f"train/{k}", v, prog_bar=False, batch_size=num_batch) |
|
|
| |
| self._log_scalar( |
| "train/length", batch['res_mask'].shape[1], prog_bar=False, batch_size=num_batch) |
| self._log_scalar( |
| "train/batch_size", num_batch, prog_bar=False) |
| step_time = time.time() - step_start_time |
| self._log_scalar( |
| "train/examples_per_second", num_batch / step_time) |
| train_loss = ( |
| total_losses[self._exp_cfg.training.loss] |
| + total_losses['auxiliary_loss'] |
| ) |
| self._log_scalar( |
| "train/loss", train_loss, batch_size=num_batch) |
| return train_loss |
|
|
| def configure_optimizers(self): |
| return torch.optim.AdamW( |
| params=self.model.parameters(), |
| **self._exp_cfg.optimizer |
| ) |
| |
| def load_classifiers(self, cfg, requires_grad = True): |
| self._classf_cfg = cfg |
| self.cls_model = ClasfModule.load_from_checkpoint( |
| checkpoint_path=self._classf_cfg.ckpt_path, |
| map_location=f'cuda:{torch.cuda.current_device()}' |
| ) |
| |
| self._pmpnn_dir = self._infer_cfg.pmpnn_dir |
| |
| |
| |
| |
| for param in self.cls_model.parameters(): |
| param.requires_grad = requires_grad |
| |
| def load_folding_model(self): |
| print(f"Current GPU of folding model is {torch.cuda.current_device()}") |
| from transformers import AutoTokenizer,EsmForProteinFolding |
| self._tokenizer = AutoTokenizer.from_pretrained("facebook/esmfold_v1") |
| self._folding_model = EsmForProteinFolding.from_pretrained("facebook/esmfold_v1", low_cpu_mem_usage=True) |
| self._folding_model = self._folding_model.to(f'cuda:{torch.cuda.current_device()}') |
| self._folding_model.esm = self._folding_model.esm.half() |
| |
| """ |
| self._folding_model = esm.pretrained.esmfold_v1() |
| self._folding_model = self._folding_model.eval() |
| self._folding_model = self._folding_model.to(f'cuda:{torch.cuda.current_device()}') |
| """ |
|
|
| def run_self_consistency( |
| self, |
| decoy_pdb_dir: str, |
| reference_pdb_path: str, |
| motif_mask = None, |
| run_folding=True, |
| ): |
| device = f'cuda:{torch.cuda.current_device()}' |
| |
| output_path = os.path.join(decoy_pdb_dir, "parsed_pdbs.jsonl") |
| process = subprocess.Popen( |
| [ |
| "python", |
| f"{self._pmpnn_dir}/helper_scripts/parse_multiple_chains.py", |
| f"--input_path={decoy_pdb_dir}", |
| f"--output_path={output_path}", |
| ] |
| ) |
| _ = process.wait() |
| num_tries = 0 |
| ret = -1 |
| pmpnn_args = [ |
| "python", |
| f"{self._pmpnn_dir}/protein_mpnn_run.py", |
| "--out_folder", |
| decoy_pdb_dir, |
| "--jsonl_path", |
| output_path, |
| "--num_seq_per_target", |
| str(self._samples_cfg.seq_per_sample), |
| "--sampling_temp", |
| "0.1", |
| "--seed", |
| str(self._infer_cfg.seed), |
| "--batch_size", |
| "1", |
| ] |
| pmpnn_args.append("--device") |
| pmpnn_args.append(str(torch.cuda.current_device())) |
| while ret < 0: |
| try: |
| process = subprocess.Popen( |
| pmpnn_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT |
| ) |
| ret = process.wait() |
| except Exception as e: |
| num_tries += 1 |
| self._log.info(f"Failed ProteinMPNN. Attempt {num_tries}/5 {e}") |
| torch.cuda.empty_cache() |
| if num_tries < 4: |
| raise e |
| |
| mpnn_fasta_path = os.path.join( |
| decoy_pdb_dir, |
| "seqs", |
| os.path.basename(reference_pdb_path).replace(".pdb", ".fa") |
| ) |
| if not run_folding: |
| return mpnn_fasta_path |
| |
| mpnn_results = { |
| "tm_score": [], |
| "sample_path": [], |
| "header": [], |
| "sequence": [], |
| "rmsd": [], |
| } |
| if motif_mask is not None: |
| mpnn_results["motif_rmsd"] = [] |
| esmf_dir = os.path.join(decoy_pdb_dir, "esmf") |
| os.makedirs(esmf_dir, exist_ok=True) |
| fasta_seqs = fasta.FastaFile.read(mpnn_fasta_path) |
| sample_feats = du.parse_pdb_feats("sample", reference_pdb_path) |
| for i, (header, string) in enumerate(fasta_seqs.items()): |
| |
| esmf_sample_path = os.path.join(esmf_dir, f"sample_{i}.pdb") |
| _ = self.run_folding(string, esmf_sample_path) |
| esmf_feats = du.parse_pdb_feats("folded_sample", esmf_sample_path) |
| sample_seq = du.aatype_to_seq(sample_feats["aatype"]) |
| |
| _, tm_score = metrics.calc_tm_score( |
| sample_feats["bb_positions"], |
| esmf_feats["bb_positions"], |
| sample_seq, |
| sample_seq, |
| ) |
| rmsd = metrics.calc_aligned_rmsd( |
| sample_feats["bb_positions"], esmf_feats["bb_positions"] |
| ) |
| if motif_mask is not None: |
| sample_motif = sample_feats["bb_positions"][motif_mask] |
| of_motif = esmf_feats["bb_positions"][motif_mask] |
| motif_rmsd = metrics.calc_aligned_rmsd(sample_motif, of_motif) |
| mpnn_results["motif_rmsd"].append(motif_rmsd) |
| mpnn_results["rmsd"].append(rmsd) |
| mpnn_results["tm_score"].append(tm_score) |
| mpnn_results["sample_path"].append(esmf_sample_path) |
| mpnn_results["header"].append(header) |
| mpnn_results["sequence"].append(string) |
| |
| csv_path = os.path.join(decoy_pdb_dir, "sc_results.csv") |
| mpnn_results = pd.DataFrame(mpnn_results) |
| mpnn_results.to_csv(csv_path) |
| |
| |
| def run_folding(self, sequence, save_path): |
| def convert_outputs_to_pdb(outputs): |
| from transformers.models.esm.openfold_utils.protein import to_pdb, Protein as OFProtein |
| from transformers.models.esm.openfold_utils.feats import atom14_to_atom37 |
| |
| final_atom_positions = atom14_to_atom37(outputs["positions"][-1], outputs) |
| outputs = {k: v.to("cpu").numpy() for k, v in outputs.items()} |
| final_atom_positions = final_atom_positions.cpu().numpy() |
| final_atom_mask = outputs["atom37_atom_exists"] |
| pdbs = [] |
| for i in range(outputs["aatype"].shape[0]): |
| aa = outputs["aatype"][i] |
| pred_pos = final_atom_positions[i] |
| mask = final_atom_mask[i] |
| resid = outputs["residue_index"][i] + 1 |
| pred = OFProtein( |
| aatype=aa, |
| atom_positions=pred_pos, |
| atom_mask=mask, |
| residue_index=resid, |
| b_factors=outputs["plddt"][i], |
| chain_index=outputs["chain_index"][i] if "chain_index" in outputs else None, |
| ) |
| pdbs.append(to_pdb(pred)) |
| return pdbs |
| |
| |
| tokenized_input = self._tokenizer([sequence], return_tensors="pt", add_special_tokens=False)['input_ids'] |
| tokenized_input = tokenized_input.to(f'cuda:{torch.cuda.current_device()}') |
| with torch.no_grad(): |
| output = self._folding_model(tokenized_input) |
| """ |
| with torch.no_grad(): |
| # print(sequence) |
| output = self._folding_model.infer_pdb(sequence) |
| """ |
| output = convert_outputs_to_pdb(output) |
| with open(save_path, "w") as f: |
| f.write("".join(output)) |
| return output |
| |
| def predict_step(self, batch, batch_idx): |
| device = f'cuda:{torch.cuda.current_device()}' |
| interpolant = Interpolant(self._infer_cfg.interpolant) |
| interpolant.set_device(device) |
|
|
| sample_length = batch['num_res'].item() |
| diffuse_mask = torch.ones(1, sample_length) |
| sample_id = batch['sample_id'].item() |
| sample_dir = os.path.join( |
| self._output_dir, f'length_{sample_length}', f'sample_{sample_id}') |
| top_sample_csv_path = os.path.join(sample_dir, 'top_sample.csv') |
| if os.path.exists(top_sample_csv_path): |
| self._print_logger.info( |
| f'Skipping instance {sample_id} length {sample_length}') |
| return |
|
|
| atom37_traj, model_traj, _ = interpolant.sample_clf( |
| 1, sample_length, self.model, self.cls_model |
| ) |
|
|
| os.makedirs(sample_dir, exist_ok=True) |
| bb_traj = to_numpy(torch.concat(atom37_traj, dim=0)) |
| traj_paths = save_traj( |
| bb_traj[-1], |
| bb_traj, |
| np.flip(to_numpy(torch.concat(model_traj, dim=0)), axis=0), |
| to_numpy(diffuse_mask)[0], |
| output_dir=sample_dir, |
| ) |
| |
| |
| pdb_path = traj_paths["sample_path"] |
| sc_output_dir = os.path.join(sample_dir, "self_consistency") |
| os.makedirs(sc_output_dir, exist_ok=True) |
| shutil.copy( |
| pdb_path, os.path.join(sc_output_dir, os.path.basename(pdb_path)) |
| ) |
| |
| |
| _ = self.run_self_consistency(sc_output_dir, pdb_path, motif_mask=None) |
| |
| def evaluate_structure_quality(self, pdb_path, reference_pdb_path=None, fixed_residues=None): |
| """Evaluate the quality of a generated protein structure. |
| |
| Args: |
| pdb_path: Path to the PDB file to evaluate |
| reference_pdb_path: Optional path to a reference structure for comparison |
| fixed_residues: List of residue indices that were fixed (PDB numbering) |
| |
| Returns: |
| Dictionary containing various quality metrics: |
| - Basic geometric validation (bond lengths, angles) |
| - Secondary structure content |
| - Ramachandran plot statistics |
| - RMSD and TM-score to reference if provided |
| - RMSD of fixed residues if provided |
| """ |
| quality_metrics = {} |
| |
| |
| mdtraj_metrics = metrics.calc_mdtraj_metrics(pdb_path) |
| quality_metrics.update(mdtraj_metrics) |
| |
| |
| sample_feats = du.parse_pdb_feats("sample", pdb_path) |
| ca_idx = rc.atom_order['CA'] |
| |
| |
| logger = logging.getLogger(__name__) |
| logger.info(f"PDB features keys: {sample_feats.keys()}") |
| logger.info(f"bb_positions shape: {sample_feats['bb_positions'].shape}") |
| logger.info(f"bb_positions type: {type(sample_feats['bb_positions'])}") |
| |
| |
| ca_positions = sample_feats["bb_positions"] |
| if len(ca_positions.shape) == 1: |
| |
| ca_positions = ca_positions.reshape(-1, 3) |
| elif len(ca_positions.shape) > 2: |
| |
| ca_positions = ca_positions[:, ca_idx] |
| |
| |
| if isinstance(ca_positions, torch.Tensor): |
| ca_positions = ca_positions.detach().cpu().numpy() |
| |
| logger.info(f"Final ca_positions shape: {ca_positions.shape}") |
| |
| |
| ca_ca_metrics = metrics.calc_ca_ca_metrics(ca_positions) |
| quality_metrics.update(ca_ca_metrics) |
| |
| |
| if reference_pdb_path is not None: |
| ref_feats = du.parse_pdb_feats("reference", reference_pdb_path) |
| ref_seq = du.aatype_to_seq(ref_feats["aatype"]) |
| sample_seq = du.aatype_to_seq(sample_feats["aatype"]) |
| |
| |
| _, tm_score = metrics.calc_tm_score( |
| sample_feats["bb_positions"], |
| ref_feats["bb_positions"], |
| sample_seq, |
| ref_seq |
| ) |
| quality_metrics["tm_score"] = tm_score |
| |
| |
| rmsd = metrics.calc_aligned_rmsd( |
| sample_feats["bb_positions"], |
| ref_feats["bb_positions"] |
| ) |
| quality_metrics["rmsd_to_ref"] = rmsd |
| |
| |
| if fixed_residues is not None: |
| |
| sample_res_indices = sample_feats['residue_index'] |
| ref_res_indices = ref_feats['residue_index'] |
| sample_fixed_indices = [] |
| ref_fixed_indices = [] |
| for resnum in fixed_residues: |
| sample_matches = np.where(sample_res_indices == resnum)[0] |
| ref_matches = np.where(ref_res_indices == resnum)[0] |
| if len(sample_matches) == 0 or len(ref_matches) == 0: |
| logger.warning(f"Residue number {resnum} not found in sample or reference residue_indices.") |
| continue |
| sample_fixed_indices.append(sample_matches[0]) |
| ref_fixed_indices.append(ref_matches[0]) |
| if sample_fixed_indices and ref_fixed_indices: |
| sample_fixed = sample_feats["bb_positions"][sample_fixed_indices] |
| ref_fixed = ref_feats["bb_positions"][ref_fixed_indices] |
| |
| fixed_rmsd = metrics.calc_aligned_rmsd(sample_fixed, ref_fixed) |
| quality_metrics["fixed_residues_rmsd"] = fixed_rmsd |
| logger.info(f"RMSD for fixed residues {fixed_residues}: {fixed_rmsd:.3f} Å") |
| else: |
| logger.warning(f"No valid fixed residue indices found for RMSD calculation.") |
| |
| return quality_metrics |
|
|
| def analyze_sample_diversity(self, sample_pdbs, reference_pdb=None): |
| """Analyze the diversity of a set of generated protein samples. |
| |
| Args: |
| sample_pdbs: List of paths to PDB files of generated samples |
| reference_pdb: Optional path to reference structure |
| |
| Returns: |
| Dictionary containing diversity metrics: |
| - Pairwise RMSD statistics between samples |
| - RMSD to reference if provided |
| - Structure clustering analysis |
| - Secondary structure diversity |
| """ |
| diversity_metrics = {} |
| |
| |
| num_samples = len(sample_pdbs) |
| pairwise_rmsds = [] |
| for i in range(num_samples): |
| sample_i_feats = du.parse_pdb_feats("sample_i", sample_pdbs[i]) |
| for j in range(i+1, num_samples): |
| sample_j_feats = du.parse_pdb_feats("sample_j", sample_pdbs[j]) |
| rmsd = metrics.calc_aligned_rmsd( |
| sample_i_feats["bb_positions"], |
| sample_j_feats["bb_positions"] |
| ) |
| pairwise_rmsds.append(rmsd) |
| |
| diversity_metrics["mean_pairwise_rmsd"] = np.mean(pairwise_rmsds) |
| diversity_metrics["std_pairwise_rmsd"] = np.std(pairwise_rmsds) |
| diversity_metrics["min_pairwise_rmsd"] = np.min(pairwise_rmsds) |
| diversity_metrics["max_pairwise_rmsd"] = np.max(pairwise_rmsds) |
| |
| |
| if reference_pdb is not None: |
| ref_feats = du.parse_pdb_feats("reference", reference_pdb) |
| ref_seq = du.aatype_to_seq(ref_feats["aatype"]) |
| ref_rmsds = [] |
| ref_tm_scores = [] |
| |
| for sample_pdb in sample_pdbs: |
| sample_feats = du.parse_pdb_feats("sample", sample_pdb) |
| sample_seq = du.aatype_to_seq(sample_feats["aatype"]) |
| |
| |
| rmsd = metrics.calc_aligned_rmsd( |
| sample_feats["bb_positions"], |
| ref_feats["bb_positions"] |
| ) |
| ref_rmsds.append(rmsd) |
| |
| |
| _, tm_score = metrics.calc_tm_score( |
| sample_feats["bb_positions"], |
| ref_feats["bb_positions"], |
| sample_seq, |
| ref_seq |
| ) |
| ref_tm_scores.append(tm_score) |
| |
| diversity_metrics["mean_rmsd_to_ref"] = np.mean(ref_rmsds) |
| diversity_metrics["std_rmsd_to_ref"] = np.std(ref_rmsds) |
| diversity_metrics["mean_tm_score_to_ref"] = np.mean(ref_tm_scores) |
| diversity_metrics["std_tm_score_to_ref"] = np.std(ref_tm_scores) |
| |
| return diversity_metrics |
| |
| def prepare_conditional_inputs(self, pdb_path, fixed_residues=None, chain_id='A'): |
| """Prepare inputs for conditional sampling from a PDB file. |
| |
| Args: |
| pdb_path: Path to the PDB file containing the partial/reference structure |
| fixed_residues: List of residue indices to fix (PDB numbering, e.g. 628, 629, ...) |
| chain_id: Chain ID to use from the PDB file (default='A') |
| |
| Returns: |
| Dictionary containing: |
| - fixed_positions: [N, 3] tensor of fixed atom positions |
| - fixed_mask: [N] boolean mask indicating which positions are fixed |
| - num_res: Total number of residues |
| - residue_indices: Original residue indices from PDB file |
| """ |
| |
| pdb_feats = du.parse_pdb_feats("reference", pdb_path, exclude_hetatm=True) |
| |
| |
| num_res = pdb_feats["aatype"].shape[0] |
| device = next(self.parameters()).device |
| |
| |
| logger = logging.getLogger(__name__) |
| logger.info(f"PDB features keys: {pdb_feats.keys()}") |
| |
| |
| if 'bb_positions' not in pdb_feats: |
| raise ValueError("No backbone positions found in PDB features") |
| |
| fixed_positions = pdb_feats['bb_positions'] |
| |
| |
| residue_indices = pdb_feats['residue_index'] |
| |
| |
| if fixed_residues is None: |
| |
| fixed_mask = torch.ones(num_res, dtype=torch.bool, device=device) |
| else: |
| |
| fixed_indices = [] |
| for resnum in fixed_residues: |
| matches = np.where(residue_indices == resnum)[0] |
| if len(matches) == 0: |
| raise ValueError(f"Residue number {resnum} not found in PDB residue_indices: {residue_indices}") |
| fixed_indices.append(matches[0]) |
| fixed_mask = torch.zeros(num_res, dtype=torch.bool, device=device) |
| fixed_mask[fixed_indices] = True |
| |
| |
| fixed_positions = torch.tensor(fixed_positions, dtype=torch.float32, device=device) |
| |
| |
| logger.info(f"fixed_positions shape: {fixed_positions.shape}") |
| logger.info(f"fixed_mask shape: {fixed_mask.shape}") |
| |
| if len(fixed_positions.shape) != 2 or fixed_positions.shape[1] != 3: |
| raise ValueError(f"Expected fixed_positions shape [N, 3], got {fixed_positions.shape}") |
| |
| return { |
| 'fixed_positions': fixed_positions, |
| 'fixed_mask': fixed_mask, |
| 'num_res': num_res, |
| 'residue_indices': residue_indices |
| } |
|
|
| def sample_with_fixed_residues( |
| self, |
| pdb_path, |
| fixed_residues=None, |
| num_samples=1, |
| temperature=1.0, |
| chain_id='A', |
| output_dir=None, |
| clf_model=None, |
| guidance_scale=0.2, |
| target_class=1 |
| ): |
| """Generate protein samples while keeping specified residues fixed. |
| |
| Args: |
| pdb_path: Path to PDB file with reference structure |
| fixed_residues: List of residue indices to fix (1-indexed) |
| num_samples: Number of samples to generate |
| temperature: Temperature for sampling diversity |
| chain_id: Chain ID to use from PDB |
| output_dir: Directory to save samples (if None, uses self._sample_write_dir) |
| clf_model: Optional classifier model for guidance |
| guidance_scale: Scale factor for classifier guidance (default=0.2) |
| target_class: Target class for classifier guidance (default=1) |
| Returns: |
| List of paths to generated PDB files |
| """ |
| |
| inputs = self.prepare_conditional_inputs( |
| pdb_path, |
| fixed_residues=fixed_residues, |
| chain_id=chain_id |
| ) |
| |
| |
| device = next(self.parameters()).device |
| self.interpolant.set_device(device) |
| |
| |
| |
| fixed_positions = inputs['fixed_positions'].unsqueeze(0).expand(num_samples, -1, -1) |
| |
| |
| fixed_mask = inputs['fixed_mask'].to(device) |
| |
| |
| batch = { |
| 'res_mask': torch.ones(num_samples, inputs['num_res'], device=device), |
| 'flow_mask': ~fixed_mask, |
| 'fixed_positions': fixed_positions, |
| 'fixed_mask': fixed_mask, |
| } |
| |
| |
| atom37_traj, clean_atom37_traj, clean_traj = self.interpolant.sample_conditional( |
| num_batch=num_samples, |
| num_res=inputs['num_res'], |
| model=self.model, |
| fixed_positions=fixed_positions, |
| fixed_mask=fixed_mask, |
| temperature=temperature, |
| clf_model=clf_model, |
| guidance_scale=guidance_scale, |
| target_class=target_class |
| ) |
| |
| |
| sample_paths = [] |
| save_dir = output_dir if output_dir is not None else self._sample_write_dir |
| os.makedirs(save_dir, exist_ok=True) |
| |
| |
| parser = PDB.PDBParser(QUIET=True) |
| structure = parser.get_structure('ref', pdb_path) |
| residue_indices = np.array([residue.id[1] for residue in structure[0][chain_id]]) |
| |
| for i in range(num_samples): |
| sample_path = os.path.join( |
| save_dir, |
| f'conditional_sample_{i}.pdb' |
| ) |
| |
| |
| b_factors = torch.zeros((inputs['num_res'], 37), device=device) |
| b_factors[fixed_mask] = 100.0 |
| |
| |
| sample_coords = clean_atom37_traj[i].detach().cpu().numpy() |
| b_factors = b_factors.detach().cpu().numpy() |
| |
| |
| if len(sample_coords.shape) == 4: |
| sample_coords = sample_coords[0] |
| |
| |
| atom37_mask = np.sum(np.abs(sample_coords), axis=-1) > 1e-7 |
| |
| |
| prot = create_full_prot( |
| sample_coords, |
| atom37_mask, |
| b_factors=b_factors, |
| residue_indices=residue_indices |
| ) |
| |
| |
| pdb_str = protein.to_pdb(prot, model=1, add_end=False) |
| with open(sample_path, 'w') as f: |
| f.write(pdb_str) |
| f.write('END\n') |
| |
| sample_paths.append(sample_path) |
| |
| return sample_paths |
| |