<jupyter_start><jupyter_text>Analise de Relação de Distância entre Molécula e ResíduoA problemática que essa análise visa resolver é entender como os arquivos exportados pelo programa **Gold** em *.mol2* e uma base de dados de Proteina (*.pdb*) obtida de uma outra pesquisa se relacionam na intenção de localizar residuos da proteina que reagem com a molécula em uma determinada distancia máxima.Inicialmente vamos declarar todos os caminhos de arquivos que serão analisados, `MOL2_FILE` é o arquivo onde se encontra todos os blocos de molécula (poses) a serem analisados. `PDB_FILES_FOLDER` é o caminho até a pasta onde estão todos os pdbs a serem analisados. `CSV_INPUT_FILE` é uma tabela gerada tabém pelo gold que mostra o nome da molécula e o número do **pdb** que melhor reagiu a molécula. E por fim o `MAX_DISTANCE` que é o total de angström.<jupyter_code>import os
import re
import pandas as pd
import numpy as np

from biopandas.pdb import PandasPdb
from biopandas.mol2 import PandasMol2, split_multimol2

MOL2_FILE = '../../data/chemscore_2_solutions.mol2'
PDB_FILES_FOLDER = '../../data/pdbs'
CSV_INPUT_FILE = '../../data/chemscore2.csv'
MAX_DISTANCE = 6.0
EXPECTED_RESIDUES = ['ALA_98', 'LEU_84', 'ILE_89', 'LEU_91', 'ARG_13', 'ARG_9']<jupyter_output><empty_output><jupyter_text>O primeiro passo é converter o *.mol2* e o *.pdb*, para isso será utilizado o *package* **biopandas**. Além disso é necessário que seja carregado o CSV de entrada com os dados da molécula e a pose que será analisada.<jupyter_code>converted_mols = {}
pdbs = {}

mols2 = split_multimol2(MOL2_FILE)

for pdb in os.listdir(PDB_FILES_FOLDER):
    name = pdb.split('_')[-1].replace('.pdb', '')
    pdbs[name] = PandasPdb().read_pdb('{}/{}'.format(PDB_FILES_FOLDER, pdb)).df

for mol2 in mols2:
    pmol = PandasMol2().read_mol2_from_list(mol2_lines=mol2[1], mol2_code=mol2[0])
    converted_mols[pmol.df.iloc[0]['subst_name']] = pmol.df
    
input_list = pd.read_csv(CSV_INPUT_FILE)<jupyter_output><empty_output><jupyter_text>Para analisar a distancia entre os atomos, primeiro será criado uma função que crie o ponto em dimensional,ela usara um **np.array** com as 3 coordenadas. Depois disso existira uma função que recebera esse ponto dimensional e usara a função **np.linalg.norm** para gerar a distancia. Essa distancia será comparada com a distancia desejada para entender se ela esta ou não dentro do desejavel. Caso a função devolva `True` significa que os átomos estão dentro de uma distancia, caso ao contrário não.Vale resaltar que o resíduo tem suas coordenadas traçadas pelas colunas: **x_coord**, **y_coord** e **z_coord** enquanto atomos tradicionais do mol2 tem suas posições definidas unicamente pelo nome do eixo **x**, **y** e **z**.<jupyter_code>def create_atom_dimensional_position(x, y, z):
    return np.array((x, y, z))

def atom_is_close_to_atom(resiude_atom, atom, distance):  
    return np.linalg.norm(resiude_atom - atom) <= distance


ad1 = pdbs['1']['ATOM'].iloc[0]
ad1 = create_atom_dimensional_position(x=ad1['x_coord'], y=ad1['y_coord'], z=ad1['z_coord'])
ad2 = converted_mols['alcaloide_1'].iloc[0]
ad2 = create_atom_dimensional_position(x=ad2['x'], y=ad2['y'], z=ad2['z'])

atom_is_close_to_atom(ad1, ad2, 6.0)<jupyter_output><empty_output><jupyter_text>Para a pesquisa atomos de hidrogenio tendem a ser poucos relevantes, para simplificar a interação e não gerar conflitos com o objetivo final, é necessário criar uma função que limpe o DataFrame do *.pdb* retirando todos os átomos de hidrogenio. Foi utilizada a própria API do DataFrame de filtro junto com a função **.str.contains** para localizar os atomos com H. Originalmente isso filtraria a lista para trazer apenas átomo **COM** a letra **H** por esse motivo foi utilizado o **~** na frente desse filtro, ele é uma negativa.<jupyter_code>def remove_hydrogen_atoms(df):
    return df['ATOM'][~df['ATOM']['atom_name'].str.contains('H')]<jupyter_output><empty_output><jupyter_text>A função mais relevante, é responsavel por navegar entre as moléculas, gerar seus pontos dimensionais, e após isso ir em cada resíduo (átomo por átomo) e testar se algum deles esta dentro do raio a partir da molécula que esta sendo verificada naquele momento dentro do *loop*. Caso os pontos estejam conforme a regra, é necessário verificar se ele ja não esta na lista de interações, caso não esteja é necessário adicioná-lo. No final a lista com todas as interações válidas é retornada.<jupyter_code>def get_interactions_molecule_for_residues(molecule, residues):
    matched_atoms = []
    mol_points = []
    
    for midx, mol_row in molecule.iterrows():
        mol_points.append(create_atom_dimensional_position(
            x=mol_row['x'], y=mol_row['y'], z=mol_row['z'], 
        ))
    
    for pidx, protein_row in remove_hydrogen_atoms(residues).iterrows():
        protein_tag = '{}_{}'.format(
            protein_row['residue_name'], protein_row['residue_number']
        )

        protein_ad = create_atom_dimensional_position(
            x=protein_row['x_coord'], y=protein_row['y_coord'], z=protein_row['z_coord']
        )

        for point in mol_points:
            if atom_is_close_to_atom(protein_ad, point, MAX_DISTANCE) and protein_tag not in matched_atoms:
                matched_atoms.append(protein_tag)
    
    return matched_atoms

print(get_interactions_molecule_for_residues(converted_mols['alcaloide_1'], pdbs['1']))<jupyter_output>['THR_5', 'ILE_6', 'ILE_7', 'ARG_9', 'ILE_10', 'ARG_13', 'LEU_31', 'VAL_63', 'THR_64', 'VAL_65', 'GLY_66', 'ASP_69', 'MET_70', 'LEU_71', 'ALA_72', 'ILE_73', 'SER_74', 'GLY_75', 'VAL_80', 'LYS_88', 'ILE_89', 'ALA_98', 'ALA_99', 'LYS_100', 'LEU_101', 'ALA_102', 'GLU_103', 'VAL_104', 'ILE_105']<jupyter_text>A última função é responsavel por receber o item do CSV de entrada junto com os resíduos que necessitam ser verificados se há uma interação. A função coleta dentro dos arquivos carregados a molécula referente ao item, e o PDB que teve a melhor reação. Após feito isso utiliza a função descrita acima para pegar todos os residuos que tiveram reação com a molécula. Por final ele aplica um filtro baseado no argumento de entrada para ver quais dos residuos retornados estão dentro dos residuos desejados e retorna apenas os residuos desejados que foram encontrados.<jupyter_code>def interate_with_expected_residues(item, expected_residues):
    resp = []
    m = converted_mols[item['NAME']]
    r = pdbs[str(item['Gold.Ensemble.ID'])]

    interactions = get_interactions_molecule_for_residues(m, r)

    return [key for key in interactions if key in expected_residues]

interate_with_expected_residues(input_list.iloc[0], ['ARG_13', 'HIS_111'])<jupyter_output><empty_output><jupyter_text>Por fim, porém não menos importante, temos o que seria o corpo do *script*, aqui é onde seria criado um novo CSV com a moécula, pdb, número de resíduos desejados que interagem e quais são eles. Para isso é feito uma interação no CSV de entrada, e para cada linha é executado a função acima. Uma vez retornado os resíduos ele são condensados em uma string separa por virgulas para a coluna `residues` e contados (para saber quantos reagiram) para coluna `residues_quantity`, as colunas `molecule_name` e `pdb` são simplesmente realocações dos valores `NAME` e `Gold.Ensemble.ID` do CSV original respectivamente.Para melhor visualização os dados são ordenado para elencar as moléculas com maior quantidade de reações acima.<jupyter_code>dataframe = pd.DataFrame(columns=['molecule_name', 'pdb', 'residues', 'residues_quantity'])

for i, row in input_list.iterrows():
    reactions = interate_with_expected_residues(row, EXPECTED_RESIDUES)
    dataframe = dataframe.append({
        'molecule_name': row['NAME'],
        'pdb': row['Gold.Ensemble.ID'],
        'residues': ",".join(reactions),
        'residues_quantity': len(reactions),
    }, ignore_index=True)

dataframe.sort_values(by=['residues_quantity'], ascending=False)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09951121694447926, 'fraction_numerical': 0.015791452562977818, 'mean_word_length': 5.200466200466201, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 28, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '568076', 'license': 'MIT', 'n_tokens_mistral': 2940, 'n_tokens_neox': 2810, 'n_words': 871, 'path': 'scripts/get_mols_by_residues/explanation.ipynb', 'repo_name': 'guidiego/iq-research-scripts', 'size': '7979'}