import dataclasses import numpy as np from utils import residue_constants PDB_CHAIN_IDS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' PDB_MAX_CHAINS = len(PDB_CHAIN_IDS) @dataclasses.dataclass(frozen=True) class Protein: atom_positions: np.ndarray aatype: np.ndarray atom_mask: np.ndarray residue_index: np.ndarray chain_index: np.ndarray b_factors: np.ndarray def __post_init__(self): if len(np.unique(self.chain_index)) > PDB_MAX_CHAINS: raise ValueError( f'Cannot build an instance with more than {PDB_MAX_CHAINS}' ) 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 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) chain_index = prot.chain_index.astype(int) b_factors = prot.b_factors if np.any(aatype > residue_constants.restype_num): raise ValueError('Invalid aatypes.') # Construct a mapping from chain integer indices to chain ID strings. chain_ids = {} for i in np.unique(chain_index): # np.unique gives sorted output. 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] # Add all atom sites. for i in range(aatype.shape[0]): # Close the previous chain if in a multichain PDB. 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] atom_index += 1 # Atom index increases at the TER symbol. 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] # Protein supports only C, N, O, S, this works. charge = '' # PDB is a columnar format, every space matters here! 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]:>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 # Close the final chain. 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') # Pad all lines to 80 characters. pdb_lines = [line.ljust(80) for line in pdb_lines] return '\n'.join(pdb_lines) + '\n' # Add terminating newline.