| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| """Protein data type.""" |
| import io |
| import os |
| import re |
| from typing import Any, Mapping, Optional |
|
|
| import numpy as np |
| from Bio.PDB import PDBParser |
|
|
| from onescience.utils.openfold.np import residue_constants |
| from onescience.utils.openfold.np.protein import Protein |
|
|
| FeatureDict = Mapping[str, np.ndarray] |
| ModelOutput = Mapping[str, Any] |
|
|
| |
| PDB_CHAIN_IDS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
| PDB_MAX_CHAINS = len(PDB_CHAIN_IDS) |
|
|
|
|
| def create_full_prot( |
| atom37: np.ndarray, |
| atom37_mask: np.ndarray, |
| chain_index=None, |
| aatype=None, |
| b_factors=None, |
| ): |
| assert atom37.ndim == 3 |
| assert atom37.shape[-1] == 3 |
| assert atom37.shape[-2] == 37 |
| n = atom37.shape[0] |
| residue_index = np.arange(n) |
| if chain_index is None: |
| chain_index = np.zeros(n) |
| if b_factors is None: |
| b_factors = np.zeros([n, 37]) |
| if aatype is None: |
| aatype = np.zeros(n, dtype=int) |
| return Protein( |
| atom_positions=atom37, |
| atom_mask=atom37_mask, |
| aatype=aatype, |
| residue_index=residue_index, |
| chain_index=chain_index, |
| b_factors=b_factors, |
| ) |
|
|
|
|
| def write_prot_to_pdb( |
| prot_pos: np.ndarray, |
| file_path: str, |
| aatype: np.ndarray = None, |
| chain_index: np.ndarray = None, |
| overwrite=False, |
| no_indexing=False, |
| b_factors=None, |
| ): |
| if overwrite: |
| max_existing_idx = 0 |
| else: |
| file_dir = os.path.dirname(file_path) |
| file_name = os.path.basename(file_path).strip(".pdb") |
| existing_files = [x for x in os.listdir(file_dir) if file_name in x] |
| max_existing_idx = max( |
| [ |
| int(re.findall(r"_(\d+).pdb", x)[0]) |
| for x in existing_files |
| if re.findall(r"_(\d+).pdb", x) |
| if re.findall(r"_(\d+).pdb", x) |
| ] |
| + [0] |
| ) |
| if not no_indexing: |
| save_path = file_path.replace(".pdb", "") + f"_{max_existing_idx+1}.pdb" |
| else: |
| save_path = file_path |
| with open(save_path, "w") as f: |
| if prot_pos.ndim == 4: |
| for t, pos37 in enumerate(prot_pos): |
| atom37_mask = np.sum(np.abs(pos37), axis=-1) > 1e-7 |
| prot = create_full_prot( |
| pos37, |
| atom37_mask, |
| chain_index=chain_index, |
| aatype=aatype, |
| b_factors=b_factors, |
| ) |
| pdb_prot = to_pdb(prot, model=t + 1, add_end=False) |
| f.write(pdb_prot) |
| elif prot_pos.ndim == 3: |
| atom37_mask = np.sum(np.abs(prot_pos), axis=-1) > 1e-7 |
| prot = create_full_prot( |
| prot_pos, |
| atom37_mask, |
| chain_index=chain_index, |
| aatype=aatype, |
| b_factors=b_factors, |
| ) |
| pdb_prot = to_pdb(prot, model=1, add_end=False) |
| f.write(pdb_prot) |
| else: |
| raise ValueError(f"Invalid positions shape {prot_pos.shape}") |
| f.write("END") |
| return save_path |
|
|
|
|
| def to_pdb(prot: Protein, model=1, add_end=True) -> str: |
| """Converts a `Protein` instance to a PDB string. |
| |
| Args: |
| prot: The protein to convert to PDB. |
| |
| Returns: |
| PDB string. |
| """ |
| restypes = residue_constants.restypes + ["X"] |
| res_1to3 = lambda r: residue_constants.restype_1to3.get(restypes[r], "UNK") |
| atom_types = residue_constants.atom_types |
|
|
| pdb_lines = [] |
|
|
| atom_mask = prot.atom_mask |
| aatype = prot.aatype |
| atom_positions = prot.atom_positions |
| residue_index = prot.residue_index.astype(int) + 1 |
| chain_index = prot.chain_index.astype(int) |
| b_factors = prot.b_factors |
|
|
| if np.any(aatype > residue_constants.restype_num): |
| raise ValueError("Invalid aatypes.") |
|
|
| |
| chain_ids = {} |
| for i in np.unique(chain_index): |
| if i >= PDB_MAX_CHAINS: |
| raise ValueError( |
| f"The PDB format supports at most {PDB_MAX_CHAINS} chains." |
| ) |
| chain_ids[i] = PDB_CHAIN_IDS[i] |
|
|
| pdb_lines.append(f"MODEL {model}") |
| atom_index = 1 |
| last_chain_index = chain_index[0] |
| chain_residue_index_offset = 0 |
| |
| for i in range(aatype.shape[0]): |
| |
| if last_chain_index != chain_index[i]: |
| pdb_lines.append( |
| _chain_end( |
| atom_index, |
| res_1to3(aatype[i - 1]), |
| chain_ids[chain_index[i - 1]], |
| residue_index[i - 1], |
| ) |
| ) |
| last_chain_index = chain_index[i] |
| chain_residue_index_offset = residue_index[i] |
| atom_index += 1 |
|
|
| res_name_3 = res_1to3(aatype[i]) |
| for atom_name, pos, mask, b_factor in zip( |
| atom_types, atom_positions[i], atom_mask[i], b_factors[i] |
| ): |
| if mask < 0.5: |
| continue |
|
|
| record_type = "ATOM" |
| name = atom_name if len(atom_name) == 4 else f" {atom_name}" |
| alt_loc = "" |
| insertion_code = "" |
| occupancy = 1.00 |
| element = atom_name[0] |
| charge = "" |
| |
| atom_line = ( |
| f"{record_type:<6}{atom_index:>5} {name:<4}{alt_loc:>1}" |
| f"{res_name_3:>3} {chain_ids[chain_index[i]]:>1}" |
| f"{residue_index[i] - chain_residue_index_offset:>4}{insertion_code:>1} " |
| f"{pos[0]:>8.3f}{pos[1]:>8.3f}{pos[2]:>8.3f}" |
| f"{occupancy:>6.2f}{b_factor:>6.2f} " |
| f"{element:>2}{charge:>2}" |
| ) |
| pdb_lines.append(atom_line) |
| atom_index += 1 |
|
|
| |
| pdb_lines.append( |
| _chain_end( |
| atom_index, |
| res_1to3(aatype[-1]), |
| chain_ids[chain_index[-1]], |
| residue_index[-1], |
| ) |
| ) |
| pdb_lines.append("ENDMDL") |
| if add_end: |
| pdb_lines.append("END") |
|
|
| |
| pdb_lines = [line.ljust(80) for line in pdb_lines] |
| return "\n".join(pdb_lines) + "\n" |
|
|
|
|
| def _chain_end(atom_index, end_resname, chain_name, residue_index) -> str: |
| chain_end = "TER" |
| return ( |
| f"{chain_end:<6}{atom_index:>5} {end_resname:>3} " |
| f"{chain_name:>1}{residue_index:>4}" |
| ) |
|
|
|
|
| def from_pdb_file(pdb_file: str, chain_id: Optional[str] = None) -> Protein: |
| """Takes a PDB file and constructs a Protein object. |
| |
| WARNING: All non-standard residue types will be converted into UNK. All |
| non-standard atoms will be ignored. |
| |
| Args: |
| pdb_file: Path to the PDB file |
| chain_id: If chain_id is specified (e.g. A), then only that chain is parsed. Otherwise all chains are parsed. |
| |
| Returns: |
| A new `Protein` parsed from the pdb contents. |
| """ |
| with open(pdb_file, "r") as f: |
| pdb_str = f.read() |
| return from_pdb_string(pdb_str=pdb_str, chain_id=chain_id) |
|
|
|
|
| def from_pdb_string(pdb_str: str, chain_id: Optional[str] = None) -> Protein: |
| """Takes a PDB string and constructs a Protein object. |
| |
| WARNING: All non-standard residue types will be converted into UNK. All |
| non-standard atoms will be ignored. |
| |
| Args: |
| pdb_str: The contents of the pdb file |
| chain_id: If chain_id is specified (e.g. A), then only that chain |
| is parsed. Otherwise all chains are parsed. |
| |
| Returns: |
| A new `Protein` parsed from the pdb contents. |
| """ |
| pdb_fh = io.StringIO(pdb_str) |
| parser = PDBParser(QUIET=True) |
| structure = parser.get_structure("none", pdb_fh) |
| models = list(structure.get_models()) |
| if len(models) != 1: |
| raise ValueError( |
| f"Only single model PDBs are supported. Found {len(models)} models." |
| ) |
| model = models[0] |
|
|
| atom_positions = [] |
| aatype = [] |
| atom_mask = [] |
| residue_index = [] |
| chain_ids = [] |
| b_factors = [] |
|
|
| for chain in model: |
| if chain_id is not None and chain.id != chain_id: |
| continue |
| for res in chain: |
| if res.id[2] != " ": |
| raise ValueError( |
| f"PDB contains an insertion code at chain {chain.id} and residue " |
| f"index {res.id[1]}. These are not supported." |
| ) |
| res_shortname = residue_constants.restype_3to1.get(res.resname, "X") |
| restype_idx = residue_constants.restype_order.get( |
| res_shortname, residue_constants.restype_num |
| ) |
| pos = np.zeros((residue_constants.atom_type_num, 3)) |
| mask = np.zeros((residue_constants.atom_type_num,)) |
| res_b_factors = np.zeros((residue_constants.atom_type_num,)) |
| for atom in res: |
| if atom.name not in residue_constants.atom_types: |
| continue |
| pos[residue_constants.atom_order[atom.name]] = atom.coord |
| mask[residue_constants.atom_order[atom.name]] = 1.0 |
| res_b_factors[residue_constants.atom_order[atom.name]] = atom.bfactor |
| if np.sum(mask) < 0.5: |
| |
| continue |
| aatype.append(restype_idx) |
| atom_positions.append(pos) |
| atom_mask.append(mask) |
| residue_index.append(res.id[1]) |
| chain_ids.append(chain.id) |
| b_factors.append(res_b_factors) |
|
|
| |
| unique_chain_ids = np.unique(chain_ids) |
| chain_id_mapping = {cid: n for n, cid in enumerate(unique_chain_ids)} |
| chain_index = np.array([chain_id_mapping[cid] for cid in chain_ids]) |
|
|
| return Protein( |
| atom_positions=np.array(atom_positions), |
| atom_mask=np.array(atom_mask), |
| aatype=np.array(aatype), |
| residue_index=np.array(residue_index), |
| chain_index=chain_index, |
| b_factors=np.array(b_factors), |
| ) |
|
|
|
|
| def load_pdb(fname: str) -> str: |
| """Returns pdb stored in input file as string.""" |
| with open(fname, "r") as f: |
| return from_pdb_string(f.read()) |
|
|