Iteratory

Celem tego zadania jest dodanie brakujących iteratorów do klas Chain i Structure.

Klasy opisujące biomakromolekuły

Makromolekuły z których zbudowane są organizmy żywe: białka oraz kwasy nukleinowe składają się z oddzielnych łańcuchów. Każdy z nich składa się z reszt aminokwasowych (w przypadku białek) bądź nukleotydów (kwasy nukleinowe). Każda z reszt zaś zbudowana jest z atomów. Poniższy program jest odtworzeniem tej biochemicznej konstrukcji w postaci klas Structure, Chain, Residue i Atom. Struktura biomolekuły (Structure) przechowuje w prywatnym słowniku swoje łańcuchy. Każdy z łańcuchów (Chain) zawiera słownik reszt. Wreszcie, każda z reszt (Residue) zawiera listę jej atomów. Kompletny kod tych klas znajduje się na dole strony.

Iteratory

W przyjętej konstrukcji odwiedzenie wszystkich atomów ze struktury wymaga de facto trzech petli:

1from protein_structure import *
2
3strctr = read_pdb_content("2GB1", open("2gb1.pdb"))
4for chain in strctr.chains():
5    for residue in chain.residues():
6        for atom in residue.atoms():
7            print(atom.name)

Zaimplementuj brakujące metody: atoms() i residues() w klasie Structure, oraz atoms() w klasie Chain tak, aby możliwe było iterowanie po atomach i resztach w jednej pętli. Dla przykładu, poniższy kod powinien prawidłowo policzyć środek masy całego białka.

 1from protein_structure import *
 2
 3strctr = read_pdb_content("2GB1", open("2gb1.pdb"))
 4n, cm_x, cm_y, cm_z = 0, 0.0, 0.0, 0.0
 5for a in strctr.atoms():
 6    cm_x += a.x
 7    cm_y += a.y
 8    cm_z += a.z
 9    n += 1
10cm_x /= n
11cm_y /= n
12cm_z /= n

Zaimplementowanie iteratorów przyniesie także inne korzyści. Dla przykładu, użytkownicy będą mogli wykorzystać bibliotekę itertools w analizie struktur. Poniżej, oprócz klas reprezentujących biomolekuły, dołączono prosty test obliczający mapę odległości pomiędzy węglami alfa (atomy o nazwie ” CA “) wczytanego białka.

  1from __future__ import annotations
  2
  3import itertools
  4import math
  5from dataclasses import dataclass, field
  6from typing import Dict
  7
  8
  9@dataclass
 10class Structure:
 11    id_code: str
 12    __chains: Dict[str, Chain] = field(default_factory=dict)
 13
 14    def get_chain(self, chain_id: str):
 15        return self.__chains.get(chain_id, None)
 16
 17    def add_chain(self, chain: Chain):
 18        self.__chains[chain.code] = chain
 19
 20    def chains(self):
 21        return self.__chains.values()
 22
 23    def atoms(self):
 24        raise NotImplemented
 25
 26    def residues(self):
 27        raise NotImplemented
 28
 29@dataclass
 30class Chain:
 31    code: str
 32    __residues: Dict[str, Residue] = field(default_factory=dict)
 33
 34    def get_residue(self, res_seq: int, i_code: str):
 35        key = str(res_seq) + i_code
 36        return self.__residues.get(key, None)
 37
 38    def add_residue(self, res: Residue):
 39        key = str(res.res_seq) + res.i_code
 40        self.__residues[key] = res
 41
 42    def residues(self):
 43        return self.__residues.values()
 44
 45    def atoms(self):
 46        raise NotImplemented
 47
 48
 49@dataclass
 50class Residue:
 51    res_name: str
 52    res_seq: int
 53    i_code: str
 54    __atoms: list[Atom] = field(default_factory=list)
 55
 56    def add_atom(self, atom):
 57        self.__atoms.append(atom)
 58
 59    def ca(self):
 60        """Returns the alpha carbon of this residue or None when not present"""
 61        return next((a for a in self.__atoms if a.name == " CA "), None)
 62
 63    def atoms(self):
 64        return self.__atoms
 65
 66    def __str__(self):
 67        return f"{self.res_name} {self.res_seq}{self.i_code}"
 68
 69@dataclass
 70class Atom:
 71    serial: int
 72    name: str
 73    x: float
 74    y: float
 75    z: float
 76
 77    def distance_to(self, a):
 78        d2 = (self.x - a.x) * (self.x - a.x)
 79        d2 += (self.y - a.y) * (self.y - a.y)
 80        return math.sqrt(d2 + (self.z - a.z) * (self.z - a.z))
 81
 82def read_pdb_content(code, input_lines):
 83
 84    strctr = Structure(code)
 85
 86    for line in input_lines:
 87        if not line.startswith("ATOM") and not line.startswith("HETAT"): continue
 88        name = line[12:16]
 89        serial = int(line[6:11].strip())
 90        res_name = line[17:20]
 91        res_seq = int(line[22:26].strip())
 92        i_code = line[26]
 93        chain_id = line[21]
 94        x = float(line[30:38].strip())
 95        y = float(line[38:46].strip())
 96        z = float(line[46:54].strip())
 97        chain = strctr.get_chain(chain_id)
 98        if chain is None:
 99            chain = Chain(chain_id)
100            strctr.add_chain(chain)
101        resid = chain.get_residue(res_seq, i_code)
102        if resid is None:
103            resid = Residue(res_name, res_seq, i_code)
104            chain.add_residue(resid)
105        atom = Atom(serial, name, x, y, z)
106        resid.add_atom(atom)
107
108    return strctr
109
110
111if __name__ == "__main__":
112    strctr = read_pdb_content("2GB1",open("2gb1.pdb"))
113    for c in strctr.chains():
114        print(c.code)
115
116    chain_A = strctr.get_chain("A")
117    for ri, rj in itertools.combinations(chain_A.residues(), 2):
118        print(ri, rj, ri.ca().distance_to(rj.ca()))

Przykładowy plik w formacie PDB znajduje się tutaj.

Poniżej zaś znajduje się szkic rozwiązania tego zadania:

(rozwiń szkic rozwiązania)
 1from protein_structure import Structure, Chain
 2
 3
 4class AtomsInChainIterator:
 5    def __init__(self, chain: Chain):
 6        self.__residues = list(chain.residues())
 7        self.__current_residue = 0
 8        self.__current_atom = 0
 9
10    def __next__(self):
11        # sprawdź, czy możesz dać (__current_atom+1) atom z __current_residue
12        # TAK:
13        #    __current_atom+=1
14        #    zwróć __current_atom-1
15        # NIE:
16        #    __current_residue += 1
17        #    sprawdź, czy nie wyszliśmy poza łańcuch
18        #    NIE:
19        #        __current_residue += 1
20        #        __current_atom = 1
21        #        zwróć atom 0
22        #    TAK:
23        #        raise StopIteration
24        pass
25
26
27class AtomsInStructureIterator:
28    def __init__(self, strctr: Structure):
29        self.__chains = list(strctr.chains())
30        self.__current_chain = 0
31        self.__current_chain_it = AtomsInChainIterator(self.__chains[0])
32
33    def __next__(self):
34        try:
35            a = self.__current_chain_it.__next__()
36            return a
37        except:
38            # przesuń AtomsInChainIterator o jeden łańcuch
39            # zwróć atom
40            pass
41
42
43class Structure:
44
45    def atoms(self):
46        return AtomsInStructureIterator(self)