In [4]:
# Поставить свежий проди локально: 
# pip install biopython
# pip install prody
In [3]:
import prody as pd
In [12]:
import numpy as np # Модуль для векторных и матричных операций и всякой линейной алгебры, которая нам тут не пригодится

Я скачал только ту из двух рибосом, в которой есть амикумацин (на сайте PDB можно скачать Biological Assembly, это то, в каком виде эта молекула по мнению ученых должна выглядеть и работать в оригинале, а не в условиях кристаллографического эксперимента, которые зачастую очень отличаются от физиологических).

In [6]:
R = pd.parseCIF('../../Downloads/5i4l-assembly2.cif')
@> 206590 atoms and 1 coordinate set(s) were parsed in 3.81s.

206к атомов это просто запредельно для нашей задачи. В идеале всего атомов, вместе с водой, которую мы зальем вокруг, должно быть около 100к, а еще лучше, если меньше. А тут только одного белка 200к. Надо что-то делать!

In [7]:
ami = R.select('resname UAM') # выделил только атомы амикумацина

Читать про выделения тут: http://prody.csb.pitt.edu/manual/reference/atomic/select.html

In [8]:
?ami # это свой тип данных (Selection), по сути это итератор по атомам. Его можно преобразовать в список: list(ami)

Чтобы посмотреть список методов объекта, надо вбить его название и нажать Tab.

In [10]:
ami_c = ami.getCoords()
print(ami_c) # список координат всех атомов амикумацина
[[223.922 -26.296 227.67 ]
 [224.749 -27.486 228.093]
 [224.365 -28.321 229.139]
 [225.169 -29.372 229.548]
 [226.373 -29.617 228.913]
 [226.781 -28.81  227.856]
 [227.971 -29.067 227.244]
 [225.973 -27.731 227.432]
 [226.442 -26.84  226.376]
 [227.425 -27.037 225.707]
 [225.717 -25.733 226.109]
 [224.269 -25.742 226.305]
 [223.89  -24.274 226.219]
 [224.22  -23.665 224.857]
 [224.328 -22.146 224.859]
 [225.693 -21.683 224.391]
 [223.223 -21.517 224.049]
 [224.585 -23.53  227.263]
 [223.953 -22.764 228.156]
 [222.733 -22.629 228.192]
 [224.831 -22.049 229.168]
 [225.513 -20.974 228.551]
 [223.953 -21.548 230.32 ]
 [224.697 -21.635 231.528]
 [223.405 -20.134 230.084]
 [222.065 -19.943 230.659]
 [224.336 -19.012 230.571]
 [224.564 -18.947 232.065]
 [223.614 -18.978 232.852]
 [225.823 -18.88  232.464]]
In [13]:
ami_center = np.mean(ami_c, axis = 0) # возьмем среднее по строкам
In [14]:
ami_center # и вот наш центер амикумацина (не центр масс, конечно, но тоже сгодится)
Out[14]:
array([224.8194    , -23.87866667, 228.22133333])
In [15]:
within_30 = R.select('within 30 of center', center = ami_center)
In [16]:
len(within_30) # на расстоянии 30 ангстрем от центра амикумацина находится 3345 атома
Out[16]:
3345
In [18]:
def list_chain_breaks(atom_list):
    resindices = set(atom_list.getResindices()) 
    # сохранил сет индексов остатков, к которым принадлежит исходный набор атомов
    # важно: Resindex это не то же, что и Resnum. Resnum может повторяться, 
    # Resindex уникален и вообще говоря не совпадает с Resnum.
    
    breaks = []
    br_start, br_end = 0, 0
    inside_break = False
    
    for i in range(min(resindices), max(resindices) + 1):
        if i in resindices:
            if inside_break: 
                # это значит, что мы встретили первый остаток после разрыва
                inside_break = False
                br_end = i-1
                breaks.append((br_start, br_end))
            else:
                continue
        else:
            if inside_break:
                # это значит, что мы продолжаем идти по разрыву
                continue
            else:
                # мы встретили первый остаток разрыва
                br_start = i
                inside_break = True
                
    return breaks
In [20]:
breaks_chain_5 = list_chain_breaks(within_30.select('chain 5'))
print(breaks_chain_5)
[(4578, 4618), (4622, 4632), (4635, 4641), (4648, 4649), (4654, 7501), (7503, 7510), (7512, 7725), (7727, 7749), (7751, 7803), (7805, 7809), (7811, 8054)]

Voila, индексы остатков разрывов. На наше исходное выделение приходится 11 разрывов только по цепи "5". Теперь узнаем, сколько в них атомов:

In [21]:
for br in breaks_chain_5:
    start, end = br
    loop = R.select('resindex %d to %d' % (start, end))
    print(start, end, len(loop))
4578 4618 880
4622 4632 243
4635 4641 149
4648 4649 40
4654 7501 82244
7503 7510 56
7512 7725 1498
7727 7749 161
7751 7803 221
7805 7809 5
7811 8054 244
In [25]:
within_30_chains = set(within_30.getChids()) # Какие цепи попали в выделение?
In [26]:
within_30_chains
Out[26]:
{'5', '6', 'c', 'd', 'q', 's'}
In [27]:
within_30_breaks = {}
for chain in within_30_chains:
    atoms = within_30.select('chain %s' % chain)
    within_30_breaks[chain] = list_chain_breaks(atoms)
In [28]:
for chain,br in within_30_breaks.items():
    print(chain, len(br))
6 26
5 11
c 4
d 8
q 3
s 1
In [29]:
print('Всего возможных разрывов %d' % (sum([26,11,4,8,3,1])))
Всего возможных разрывов 53

Всего вариантов попарного их сочетания 2**53, что есть

In [30]:
2**53
Out[30]:
9007199254740992

Пожалуй, сразу надо бы ввести какие-то ограничения на возможную длину заполняемого разрыва. Допустим, оставим только такие разрывы, длина которых не превосходит 50.

In [31]:
w30br_short50 = {} # horrible variable name, I know
for chain,breaks in within_30_breaks.items():
    filtered_breaks = []
    for br in breaks:
        start, end = br
        if end - start + 1 <= 50:
            filtered_breaks.append(br)
    w30br_short50[chain] = filtered_breaks
In [32]:
N_breaks = 0
for chain,br in w30br_short50.items():
    N_breaks += len(br)
    print(chain, len(br))
print(2**N_breaks)
6 21
5 7
c 4
d 8
q 3
s 0
8796093022208

Все еще дохрена вариантов. Что ж, поиграемся с порогами длины.

In [36]:
breaks_short = {'5': {},
                '10': {},
                '15': {},
                '20': {},
                '30': {},
                '40': {}}
for br_maxlen in breaks_short:
    N_breaks = 0
    for chain,breaks in within_30_breaks.items():
        filtered_breaks = []
        for br in breaks:
            start, end = br
            if end - start + 1 <= int(br_maxlen):
                filtered_breaks.append(br)
        breaks_short[br_maxlen][chain] = filtered_breaks
        N_breaks += len(filtered_breaks)
    print(br_maxlen, N_breaks, 2**N_breaks)
5 17 131072
10 25 33554432
15 28 268435456
20 29 536870912
30 38 274877906944
40 42 4398046511104

Варианты 5-20 выглядят подъемными. Посмотрим, как распределятся значения при отсечке в 10.

In [40]:
chains = sorted(list(breaks_short['10'].keys()))
chains
Out[40]:
['5', '6', 'c', 'd', 'q', 's']
In [42]:
lengths = [len(breaks_short['10'][x]) for x in chains]
lengths
Out[42]:
[4, 12, 1, 6, 2, 0]
In [43]:
chains = ['5','6','c','d','q']
# зафиксировал определенный порядок цепей и убрал 's', так как в ней ноль разрывов
In [35]:
# config = {'chain': [break_number_1, break_number_3,...]}
In [84]:
def configuration_to_values(config, breaks):
    n_atoms = 0
    n_breaks = 25 # потому что при отсечке в 10 у нас всего 25 разрывов
    for chain, c_indices in config.items():
        for i in c_indices:
            br = breaks[chain][i]
            start, end = br
            n_atoms += end - start + 1 # спойлер: это не число атомов, а число остатков, но осознал я это только сильно позже
            n_breaks -= 1 
    return n_atoms, n_breaks
    
In [44]:
# binary_code = '0110110001...01' - строчка длиной в 25, кодирующая конфигурацию разрывов
In [45]:
bool('0'), bool('1')
Out[45]:
(True, True)
In [47]:
bool(int('0')), bool(int('1'))
Out[47]:
(False, True)
In [56]:
def code_to_config(code, chains, breaks):
    i = -1
    config = {}
    for c in chains:
        coded_breaks = []
        for bi,b in enumerate(breaks[c]):
            i += 1
            if int(code[i]):
                coded_breaks.append(bi)
        config[c] = coded_breaks
    return config
In [57]:
code_to_config('0100011001001001001101011', chains, breaks_short['10'])
Out[57]:
{'5': [1], '6': [1, 2, 5, 8, 11], 'c': [], 'd': [1, 2, 4], 'q': [0, 1]}

Все такие коды можно сгенерировать с помощью itertools

In [50]:
from itertools import product
In [70]:
all_25_codes = product(['0','1'], repeat=25)
In [66]:
test = product(['a','b'], repeat = 3)
list(test)
Out[66]:
[('a', 'a', 'a'),
 ('a', 'a', 'b'),
 ('a', 'b', 'a'),
 ('a', 'b', 'b'),
 ('b', 'a', 'a'),
 ('b', 'a', 'b'),
 ('b', 'b', 'a'),
 ('b', 'b', 'b')]
In [71]:
codes = []
N_atoms = []
N_breaks = []
In [68]:
int('10011', 2) # чтобы не забивать память, буду сохранять коды как числа
Out[68]:
19
In [72]:
i = 0
for code in all_25_codes:
    codestring = ''.join(code) # перевел код в строку
    config = code_to_config(codestring, chains, breaks_short['10'])
    na,nb = configuration_to_values(config, breaks_short['10'])
    
    codes.append(int(codestring,2))
    N_atoms.append(na)
    N_breaks.append(nb)
    i += 1
    if i % 1000000 == 0:
        print("Processed %d combinations" % i)
Processed 1000000 combinations
Processed 2000000 combinations
Processed 3000000 combinations
Processed 4000000 combinations
Processed 5000000 combinations
Processed 6000000 combinations
Processed 7000000 combinations
Processed 8000000 combinations
Processed 9000000 combinations
Processed 10000000 combinations
Processed 11000000 combinations
Processed 12000000 combinations
Processed 13000000 combinations
Processed 14000000 combinations
Processed 15000000 combinations
Processed 16000000 combinations
Processed 17000000 combinations
Processed 18000000 combinations
Processed 19000000 combinations
Processed 20000000 combinations
Processed 21000000 combinations
Processed 22000000 combinations
Processed 23000000 combinations
Processed 24000000 combinations
Processed 25000000 combinations
Processed 26000000 combinations
Processed 27000000 combinations
Processed 28000000 combinations
Processed 29000000 combinations
Processed 30000000 combinations
Processed 31000000 combinations
Processed 32000000 combinations
Processed 33000000 combinations
In [73]:
len(codes)
Out[73]:
33554432

Уии, теперь есть статистика по 33 миллионам вариантов рибовырезки. (2 Гб оперативки и около 10 минут счета)

Есть смысл сохранить данные, чтобы не пересчитывать. Для этого у Питона есть возможность записать компактный бинарный файл со своими объектами внутри.

In [74]:
import pickle as pkl
In [78]:
pkl.dump(codes, open('./ribo_30_10_codes.pkl', 'wb'))
pkl.dump(N_atoms, open('./ribo_30_10_nA.pkl', 'wb'))
pkl.dump(N_breaks, open('./ribo_30_10_nB.pkl', 'wb'))

Суммарно файлы заняли около 300 Мб

In [79]:
import matplotlib.pyplot as plt # Время графиков!
In [82]:
%matplotlib notebook
In [83]:
fig,ax = plt.subplots()
ax.hist(N_atoms)
fig.show()

На этом месте я осознал, что накодил не число атомов, а число остатков, что конечно не одно и то же, и нам интереснее было бы все же число атомов. Но пока можно проанализировать данные и так.

In [85]:
fig,ax = plt.subplots()
ax.hist(N_breaks)
fig.show()

Я боюсь, что полный поточечный график на 33 миллиона точек - перебор, поэтому сначала я построю 2д-гистограмму и определюсь, какие значения меня больше всего интереуют

In [90]:
fig,ax = plt.subplots()
ax.hist2d(N_atoms, N_breaks, bins=[max(N_atoms)+1,max(N_breaks)+1])
fig.show()

Зависимость линейная. Отнормируем все и посчитаем например произведение двух параметров.

In [91]:
N_atoms_np = np.array(N_atoms)
In [93]:
N_atoms_np = N_atoms_np / np.max(N_atoms_np)
In [94]:
N_breaks_np = np.array(N_breaks)
N_breaks_np = N_breaks_np / np.max(N_breaks_np)
In [96]:
N_breaks_np
Out[96]:
array([1.  , 0.96, 0.96, ..., 0.04, 0.04, 0.  ])
In [95]:
fig,ax = plt.subplots()
ax.hist2d(N_atoms_np, N_breaks_np, bins=[100,100])
fig.show()
In [97]:
t1 = np.array([0,1,2,3,4])
In [98]:
t1 * t1
Out[98]:
array([ 0,  1,  4,  9, 16])

Просто произведение нам не даст того, чего хотим, так как случай с 0 разрывов при умножении на явно большое число остатков даст 0, равно как и случай с 25 разрывами и 0 атомами.

Изолируем эти два случая

In [102]:
N_breaks_np[N_breaks_np == 0.] = 1.0
In [106]:
N_atoms_np[N_atoms_np == 0.] = 1.0
In [107]:
nA_nB = N_breaks_np * N_atoms_np
In [108]:
np.argmin(nA_nB)
Out[108]:
2
In [109]:
nA_nB[2]
Out[109]:
0.008888888888888889
In [110]:
codes[2]
Out[110]:
2
In [111]:
N_atoms[2]
Out[111]:
1
In [112]:
N_breaks[2]
Out[112]:
24

Такое себе, метрика с умножением делает лучшим вариант, когда явно много разрывов.

In [115]:
N_atoms_np = np.array(N_atoms)
N_atoms_np = N_atoms_np / np.max(N_atoms_np)
N_breaks_np = np.array(N_breaks)
N_breaks_np = N_breaks_np / np.max(N_breaks_np)
In [116]:
nA_plus_nB = N_atoms_np + N_breaks_np
In [117]:
np.argmin(nA_plus_nB)
Out[117]:
9344503
In [118]:
codes[9344503]
Out[118]:
9344503
In [119]:
N_atoms[9344503],N_breaks[9344503]
Out[119]:
(38, 10)

Вот это больше похоже на то, что хочется. 10 разрывов и 38 новых остатков.