From 2fa806d8b2d2bd2c2d34eab13bf94f721233b686 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Sun, 15 Jun 2025 01:49:36 +0200 Subject: [PATCH 01/19] =?UTF-8?q?A=C3=B1adir=20m=C3=B3dulo=20para=20detecc?= =?UTF-8?q?i=C3=B3n=20autom=C3=A1tica=20de=20h=C3=A9lices=20PPII?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PPIIMol/PPIIMoL.py | 518 +++++++++++++++++++++++++++++++++++++++++++ PPIIMol/README_EN.md | 47 ++++ PPIIMol/README_ES.md | 47 ++++ 3 files changed, 612 insertions(+) create mode 100644 PPIIMol/PPIIMoL.py create mode 100644 PPIIMol/README_EN.md create mode 100644 PPIIMol/README_ES.md diff --git a/PPIIMol/PPIIMoL.py b/PPIIMol/PPIIMoL.py new file mode 100644 index 0000000..a9d82a6 --- /dev/null +++ b/PPIIMol/PPIIMoL.py @@ -0,0 +1,518 @@ +from pymol import cmd, stored +import tkinter as tk +import math +from tkinter import filedialog, messagebox +from tkinter.scrolledtext import ScrolledText +import os +import csv + +pdb_file = None +segmentos_ppii_global = [] # Variable global para guardar segmentos PPII detectados + + # global para controlar si hay proteína cargada + +def seleccionar_archivo(): + global pdb_file + pdb_file = filedialog.askopenfilename( + title="Selecciona un archivo PDB", + filetypes=[("Archivos PDB", "*.pdb")] + ) + if pdb_file: + cmd.reinitialize() + cmd.load(pdb_file, "proteina") + cmd.hide("everything", "proteina") # Ocultar todo lo visible + cmd.show("licorice", "proteina") # Mostrar como licorice + messagebox.showinfo("Archivo cargado", f"Se cargó:\n{pdb_file}") + +def descargar_molecula(): + global pdb_file + def fetch_pdb(): + global pdb_file + pdb_id = entry.get().strip() + if pdb_id: + try: + cmd.reinitialize() + cmd.fetch(pdb_id, name="proteina") + cmd.hide("everything", "proteina") + cmd.show("licorice", "proteina") + # Aquí asignamos pdb_file con un valor para simular archivo cargado + pdb_file = pdb_id # Solo guardamos el ID para que funcione la lógica de “selección” + messagebox.showinfo("Descarga completa", f"Molécula {pdb_id.upper()} descargada y cargada.") + fetch_window.destroy() + except Exception as e: + messagebox.showerror("Error", f"No se pudo descargar {pdb_id}: {e}") + else: + messagebox.showwarning("Advertencia", "Por favor ingresa un ID de PDB.") + + fetch_window = tk.Toplevel() + fetch_window.title("Descargar proteína") + tk.Label(fetch_window, text="ID de la proteína (PDB):").pack(pady=5) + entry = tk.Entry(fetch_window, width=20) + entry.pack(pady=5) + tk.Button(fetch_window, text="Descargar", command=fetch_pdb).pack(pady=5) + + + + +def anadir_hidrogenos(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + cmd.h_add("all") + cmd.sort("all extend 1") + cmd.show("licorice", "all") + messagebox.showinfo("Hidrógenos", "Hidrógenos añadidos y mostrados como licorice.") + + +def eliminar_solventes(): + cmd.remove("solvent") + messagebox.showinfo("Solventes", "Solventes eliminados.") + + +def ocultar_side_chains(): + cmd.hide("everything", "proteina and not name N+CA+C+O") + messagebox.showinfo("Backbone", "Cadenas laterales ocultas (solo backbone).") + + +def separar_cadenas(): + stored.chains = [] + cmd.iterate("proteina", "stored.chains.append(chain)") + for cadena in set(stored.chains): + nuevo_objeto = f"cadena_{cadena}" + cmd.create(nuevo_objeto, f"proteina and chain {cadena}") + + +def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): + resultados = {} + chains = cmd.get_chains(objeto) + for chain in chains: + sel_ca = f"{objeto} and chain {chain} and name CA" + phipsi = cmd.get_phipsi(sel_ca) + if not phipsi: + continue + for (obj, idx), (phi, psi) in sorted(phipsi.items()): + if phi is None or psi is None: + continue + stored.info = [] + cmd.iterate(f"({obj}`{idx})", "stored.info.append((chain, resn, resi))", space={'stored': stored}) + if not stored.info: + continue + ch, resn, resi = stored.info[0] + resultados[(ch, resi)] = (resn, phi, psi) + return resultados + + +def guardar_csv_angulos_phi_psi(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_map = obtener_angulos_phi_psi_por_cadena("proteina") + + stored.res_list = [] + cmd.iterate("proteina and name CA", "stored.res_list.append((chain, resn, resi))") + + datos_csv = [("Cadena", "Residuo", "Número", "Phi", "Psi")] + + for chain, resn, resi in sorted(stored.res_list, key=lambda x: (x[0], int(x[2]))): + key = (chain, resi) + if key in phi_map: + resn_val, phi, psi = phi_map[key] + datos_csv.append((chain, resn, resi, f"{phi:.2f}", f"{psi:.2f}")) + + # Guardar en CSV con separador ; + if len(datos_csv) > 1: + ruta_csv = os.path.join(os.getcwd(), "angulos_phi_psi.csv") + with open(ruta_csv, mode="w", newline="", encoding="utf-8") as file: + writer = csv.writer(file, delimiter=";") + writer.writerows(datos_csv) + + +def localizar_atomicos_clave_segmentos(segmentos): + cmd.delete("esfera_*") + objetos_por_segmento = [] + cmd.h_add() + + for idx, seg in enumerate(segmentos, start=1): + objetos_segmento = [] + + for (resn, resi, chain, _, _) in seg: + sele_base = f"proteina and chain {chain} and resi {resi}" + stored.coords = [] + + # Átomo CA (carbono alfa) + stored.ca_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name CA", + "stored.ca_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Oxígeno carbonilo + stored.o_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name O", + "stored.o_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Hidrógeno unido al CA (usar nombre fijo "H" en lugar del nombre real) + stored.h_coords = [] + cmd.iterate_state( + 1, + f"(neighbor ({sele_base} and name CA)) and elem H", + "stored.h_coords.append((x, y, z))", # Solo coordenadas, no nombre + space={'stored': stored} + ) + + # Crear pseudoatomos con nombres consistentes + if stored.ca_coords: + x, y, z = stored.ca_coords[0] + esfera_name = f"esfera_s{idx}_CA_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("blue", esfera_name) + objetos_segmento.append(esfera_name) + + if stored.o_coords: + x, y, z = stored.o_coords[0] + esfera_name = f"esfera_s{idx}_O_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("red", esfera_name) + objetos_segmento.append(esfera_name) + + if stored.h_coords: + x, y, z = stored.h_coords[0] + # NOMBRE FIJADO COMO "H" (en lugar del nombre real del átomo) + esfera_name = f"esfera_s{idx}_H_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("green", esfera_name) + objetos_segmento.append(esfera_name) + + objetos_por_segmento.append(objetos_segmento) + + cmd.group("atomos_clave", "esfera_*") + return objetos_por_segmento + + + + +def calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt"): + tripletas_candidatas = [] + + # Precalcular coordenadas de todos los objetos + coordenadas = {} + for segmento in objetos_por_segmento: + for obj in segmento: + coords = cmd.get_coords(obj) + if coords is not None and len(coords) > 0: + coordenadas[obj] = coords[0] # Tomamos la primera posición + + # Funciones de utilidad + def es_CA(obj_name): return "_CA_" in obj_name + def es_O(obj_name): return "_O_" in obj_name + def es_H(obj_name): return any(x in obj_name for x in ["_HA_", "_H_H_", "_H_CA_", "_H_"]) + + def extraer_resi_chain(obj_name): + partes = obj_name.rsplit('_', 2) + if len(partes) >= 3: + return partes[1], partes[2] # resi, chain + return None, None + + def distancia(p1, p2): + return math.sqrt(sum((a - b) ** 2 for a, b in zip(p1, p2))) + + max_dist_sq = max_dist ** 2 + + for i in range(len(objetos_por_segmento) - 1): + seg_actual = objetos_por_segmento[i] + seg_siguiente = objetos_por_segmento[i + 1] + + ca_actual = [obj for obj in seg_actual if es_CA(obj)] + o_actual = [obj for obj in seg_actual if es_O(obj)] + + ca_siguiente = [obj for obj in seg_siguiente if es_CA(obj)] + o_siguiente = [obj for obj in seg_siguiente if es_O(obj)] + + mapa_h_actual = {} + mapa_h_siguiente = {} + + for obj in seg_actual: + if es_H(obj): + resi, chain = extraer_resi_chain(obj) + if resi and chain and obj in coordenadas: + mapa_h_actual[(resi, chain)] = obj + + for obj in seg_siguiente: + if es_H(obj): + resi, chain = extraer_resi_chain(obj) + if resi and chain and obj in coordenadas: + mapa_h_siguiente[(resi, chain)] = obj + + for ca in ca_actual: + if ca not in coordenadas: + continue + + resi_ca, chain_ca = extraer_resi_chain(ca) + h_asociado = mapa_h_actual.get((resi_ca, chain_ca), None) + coord_ca = coordenadas[ca] + + for o in o_siguiente: + if o not in coordenadas: + continue + + coord_o = coordenadas[o] + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + + if dist_sq < max_dist_sq: + dist = math.sqrt(dist_sq) + tripletas_candidatas.append((ca, o, h_asociado, dist)) + + for o in o_actual: + if o not in coordenadas: + continue + + coord_o = coordenadas[o] + + for ca in ca_siguiente: + if ca not in coordenadas: + continue + + resi_ca, chain_ca = extraer_resi_chain(ca) + h_asociado = mapa_h_siguiente.get((resi_ca, chain_ca), None) + coord_ca = coordenadas[ca] + + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_o, coord_ca)) + + if dist_sq < max_dist_sq: + dist = math.sqrt(dist_sq) + tripletas_candidatas.append((o, ca, h_asociado, dist)) + + # Guardar reporte en archivo + with open(archivo_salida, "w") as f: + if tripletas_candidatas: + f.write(f"Tripletas colindantes con distancia < {max_dist:.1f} Å:\n") + for a1, a2, a3, dist in tripletas_candidatas: + f.write(f" {a1} - {a2} - {a3 if a3 else 'Sin H'} : {dist:.2f} Å\n") + else: + f.write(f"No se encontraron pares con distancia < {max_dist:.1f} Å.\n") + + return tripletas_candidatas + + + +def calcular_angulos_ca_h_o(tripletas_candidatas, archivo_salida="angulos_ca_h_o.txt"): + """ + Para cada tripleta (a1, a2, h, distancia) en tripletas_candidatas, + calcula el ángulo CA–H–O y lo guarda en un .txt **solo** si el ángulo está entre 110° y 180°. + + Parámetros: + - tripletas_candidatas: lista de tuplas (obj1, obj2, objH, distancia) + - archivo_salida: ruta del archivo donde se guardan los ángulos + + Retorna: + - lista de tuplas (CA, H, O, angulo_en_grados) filtrada con ángulos [110, 180] + """ + angulos = [] + + with open(archivo_salida, "w") as f: + f.write("CA - H - O : Ángulo (grados)\n") + for obj1, obj2, objH, dist in tripletas_candidatas: + # Saltar si no hay hidrógeno asociado + if objH is None: + continue + + # Determinar cuál es CA y cuál es O + if "_CA_" in obj1: + ca, o = obj1, obj2 + else: + ca, o = obj2, obj1 + + # Calcular ángulo CA–H–O + try: + ang = cmd.get_angle(ca, objH, o) + except Exception as e: + # Si falla PyMOL en la selección, lo informamos y seguimos + print(f"Error calculando ángulo para {ca}, {objH}, {o}: {e}") + continue + + # Filtrar solo ángulos entre 110° y 180° + if 110.0 <= ang <= 180.0: + angulos.append((ca, objH, o, ang)) + f.write(f"{ca} - {objH} - {o} : {ang:.2f}°\n") + + return angulos + + +def calcular_y_visualizar_distancias(max_dist=5.0): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) + visualizar_distancias_pares(pares) + calcular_angulos_ca_h_o(pares) + + + +def visualizar_distancias_pares(pares_candidatos): + """ + Recibe lista de tuplas (atomo1, atomo2, atomo_H, distancia) y crea objetos distancia en PyMOL. + """ + if not pares_candidatos: + messagebox.showinfo("Visualización", "No hay pares de átomos para visualizar.") + return + + cmd.delete("distancia_ppii") + + for i, (at1, at2, _, dist) in enumerate(pares_candidatos, start=1): + nombre_dist = f"distancia_ppii_{i}" + cmd.distance(nombre_dist, at1, at2) + cmd.set("dash_width", 4, nombre_dist) + cmd.set("dash_length", 0.5, nombre_dist) + cmd.color("cyan", nombre_dist) + + messagebox.showinfo("Visualización", f"Visualizados {len(pares_candidatos)} pares de distancias colindantes.") + + + +def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol=20.0): + global segmentos_ppii_global + + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_psi_map = obtener_angulos_phi_psi_por_cadena(objeto) + lista_residuos = [] + for (chain, resi), (resn, phi, psi) in phi_psi_map.items(): + try: + resi_num = int(resi) + except: + continue + lista_residuos.append((chain, resi_num, resn, phi, psi)) + lista_residuos.sort(key=lambda x: (x[0], x[1])) + + segmentos = [] + segmento_actual = [] + + def en_rango_ppii(phi, psi, tol=tol): + return abs(phi + 75) <= tol and abs(psi - 145) <= tol + + for i, (chain, resi, resn, phi, psi) in enumerate(lista_residuos): + if en_rango_ppii(phi, psi): + if not segmento_actual: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [(resn, resi, chain, phi, psi)] + else: + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [] + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + + if not segmentos: + messagebox.showinfo("Resultado", "No se encontraron segmentos PPII.") + return + + cmd.delete("ppii_segmento*") + + salida = "Segmentos candidatos a hélices PPII:\n" + for idx, seg in enumerate(segmentos, start=1): + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + salida += f"\nSegmento {idx} (Cadena {chain}, residuos {start_resi}-{end_resi}, longitud {len(seg)}):\n" + for (resn, resi, _, phi, psi) in seg: + salida += f" {resn}-{resi}{chain}: (phi={phi:.1f}, psi={psi:.1f})\n" + + sel_str = f"proteina and chain {chain} and resi {start_resi}-{end_resi}" + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + cmd.create(obj_name, sel_str) + cmd.color("red", obj_name) + cmd.show("cartoon", obj_name) + + ruta_archivo = os.path.join(os.getcwd(), "segmentos_ppii.txt") + with open(ruta_archivo, "w") as f: + f.write(salida) + + # Guardar global + segmentos_ppii_global = segmentos + + messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados, resaltados.\n" + f"Átomos clave también visualizados en PyMOL.") + + +def guardar_segmentos_ppii_pdb(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + # Para cada segmento almacenado, construimos el nombre de objeto y hacemos cmd.save + count = 0 + for seg in segmentos_ppii_global: + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + # Comprobamos que el objeto efectivamente exista en PyMOL + if cmd.count_atoms(f"{obj_name}") > 0: + filename = os.path.join(os.getcwd(), f"{obj_name}.pdb") + try: + cmd.save(filename, obj_name) + count += 1 + except Exception as e: + print(f"Error guardando {obj_name}: {e}") + else: + print(f"Objeto {obj_name} no encontrado en la sesión de PyMOL.") + + if count > 0: + messagebox.showinfo("Éxito", f"Se guardaron {count} archivos PDB de segmentos PPII:\n" + f"{os.getcwd()}") + else: + messagebox.showwarning("Atención", "No se guardó ningún segmento (quizá no existan objetos en PyMOL).") + + +def convertir_a_selecciones_pymol(pares_con_distancias): + selecciones = [] + for at1, at2, _ in pares_con_distancias: + sele1 = f"id {cmd.index(at1)[0][1]}" + sele2 = f"id {cmd.index(at2)[0][1]}" + selecciones.append((sele1, sele2)) + return selecciones + + + + +def lanzar_interfaz(): + root = tk.Tk() + root.title("Análisis de Proteína en PyMOL") + tk.Button(root, text="Seleccionar archivo PDB", command=seleccionar_archivo, width=40).pack(pady=10) + tk.Button(root, text="Descargar proteina", command=descargar_molecula, width=40).pack(pady=10) + tk.Button(root, text="Añadir hidrógenos", command=anadir_hidrogenos, width=40).pack(pady=10) + tk.Button(root, text="Eliminar solventes", command=eliminar_solventes, width=40).pack(pady=10) + tk.Button(root, text="Ocultar cadenas laterales", command=ocultar_side_chains, width=40).pack(pady=10) + tk.Button(root, text="Guardar ángulos phi/psi en archivo", command=guardar_csv_angulos_phi_psi, width=40).pack(pady=10) + tk.Button(root, text="Detectar segmentos PPII y resaltarlos", command=detectar_segmentos_ppii, width=40).pack(pady=10) + tk.Button(root, text="Guardar segmentos PPII en PDB", command=guardar_segmentos_ppii_pdb, width=40).pack(pady=10) + tk.Button(root, text="Calcular y visualizar distancias entre CA-O colindantes",command=calcular_y_visualizar_distancias, width=40).pack(pady=10) + + + root.mainloop() + + +lanzar_interfaz() diff --git a/PPIIMol/README_EN.md b/PPIIMol/README_EN.md new file mode 100644 index 0000000..0e87023 --- /dev/null +++ b/PPIIMol/README_EN.md @@ -0,0 +1,47 @@ +# PPIIMoL – Automated Detection of PPII Helices in PyMOL + +**PPIIMoL** is a Python module designed to integrate directly into PyMOL for the automatic detection of Polyproline II (PPII) helices in protein structures. + +PPII helices, often present in glycine- and proline-rich proteins and relevant in neurobiological processes, are typically not explicitly annotated in PDB files. This tool identifies geometric patterns compatible with PPII helices and displays them directly in PyMOL, streamlining structural analysis. + +## 🔬 Key Features + +- Load local PDB files or fetch structures directly within PyMOL. +- Automatic removal of solvents and hydrogen addition. +- Calculation of φ and ψ angles for each residue. +- Detection of segments consistent with PPII conformation. +- Direct visualization of results in PyMOL using pseudoatoms. +- Export of detected segments to `.csv` and `.pdb` formats for external analysis. + +## 📦 Requirements + +- [PyMOL](https://pymol.org/) (version with Python scripting support) +- Python 3.8+ +- Standard Python libraries (`math`, `tkinter`, `csv`, `os`) + +## 🚀 Usage + +1. Launch PyMOL with Python scripting support. +2. Run the script `PPIIMoL.py` via the PyMOL command line or graphical interface. +3. Load or fetch a protein structure. +4. Add hydrogens and perform the analysis. +5. Visualize the detected PPII helices and/or export the results. + +## 🧪 Test Case + +The module has been validated using the 3BOG protein, a well-known model with a high presence of PPII helices, confirming its accuracy and utility for structural research. + +## 📄 License + +Released under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html). + +## ✍️ Author + +This module was developed as part of a Bachelor's Thesis in Computer Engineering (UNIR), in collaboration with the Protein Structure, Dynamics and Interactions Group at the Institute of Physical Chemistry “Blas Cabrera” (IQF-CSIC). + +Author: Silvia Enma Rodríguez Fernández +GitHub: [@silviaenma](https://github.com/silviaenma) + +--- + +*Suggestions, contributions, and improvements are welcome.* diff --git a/PPIIMol/README_ES.md b/PPIIMol/README_ES.md new file mode 100644 index 0000000..e529698 --- /dev/null +++ b/PPIIMol/README_ES.md @@ -0,0 +1,47 @@ +# PPIIMoL - Detección automática de hélices PPII en PyMOL + +**PPIIMoL** es un módulo en Python diseñado para integrarse directamente con PyMOL y automatizar la detección de hélices de Poliprolina II (PPII) en estructuras proteicas. + +Estas hélices, relevantes en procesos neurobiológicos y en proteínas ricas en glicina y prolina, no suelen estar identificadas explícitamente en los archivos PDB. Este módulo permite detectar automáticamente patrones geométricos compatibles con hélices PPII y visualizarlos en PyMOL, facilitando su análisis estructural. + +## 🔬 Funcionalidades principales + +- Carga de archivos PDB o descarga directa desde PyMOL. +- Eliminación automática de solventes y adición de hidrógenos. +- Cálculo de ángulos φ y ψ para cada residuo. +- Detección de segmentos con conformación PPII. +- Visualización directa en PyMOL mediante pseudóatomos. +- Exportación de resultados a `.csv` y `.pdb` para análisis externo. + +## 📦 Requisitos + +- [PyMOL](https://pymol.org/) (versión con soporte de scripts Python) +- Python 3.8+ +- Módulos estándar de Python (`math`, `tkinter`, `csv`, `os`) + +## 🚀 Cómo usar + +1. Abre PyMOL con soporte de scripts. +2. Ejecuta el módulo `PPIIMoL.py` desde la consola de PyMOL o usa el GUI incluido. +3. Carga o descarga una proteína. +4. Añade hidrógenos y ejecuta el análisis. +5. Visualiza las hélices PPII detectadas y/o exporta los datos. + +## 🧪 Caso de prueba + +La herramienta ha sido validada utilizando la proteína 3BOG, una estructura rica en hélices PPII, demostrando su eficacia y utilidad para análisis sistemático. + +## 📄 Licencia + +Publicado bajo licencia [GPL v3.0](https://www.gnu.org/licenses/gpl-3.0.html). + +## ✍️ Autoría + +Este módulo ha sido desarrollado como parte de un Trabajo de Fin de Grado en Ingeniería Informática (UNIR), en colaboración con el grupo de investigación del Instituto de Química-Física “Blas Cabrera” (IQF-CSIC). + +Autora: Silvia Enma Rodríguez Fernández +GitHub: [@silviaenma](https://github.com/silviaenma) + +--- + +*Contribuciones, sugerencias y mejoras son bienvenidas.* From bf8e2fc8a4e38284ae7e116daea2248cc15c436c Mon Sep 17 00:00:00 2001 From: silviaenma Date: Mon, 7 Jul 2025 22:40:11 +0200 Subject: [PATCH 02/19] Added PPIIMoL module and updated repo --- PPIIMoL.py | 1285 ++++++++++++++++++++++++++++++++++++ PPIIMol/PPIIMoL - copia.py | 1285 ++++++++++++++++++++++++++++++++++++ PPIIMol/PPIIMoL.py | 1123 ++++++++++++++++++++++++++----- 3 files changed, 3515 insertions(+), 178 deletions(-) create mode 100644 PPIIMoL.py create mode 100644 PPIIMol/PPIIMoL - copia.py diff --git a/PPIIMoL.py b/PPIIMoL.py new file mode 100644 index 0000000..afe7457 --- /dev/null +++ b/PPIIMoL.py @@ -0,0 +1,1285 @@ +from pymol import cmd, stored +import tkinter as tk +import math +from pathlib import Path +from datetime import datetime +import os +from tkinter import filedialog, messagebox +import tkinter as tk +from tkinter import ttk, messagebox +import os +import csv + + +# Valores por defecto (se actualizarán cuando el usuario los cambie) +tol_phi_global = 20.0 +tol_psi_global = 20.0 +min_ang_global = 110.0 +max_ang_global = 180.0 + + +pdb_file = None +segmentos_ppii_global = [] # Variable global para guardar segmentos PPII detectados +localizar_atomos=None +distancias=None +angulos_v=None +objetos = None +pares = None + +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_CARPETA_RESULTADOS <<<<<<<< +# ========================================================== + +""" +obtener_carpeta_resultados(nombre_carpeta='Resultados_PyMOL') +---------------------------------------------------------- +Descripción: + Crea una carpeta con fecha actual dentro del directorio Documentos + (o equivalente) del usuario para guardar los resultados generados + por el módulo. + +Parámetros: + nombre_carpeta : str, opcional + Nombre base para la carpeta de resultados (por defecto 'Resultados_PyMOL'). + +Retorno: + pathlib.Path + Ruta completa a la carpeta creada. + +Ejemplo de uso: + >>> carpeta = obtener_carpeta_resultados("Resultados_PPIIMoL") + >>> print(carpeta) + /home/usuario/Documentos/Resultados_PPIIMoL_2025-07-05 +---------------------------------------------------------- +""" + +def obtener_carpeta_resultados(nombre_carpeta="Resultados_PyMOL"): + # Formatear fecha y hora actual (compatible con ambos sistemas) + fecha_hora = datetime.now().strftime("%Y-%m-%d") + + # Usar ~/Documents o ~/Documentos según el sistema + documentos = Path.home() / ("Documentos" if os.getenv('LANG', '').startswith('es_') else "Documents") + + # Si no existe, crear directorio en HOME directamente + if not documentos.exists(): + documentos = Path.home() + + # Crear nombre de carpeta combinado + nombre_completo = f"{nombre_carpeta}_{fecha_hora}" + carpeta_resultados = documentos / nombre_completo + + # Crear la carpeta (con parents=True por si faltan directorios padres) + carpeta_resultados.mkdir(parents=True, exist_ok=True) + + return carpeta_resultados + +# ========================================================== +# >>>>>>>> FUNCIÓN: SELECCIONAR_ARCHIVO <<<<<<<< +# ========================================================== + +""" +seleccionar_archivo() +---------------------------------------------------------- +Descripción: + Abre un cuadro de diálogo para que el usuario seleccione un archivo PDB + de entrada. La ruta seleccionada se guarda en la variable global pdb_file. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> seleccionar_archivo() +---------------------------------------------------------- +""" + +def seleccionar_archivo(): + global pdb_file + pdb_file = filedialog.askopenfilename( + title="Selecciona un archivo PDB", + filetypes=[("Archivos PDB", "*.pdb")] + ) + if pdb_file: + cmd.reinitialize() + cmd.load(pdb_file, "proteina") + cmd.hide("everything", "proteina") # Ocultar todo lo visible + cmd.show("licorice", "proteina") # Mostrar como licorice + messagebox.showinfo("Archivo cargado", f"Se cargó:\n{pdb_file}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: DESCARGAR_MOLECULA <<<<<<<< +# ========================================================== + +""" +descargar_molecula() +---------------------------------------------------------- +Descripción: + Abre una ventana emergente para permitir al usuario introducir un ID + de proteína del Protein Data Bank (PDB) y descargar automáticamente + la estructura correspondiente. Una vez descargada, la molécula se + carga en PyMOL, se ocultan todas las representaciones gráficas + actuales y se muestra la proteína usando el estilo "licorice". + La ruta o ID de la proteína descargada se guarda en la variable global + pdb_file. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> descargar_molecula() +---------------------------------------------------------- +""" + +def descargar_molecula(): + global pdb_file + def fetch_pdb(): + global pdb_file + pdb_id = entry.get().strip() + if pdb_id: + try: + cmd.reinitialize() + cmd.fetch(pdb_id, name="proteina") + cmd.hide("everything", "proteina") + cmd.show("licorice", "proteina") + # Aquí asignamos pdb_file con un valor para simular archivo cargado + pdb_file = pdb_id # Solo guardamos el ID para que funcione la lógica de “selección” + messagebox.showinfo("Descarga completa", f"Molécula {pdb_id.upper()} descargada y cargada.") + fetch_window.destroy() + except Exception as e: + messagebox.showerror("Error", f"No se pudo descargar {pdb_id}: {e}") + else: + messagebox.showwarning("Advertencia", "Por favor ingresa un ID de PDB.") + + fetch_window = tk.Toplevel() + fetch_window.title("Descargar proteína") + tk.Label(fetch_window, text="ID de la proteína (PDB):").pack(pady=5) + entry = tk.Entry(fetch_window, width=20) + entry.pack(pady=5) + tk.Button(fetch_window, text="Descargar", command=fetch_pdb).pack(pady=5) + +# ========================================================== +# >>>>>>>> FUNCIÓN: ANADIR_HIDROGENOS <<<<<<<< +# ========================================================== + +""" +anadir_hidrogenos() +---------------------------------------------------------- +Descripción: + Añade átomos de hidrógeno a todas las moléculas cargadas + en la sesión actual de PyMOL. Una vez añadidos, la + estructura se reorganiza para optimizar la visualización + y se muestra usando el estilo "licorice". Si no hay + ningún archivo PDB cargado previamente, se muestra una + advertencia al usuario. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> anadir_hidrogenos() +---------------------------------------------------------- +""" + +def anadir_hidrogenos(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + cmd.h_add("all") + cmd.sort("all extend 1") + cmd.show("licorice", "all") + messagebox.showinfo("Hidrógenos", "Hidrógenos añadidos y mostrados como licorice.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: ELIMINAR_SOLVENTES <<<<<<<< +# ========================================================== + +""" +eliminar_solventes() +---------------------------------------------------------- +Descripción: + Elimina todas las moléculas de solvente presentes en la + estructura cargada en PyMOL. Tras la eliminación, se + muestra un mensaje informativo al usuario confirmando + la acción realizada. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> eliminar_solventes() +---------------------------------------------------------- +""" + +def eliminar_solventes(): + cmd.remove("solvent") + messagebox.showinfo("Solventes", "Solventes eliminados.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: OCULTAR_SIDE_CHAINS <<<<<<<< +# ========================================================== + +""" +ocultar_side_chains() +---------------------------------------------------------- +Descripción: + Oculta todas las cadenas laterales de los residuos de + la proteína cargada en PyMOL, dejando visibles únicamente + los átomos del esqueleto principal (backbone: N, CA, C, O). + Esta función permite centrar la visualización en la + estructura principal de la proteína sin distracciones. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> ocultar_side_chains() +---------------------------------------------------------- +""" + +def ocultar_side_chains(): + cmd.hide("everything", "proteina and not name N+CA+C+O") + messagebox.showinfo("Backbone", "Cadenas laterales ocultas (solo backbone).") + +# ========================================================== +# >>>>>>>> FUNCIÓN: SEPARAR_CADENAS <<<<<<<< +# ========================================================== + +""" +separar_cadenas() +---------------------------------------------------------- +Descripción: + Separa cada cadena de la proteína cargada en PyMOL + en objetos individuales. Cada nueva cadena se guarda + como un objeto separado con un nombre identificador + en el formato "cadena_X", donde X es el identificador + de la cadena original. Esta función facilita la + manipulación y análisis individual de las cadenas. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> separar_cadenas() +---------------------------------------------------------- +""" + +def separar_cadenas(): + stored.chains = [] + cmd.iterate("proteina", "stored.chains.append(chain)") + for cadena in set(stored.chains): + nuevo_objeto = f"cadena_{cadena}" + cmd.create(nuevo_objeto, f"proteina and chain {cadena}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_ANGULOS_PHI_PSI_POR_CADENA <<<<<<<< +# ========================================================== + +""" +obtener_angulos_phi_psi_por_cadena(objeto="proteina") +---------------------------------------------------------- +Descripción: + Calcula los ángulos torsionales phi (ϕ) y psi (ψ) de cada + residuo en todas las cadenas del objeto especificado + cargado en PyMOL. Los resultados se devuelven en un + diccionario que asocia cada residuo con su nombre, número + y los valores de los ángulos phi y psi correspondientes. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el cual se + calcularán los ángulos (por defecto "proteina"). + +Retorno: + dict + Diccionario con las claves como tuplas (cadena, número + de residuo) y valores como tuplas (nombre del residuo, + ángulo phi, ángulo psi). + +Ejemplo de uso: + >>> resultados = obtener_angulos_phi_psi_por_cadena() + >>> print(resultados) +---------------------------------------------------------- +""" + +def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): + resultados = {} + chains = cmd.get_chains(objeto) + for chain in chains: + sel_ca = f"{objeto} and chain {chain} and name CA" + phipsi = cmd.get_phipsi(sel_ca) + if not phipsi: + continue + for (obj, idx), (phi, psi) in sorted(phipsi.items()): + if phi is None or psi is None: + continue + stored.info = [] + cmd.iterate(f"({obj}`{idx})", "stored.info.append((chain, resn, resi))", space={'stored': stored}) + if not stored.info: + continue + ch, resn, resi = stored.info[0] + resultados[(ch, resi)] = (resn, phi, psi) + return resultados + +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_CSV_ANGULOS_PHI_PSI <<<<<<<< +# ========================================================== + +""" +guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con los ángulos torsionales phi (ϕ) + y psi (ψ) calculados para cada residuo de la proteína + cargada en PyMOL. El archivo se guarda en una carpeta + de resultados con el nombre "angulos_phi_psi.csv" e + incluye información de la cadena, el residuo, su número + y los valores de los ángulos. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +""" + +def guardar_csv_angulos_phi_psi(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_map = obtener_angulos_phi_psi_por_cadena("proteina") + + stored.res_list = [] + cmd.iterate("proteina and name CA", "stored.res_list.append((chain, resn, resi))") + + datos_csv = [("Cadena", "Residuo", "Número", "Phi", "Psi")] + + for chain, resn, resi in sorted(stored.res_list, key=lambda x: (x[0], int(x[2]))): + key = (chain, resi) + if key in phi_map: + resn_val, phi, psi = phi_map[key] + datos_csv.append((chain, resn, resi, f"{phi:.2f}", f"{psi:.2f}")) + carpeta= obtener_carpeta_resultados() + # Guardar en CSV con separador ; + if len(datos_csv) > 1: + ruta_csv = os.path.join(os.getcwd(), carpeta/"angulos_phi_psi.csv") + with open(ruta_csv, mode="w", newline="", encoding="utf-8") as file: + writer = csv.writer(file, delimiter=";") + writer.writerows(datos_csv) + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GENERAR_REPORTE_CSV <<<<<<<< +# ========================================================== + +""" +generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con un informe detallado sobre los + segmentos de hélices PPII detectados en la proteína cargada + en PyMOL. El reporte incluye pares de átomos Cα-O colindantes, + sus distancias, los ángulos CA-H-O calculados y los valores + validados según los rangos definidos. El archivo se guarda + en una carpeta de resultados con el nombre especificado. + +Parámetros: + segmentos_ppii : list + Lista de segmentos PPII detectados en la proteína. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los + pares de átomos colindantes. Por defecto es 5.0 Å. + nombre_archivo : str, opcional + Nombre del archivo CSV que se generará. Por defecto + es "reporte_ppii.csv". + min_ang : float, opcional + Ángulo mínimo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> generar_reporte_csv(segmentos_ppii) +---------------------------------------------------------- +""" + +def generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0): + if not segmentos_ppii: + messagebox.showwarning("Advertencia", "No hay segmentos PPII detectados.") + return + + objetos_por_segmento = localizar_atomos + pares_ca_o=distancias + angulos_validos=angulos_v + if objetos_por_segmento ==None: + objetos_por_segmento = localizar_atomicos_clave_segmentos(segmentos_ppii) + if pares_ca_o is None: + pares_ca_o = calcular_distancias_colindantes(objetos_por_segmento, max_dist=max_dist) + + # PASAMOS LOS ÁNGULOS AQUÍ + if angulos_validos is None: + angulos_validos = calcular_angulos_ca_h_o(pares_ca_o, min_ang=min_ang, max_ang=max_ang) + + dist_dict = {} + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): + continue + coord_ca = cmd.get_coords(ca)[0] + coord_o = cmd.get_coords(o)[0] + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coord_ca, coord_o))) + dist_dict[(ca, o)] = dist + + ang_dict = {(ca, o): ang for ca, _, o, ang in angulos_validos} + + datos_csv = [["Átomo Cα", "Átomo O", "Distancia (Å)", "Ángulo CA-H-O (°)"]] + for (ca, o), dist in dist_dict.items(): + ang = ang_dict.get((ca, o), None) + if ang is not None: + datos_csv.append([ca, o, f"{dist:.2f}", f"{ang:.2f}"]) + + ruta_csv = obtener_carpeta_resultados() / nombre_archivo + with open(ruta_csv, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f, delimiter=';') + writer.writerows(datos_csv) + + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: LOCALIZAR_ATOMICOS_CLAVE_SEGMENTOS <<<<<<<< +# ========================================================== + +""" +localizar_atomicos_clave_segmentos(segmentos) +---------------------------------------------------------- +Descripción: + Localiza y marca en PyMOL los átomos clave (Cα y O) de + cada segmento PPII detectado. Crea pseudoátomos en las + posiciones correspondientes para facilitar la visualización + y análisis. Los átomos Cα se marcan en color azul y los O + en color rojo, agrupándolos bajo el nombre "atomos_clave". + +Parámetros: + segmentos : list + Lista de segmentos PPII detectados en la proteína. + +Retorno: + list + Lista de listas, donde cada sublista contiene los + nombres de los pseudoátomos creados para un segmento. + +Ejemplo de uso: + >>> localizar_atomicos_clave_segmentos(segmentos_ppii) +---------------------------------------------------------- +""" + +def localizar_atomicos_clave_segmentos(segmentos): + global localizar_atomos + cmd.delete("esfera_*") + objetos_por_segmento = [] + cmd.h_add() + + for idx, seg in enumerate(segmentos, start=1): + objetos_segmento = [] + + for (resn, resi, chain, _, _) in seg: + sele_base = f"proteina and chain {chain} and resi {resi}" + stored.coords = [] + + # Átomo CA (carbono alfa) + stored.ca_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name CA", + "stored.ca_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Oxígeno carbonilo + stored.o_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name O", + "stored.o_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Crear pseudoatomos con nombres consistentes + if stored.ca_coords: + x, y, z = stored.ca_coords[0] + esfera_name = f"esfera_s{idx}_CA_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("blue", esfera_name) + objetos_segmento.append(esfera_name) + + if stored.o_coords: + x, y, z = stored.o_coords[0] + esfera_name = f"esfera_s{idx}_O_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("red", esfera_name) + objetos_segmento.append(esfera_name) + + objetos_por_segmento.append(objetos_segmento) + + cmd.group("atomos_clave", "esfera_*") + localizar_atomos = objetos_por_segmento + return objetos_por_segmento + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_DISTANCIAS_COLINDANTES <<<<<<<< +# ========================================================== + +""" +calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt") +---------------------------------------------------------- +Descripción: + Calcula las distancias entre los átomos Cα y O de segmentos + PPII consecutivos o cercanos. Identifica los pares cuya + distancia es menor al valor máximo especificado. Guarda los + resultados en un archivo de texto y devuelve la lista de + pares de átomos que cumplen el criterio. + +Parámetros: + objetos_por_segmento : list + Lista de listas con los nombres de los pseudoátomos + (Cα y O) de cada segmento. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán las + distancias calculadas. Por defecto es + "distancias_colindantes.txt". + +Retorno: + list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) cuya distancia es menor a max_dist. + +Ejemplo de uso: + >>> pares = calcular_distancias_colindantes(objetos_por_segmento) +---------------------------------------------------------- +""" + +def calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt"): + global distancias + pares_ca_o = [] + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / archivo_salida + coordenadas = {} + + for segmento in objetos_por_segmento: + for obj in segmento: + coords = cmd.get_coords(obj) + if coords is None or len(coords) == 0: + continue + coordenadas[obj] = coords[0] + + def es_CA(obj): return "_CA_" in obj + def es_O(obj): return "_O_" in obj + + max_dist_sq = max_dist ** 2 + + for salto in [1, 2]: + for i in range(len(objetos_por_segmento) - salto): + seg1 = objetos_por_segmento[i] + seg2 = objetos_por_segmento[i + salto] + + ca1 = [obj for obj in seg1 if es_CA(obj)] + o2 = [obj for obj in seg2 if es_O(obj)] + ca2 = [obj for obj in seg2 if es_CA(obj)] + o1 = [obj for obj in seg1 if es_O(obj)] + + # Comparar ca1 con o2 + for ca in ca1: + coord_ca = coordenadas.get(ca) + if coord_ca is None: + continue + for o in o2: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) + + # Comparar ca2 con o1 + for ca in ca2: + coord_ca = coordenadas.get(ca) + if coord_ca is None: + continue + for o in o1: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) + + with open(archivo_salida, "w") as f: + f.write(f"Pares CA-O con distancia < {max_dist} Å:\n") + for i, (ca, o) in enumerate(pares_ca_o): + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coordenadas[ca], coordenadas[o]))) + f.write(f"{i:03d} | {ca} - {o} : {dist:.2f} Å\n") + + print(f"[DEBUG] Total de pares CA-O guardados: {len(pares_ca_o)}") + distancias= pares_ca_o + return pares_ca_o + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_ANGULOS_CA_H_O <<<<<<<< +# ========================================================== + +""" +calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt") +---------------------------------------------------------- +Descripción: + Calcula los ángulos formados entre los átomos Cα-H-O para + cada par de átomos Cα-O proporcionado. Filtra y guarda + únicamente los ángulos que estén dentro del rango definido + por min_ang y max_ang. Los resultados se almacenan en un + archivo de texto y se devuelven como una lista. + +Parámetros: + pares_ca_o : list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) sobre los que se calcularán los ángulos. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 180.0°. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán los + ángulos calculados. Por defecto es "angulos_ca_h_o.txt". + +Retorno: + list + Lista de tuplas con la información de cada ángulo válido + en el formato (Cα, H, O, ángulo). + +Ejemplo de uso: + >>> angulos = calcular_angulos_ca_h_o(pares_ca_o) +---------------------------------------------------------- +""" + +def calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt"): + global angulos_v + angulos_validos = [] + archivo_salida = obtener_carpeta_resultados() / archivo_salida + archivo_salida.parent.mkdir(parents=True, exist_ok=True) + + with open(archivo_salida, "w") as f: + f.write("CA - H - O : Ángulo (grados)\n") + + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): + continue + + # Extraer resn, resi y chain + partes = ca.split("_") + if len(partes) < 4: + f.write(f"# Nombre inválido para {ca}\n") + continue + + resn = partes[-3] + resi = partes[-2] + chain = partes[-1] + + # Buscar Hs sin usar "model" + seleccion = f"resn {resn} and resi {resi} and chain {chain} and name H*" + try: + hidrogenos = cmd.get_model(seleccion).atom + except Exception as e: + f.write(f"# Error al buscar H para {ca} ({seleccion}): {e}\n") + continue + + if not hidrogenos: + f.write(f"# No se encontraron H para {ca} (búsqueda: {seleccion})\n") + continue + + for h in hidrogenos: + h_sel = f"/{h.model}//{h.chain}/{h.resi}/{h.name}" + h_nombre = f"{h.resn}-{h.resi}{h.chain}_{h.name}" + try: + ang = cmd.get_angle(ca, h_sel, o) + if min_ang <= abs(ang) <= max_ang: + angulos_validos.append((ca, h_nombre, o, ang)) + f.write(f"{ca} - {h_nombre} - {o} : {ang:.2f}°\n") + except Exception as e: + f.write(f"# Error al calcular ángulo para {ca}, {h_sel}, {o} : {e}\n") + angulos_v= angulos_validos + return angulos_validos + + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_GUARDAR_ANGULOS <<<<<<<< +# ========================================================== + +""" +calcular_y_guardar_angulos() +---------------------------------------------------------- +Descripción: + Calcula los ángulos CA-H-O para los segmentos PPII previamente + detectados en la proteína cargada en PyMOL. Localiza los átomos + clave, obtiene las distancias colindantes y calcula los ángulos + que cumplen con el rango de validez (110°-180°). Los resultados + se guardan en un archivo de texto llamado "angulos_ca_h_o.txt" + dentro de la carpeta de resultados. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_guardar_angulos() +---------------------------------------------------------- +""" + + +def calcular_y_guardar_angulos(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + # Obtener los objetos atómicos clave + objetos = localizar_atomos + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + + # Calcular distancias para obtener las tripletas + tripletas = calcular_distancias_colindantes(objetos) + + # Calcular y guardar ángulos + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / "angulos_ca_h_o.txt" + angulos = calcular_angulos_ca_h_o(tripletas, str(archivo_salida)) # Convertir a string para PyMOL + + if angulos: + messagebox.showinfo("Éxito", f"Ángulos CA-H-O guardados en:\n{archivo_salida}") + else: + messagebox.showwarning("Aviso", "No se encontraron ángulos válidos (110°-180°)") + + +# ========================================================== +# >>>>>>>> FUNCIÓN: VISUALIZAR_DISTANCIAS_PARES <<<<<<<< +# ========================================================== +""" +visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +Descripción: + Dibuja y visualiza en PyMOL las distancias entre los pares + de átomos proporcionados. Cada distancia se representa como + una línea discontinua (dashed line) de color cian para + facilitar la identificación de interacciones potenciales. + +Parámetros: + pares_candidatos : list + Lista de tuplas con los nombres de los pares de átomos + (atomo1, atomo2) que se desean visualizar. + +Retorno: + None + +Ejemplo de uso: + >>> visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +""" + + +def visualizar_distancias_pares(pares_candidatos): + if not pares_candidatos: + messagebox.showinfo("Visualización", "No hay pares de átomos para visualizar.") + return + + cmd.delete("distancia_ppii") + + for i, (at1, at2) in enumerate(pares_candidatos, start=1): + nombre_dist = f"distancia_ppii_{i}" + cmd.distance(nombre_dist, at1, at2) + cmd.set("dash_width", 4, nombre_dist) + cmd.set("dash_length", 0.5, nombre_dist) + cmd.color("cyan", nombre_dist) + + messagebox.showinfo("Visualización", f"Visualizados {len(pares_candidatos)} pares de distancias colindantes.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: DETECTAR_SEGMENTOS_PPII <<<<<<<< +# ========================================================== + +""" +detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0) +---------------------------------------------------------- +Descripción: + Detecta segmentos de hélices de tipo poliprolina II (PPII) + en la proteína cargada en PyMOL. Analiza los ángulos + torsionales phi (ϕ) y psi (ψ) de cada residuo para + identificar regiones consecutivas compatibles con la + conformación PPII según los criterios de tolerancia + definidos. Permite configurar la longitud mínima de los + segmentos y el número máximo de saltos permitidos entre + residuos consecutivos. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el que se + realizará la detección. Por defecto es "proteina". + min_length : int, opcional + Longitud mínima (en número de residuos) que debe tener + un segmento para ser considerado PPII. Por defecto es 3. + tol_phi : float, opcional + Tolerancia en grados para el ángulo phi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + tol_psi : float, opcional + Tolerancia en grados para el ángulo psi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + max_saltos : int, opcional + Número máximo de residuos consecutivos que pueden + incumplir los criterios de PPII dentro de un segmento + sin interrumpirlo. Por defecto es 0. + +Retorno: + None + +Ejemplo de uso: + >>> detectar_segmentos_ppii() +---------------------------------------------------------- +""" + +def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0): + global segmentos_ppii_global + + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_psi_map = obtener_angulos_phi_psi_por_cadena(objeto) + lista_residuos = [] + for (chain, resi), (resn, phi, psi) in phi_psi_map.items(): + try: + resi_num = int(resi) + except: + continue + lista_residuos.append((chain, resi_num, resn, phi, psi)) + lista_residuos.sort(key=lambda x: (x[0], x[1])) + + segmentos = [] + segmento_actual = [] + saltos_restantes = max_saltos # Contador de saltos permitidos + + def en_rango_ppii(phi, psi, tol_phi=20.0, tol_psi=20.0): + # Valores IDEALES para PPII: φ = -75°, ψ = +145° + return (abs(phi - (-75)) <= tol_phi) and (abs(psi - 145) <= tol_psi) + + for i, (chain, resi, resn, phi, psi) in enumerate(lista_residuos): + if en_rango_ppii(phi, psi, tol_phi, tol_psi): + # Si cumple, reiniciamos el contador de saltos + saltos_restantes = max_saltos + if not segmento_actual: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [(resn, resi, chain, phi, psi)] + else: + # Si no cumple pero hay saltos disponibles + if segmento_actual and saltos_restantes > 0: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + saltos_restantes -= 1 + continue + + # Si no hay saltos disponibles o no es consecutivo + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [] + saltos_restantes = max_saltos + + # Añadir el último segmento si cumple con la longitud mínima + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + + if not segmentos: + messagebox.showinfo("Resultado", "No se encontraron segmentos PPII.") + return + + cmd.delete("ppii_segmento*") + + salida = f"Segmentos candidatos a hélices PPII (saltos permitidos: {max_saltos}):\n" + for idx, seg in enumerate(segmentos, start=1): + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + salida += f"\nSegmento {idx} (Cadena {chain}, residuos {start_resi}-{end_resi}, longitud {len(seg)}):\n" + for (resn, resi, _, phi, psi) in seg: + salida += f" {resn}-{resi}{chain}: (phi={phi:.1f}, psi={psi:.1f})\n" + + sel_str = f"proteina and chain {chain} and resi {start_resi}-{end_resi}" + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + cmd.create(obj_name, sel_str) + cmd.color("red", obj_name) + cmd.show("cartoon", obj_name) + + carpeta = obtener_carpeta_resultados() + ruta_archivo = carpeta / "segmentos_ppii.txt" + with open(ruta_archivo, "w") as f: + f.write(salida) + + segmentos_ppii_global = segmentos + + messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados (con {max_saltos} saltos permitidos).\n" + f"Átomos clave visualizados en PyMOL.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_SEGMENTO_PPII_PDB <<<<<<<< +# ========================================================== + +""" +guardar_segmento_ppii_pdb(segmento, nombre_archivo="segmento_ppii.pdb") +---------------------------------------------------------- +Descripción: + Guarda en un archivo PDB un segmento específico de hélice + PPII previamente detectado en la proteína cargada en PyMOL. + El archivo se genera con el nombre especificado dentro de la + carpeta de resultados. + +Parámetros: + segmento : list + Lista con los residuos que forman el segmento PPII a guardar. + nombre_archivo : str, opcional + Nombre del archivo PDB que se generará. Por defecto es + "segmento_ppii.pdb". + +Retorno: + None + +Ejemplo de uso: + >>> guardar_segmento_ppii_pdb(segmentos_ppii[0]) +---------------------------------------------------------- +""" + +def guardar_segmentos_ppii_pdb(): + global segmentos_ppii_global + carpeta = obtener_carpeta_resultados() + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + # Para cada segmento almacenado, construimos el nombre de objeto y hacemos cmd.save + count = 0 + for seg in segmentos_ppii_global: + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + # Comprobamos que el objeto efectivamente exista en PyMOL + if cmd.count_atoms(f"{obj_name}") > 0: + + filename = carpeta / f"{obj_name}.pdb" + try: + cmd.save(filename, obj_name) + count += 1 + except Exception as e: + print(f"Error guardando {obj_name}: {e}") + else: + print(f"Objeto {obj_name} no encontrado en la sesión de PyMOL.") + + if count > 0: + messagebox.showinfo("Éxito", f"Se guardaron {count} archivos PDB de segmentos PPII:\n" + f"{os.getcwd()}") + else: + messagebox.showwarning("Atención", "No se guardó ningún segmento (quizá no existan objetos en PyMOL).") + +# ========================================================== +# >>>>>>>> FUNCIÓN: CONVERTIR_A_SELECCIONES_PYMOL <<<<<<<< +# ========================================================== + +""" +convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +Descripción: + Convierte una lista de pares de átomos con sus distancias en + selecciones de PyMOL. Cada par se representa como una selección + que facilita la visualización y el análisis de las interacciones + detectadas. + +Parámetros: + pares_con_distancias : list + Lista de tuplas en el formato (atomo1, atomo2, distancia) + que serán convertidas en selecciones de PyMOL. + +Retorno: + list + Lista con los nombres de las selecciones creadas en PyMOL. + +Ejemplo de uso: + >>> selecciones = convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +""" + +def convertir_a_selecciones_pymol(pares_con_distancias): + selecciones = [] + for at1, at2, _ in pares_con_distancias: + sele1 = f"id {cmd.index(at1)[0][1]}" + sele2 = f"id {cmd.index(at2)[0][1]}" + selecciones.append((sele1, sele2)) + return selecciones + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_VISUALIZAR_DISTANCIAS <<<<<<<< +# ========================================================== + +""" +calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Calcula las distancias entre átomos Cα-O de segmentos PPII + consecutivos y los ángulos CA-H-O asociados. Visualiza en + PyMOL los pares de átomos que cumplen con los criterios de + distancia y ángulo definidos por el usuario. Los pares + válidos se representan con líneas discontinuas (dashed lines) + en color cian para facilitar el análisis. + +Parámetros: + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_visualizar_distancias() +---------------------------------------------------------- +""" + +def calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0): + global segmentos_ppii_global + global objetos, pares + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + if pares is None: + pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) + + calcular_angulos_ca_h_o(pares, min_ang=min_ang, max_ang=max_ang) + +# ========================================================== +# >>>>>>>> FUNCIÓN: DISTANCIAS_P <<<<<<<< +# ========================================================== + +""" +distancias_p() +---------------------------------------------------------- +Descripción: + Visualiza en PyMOL todas las distancias entre los pares + de átomos Cα-O previamente calculados y almacenados. + Cada distancia se dibuja como una línea discontinua + (dashed line) para facilitar el análisis estructural + de la proteína. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> distancias_p() +---------------------------------------------------------- +""" + +def distancias_p(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + pares = calcular_distancias_colindantes(objetos) + visualizar_distancias_pares(pares) + +# ========================================================== +# >>>>>>>> FUNCIÓN: LANZAR_INTERFAZ <<<<<<<< +# ========================================================== + +""" +lanzar_interfaz() +---------------------------------------------------------- +Descripción: + Inicia la interfaz gráfica de usuario (GUI) desarrollada + con Tkinter para facilitar la interacción con el programa. + Permite acceder a las funciones principales como cargar + archivos PDB, detectar hélices PPII, añadir hidrógenos, + eliminar solventes y generar reportes, todo desde un menú + visual. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> lanzar_interfaz() +---------------------------------------------------------- +""" + +def lanzar_interfaz(): + root = tk.Tk() + root.title("PPIIMoL: PPII Detect") + root.geometry("450x1000") + root.resizable(False, False) + + style = ttk.Style(root) + style.theme_use('classic') # Puedes probar: 'alt', 'clam', 'default', 'classic' + + main_frame = ttk.Frame(root, padding=5) + main_frame.pack(fill="both", expand=True) + + def wrapper_detectar_segmentos_ppii(): + try: + tol_phi = float(entrada_tol_phi.get()) + tol_psi = float(entrada_tol_psi.get()) + max_saltos = int(entrada_saltos.get()) # Obtener el valor del campo de saltos + if max_saltos < 0 or max_saltos > 5: # Validar rango (0-5) + raise ValueError + except ValueError: + messagebox.showerror("Error", "¡Saltos debe ser un entero entre 0 y 5!") + return + + detectar_segmentos_ppii(tol_phi=tol_phi, tol_psi=tol_psi, max_saltos=max_saltos) # Pasar el parámetro + +# En la función lanzar_interfaz(), añade este frame antes del frame de ángulos CA-H-O + # Frame para parámetros phi/psi + phi_psi_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulos phi/psi", padding=10) + phi_psi_frame.pack(fill="x", pady=5) + ttk.Label(phi_psi_frame, text="Actualmente sin la tolerancia los angulos ").pack(anchor="w") + ttk.Label(phi_psi_frame, text="por defecto que se miden son hasta: phi 75 y psi 145").pack(anchor="w") + + ttk.Label(phi_psi_frame, text="Tolerancia para phi (±°):").pack(anchor="w") + entrada_tol_phi = ttk.Entry(phi_psi_frame) + entrada_tol_phi.insert(0, "20.0") + entrada_tol_phi.pack(fill="x", pady=2) + + ttk.Label(phi_psi_frame, text="Tolerancia para psi (±°): ").pack(anchor="w") + entrada_tol_psi = ttk.Entry(phi_psi_frame) + entrada_tol_psi.insert(0, "20.0") + entrada_tol_psi.pack(fill="x", pady=2) + + + saltos_frame = ttk.LabelFrame(main_frame, text="Parámetros de saltos", padding=5) + saltos_frame.pack(fill="x", pady=3) + + ttk.Label(saltos_frame, text="Saltos permitidos (0-5):").pack(anchor="w") + entrada_saltos = ttk.Entry(saltos_frame) + entrada_saltos.insert(0, "0") # Valor por defecto: 0 saltos + entrada_saltos.pack(fill="x", pady=1) + + + def wrapper_generar_reporte_csv(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + generar_reporte_csv(segmentos_ppii_global, min_ang=min_ang, max_ang=max_ang) + #cambiar por otra funcion envoltorio diferente + + # Función envoltorio para pasar los valores + def wrapper_calcular_y_visualizar(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + calcular_y_visualizar_distancias(min_ang=min_ang, max_ang=max_ang) + + # Botones funcionales + botones = [ + ("Seleccionar archivo PDB", seleccionar_archivo), + ("Descargar proteína", descargar_molecula), + ("Eliminar solventes", eliminar_solventes), + ("Añadir hidrógenos", anadir_hidrogenos), + ("Ocultar cadenas laterales", ocultar_side_chains), + ("Guardar ángulos phi/psi en archivo", guardar_csv_angulos_phi_psi), + ("Detectar segmentos PPII y resaltarlos", wrapper_detectar_segmentos_ppii), + ("Guardar segmentos PPII en PDB", guardar_segmentos_ppii_pdb), + ("Visualizar distancias", distancias_p), + ("Angulos entre CA-O-H colindantes", wrapper_calcular_y_visualizar), + ("Generar reporte completo (CSV)", wrapper_generar_reporte_csv), + ] + + for texto, accion in botones: + ttk.Button(main_frame, text=texto, command=accion).pack(fill="x", pady=1) + + # Entradas de ángulos + ang_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulo CA-H-O", padding=3) + ang_frame.pack(fill="x", pady=3) + + ttk.Label(ang_frame, text="Ángulo mínimo (°):").pack(anchor="w") + entrada_min_ang = ttk.Entry(ang_frame) + entrada_min_ang.insert(0, "110.0") + entrada_min_ang.pack(fill="x", pady=1) + + ttk.Label(ang_frame, text="Ángulo máximo (°):").pack(anchor="w") + entrada_max_ang = ttk.Entry(ang_frame) + entrada_max_ang.insert(0, "180.0") + entrada_max_ang.pack(fill="x", pady=1) + + root.mainloop() + + +lanzar_interfaz() diff --git a/PPIIMol/PPIIMoL - copia.py b/PPIIMol/PPIIMoL - copia.py new file mode 100644 index 0000000..afe7457 --- /dev/null +++ b/PPIIMol/PPIIMoL - copia.py @@ -0,0 +1,1285 @@ +from pymol import cmd, stored +import tkinter as tk +import math +from pathlib import Path +from datetime import datetime +import os +from tkinter import filedialog, messagebox +import tkinter as tk +from tkinter import ttk, messagebox +import os +import csv + + +# Valores por defecto (se actualizarán cuando el usuario los cambie) +tol_phi_global = 20.0 +tol_psi_global = 20.0 +min_ang_global = 110.0 +max_ang_global = 180.0 + + +pdb_file = None +segmentos_ppii_global = [] # Variable global para guardar segmentos PPII detectados +localizar_atomos=None +distancias=None +angulos_v=None +objetos = None +pares = None + +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_CARPETA_RESULTADOS <<<<<<<< +# ========================================================== + +""" +obtener_carpeta_resultados(nombre_carpeta='Resultados_PyMOL') +---------------------------------------------------------- +Descripción: + Crea una carpeta con fecha actual dentro del directorio Documentos + (o equivalente) del usuario para guardar los resultados generados + por el módulo. + +Parámetros: + nombre_carpeta : str, opcional + Nombre base para la carpeta de resultados (por defecto 'Resultados_PyMOL'). + +Retorno: + pathlib.Path + Ruta completa a la carpeta creada. + +Ejemplo de uso: + >>> carpeta = obtener_carpeta_resultados("Resultados_PPIIMoL") + >>> print(carpeta) + /home/usuario/Documentos/Resultados_PPIIMoL_2025-07-05 +---------------------------------------------------------- +""" + +def obtener_carpeta_resultados(nombre_carpeta="Resultados_PyMOL"): + # Formatear fecha y hora actual (compatible con ambos sistemas) + fecha_hora = datetime.now().strftime("%Y-%m-%d") + + # Usar ~/Documents o ~/Documentos según el sistema + documentos = Path.home() / ("Documentos" if os.getenv('LANG', '').startswith('es_') else "Documents") + + # Si no existe, crear directorio en HOME directamente + if not documentos.exists(): + documentos = Path.home() + + # Crear nombre de carpeta combinado + nombre_completo = f"{nombre_carpeta}_{fecha_hora}" + carpeta_resultados = documentos / nombre_completo + + # Crear la carpeta (con parents=True por si faltan directorios padres) + carpeta_resultados.mkdir(parents=True, exist_ok=True) + + return carpeta_resultados + +# ========================================================== +# >>>>>>>> FUNCIÓN: SELECCIONAR_ARCHIVO <<<<<<<< +# ========================================================== + +""" +seleccionar_archivo() +---------------------------------------------------------- +Descripción: + Abre un cuadro de diálogo para que el usuario seleccione un archivo PDB + de entrada. La ruta seleccionada se guarda en la variable global pdb_file. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> seleccionar_archivo() +---------------------------------------------------------- +""" + +def seleccionar_archivo(): + global pdb_file + pdb_file = filedialog.askopenfilename( + title="Selecciona un archivo PDB", + filetypes=[("Archivos PDB", "*.pdb")] + ) + if pdb_file: + cmd.reinitialize() + cmd.load(pdb_file, "proteina") + cmd.hide("everything", "proteina") # Ocultar todo lo visible + cmd.show("licorice", "proteina") # Mostrar como licorice + messagebox.showinfo("Archivo cargado", f"Se cargó:\n{pdb_file}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: DESCARGAR_MOLECULA <<<<<<<< +# ========================================================== + +""" +descargar_molecula() +---------------------------------------------------------- +Descripción: + Abre una ventana emergente para permitir al usuario introducir un ID + de proteína del Protein Data Bank (PDB) y descargar automáticamente + la estructura correspondiente. Una vez descargada, la molécula se + carga en PyMOL, se ocultan todas las representaciones gráficas + actuales y se muestra la proteína usando el estilo "licorice". + La ruta o ID de la proteína descargada se guarda en la variable global + pdb_file. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> descargar_molecula() +---------------------------------------------------------- +""" + +def descargar_molecula(): + global pdb_file + def fetch_pdb(): + global pdb_file + pdb_id = entry.get().strip() + if pdb_id: + try: + cmd.reinitialize() + cmd.fetch(pdb_id, name="proteina") + cmd.hide("everything", "proteina") + cmd.show("licorice", "proteina") + # Aquí asignamos pdb_file con un valor para simular archivo cargado + pdb_file = pdb_id # Solo guardamos el ID para que funcione la lógica de “selección” + messagebox.showinfo("Descarga completa", f"Molécula {pdb_id.upper()} descargada y cargada.") + fetch_window.destroy() + except Exception as e: + messagebox.showerror("Error", f"No se pudo descargar {pdb_id}: {e}") + else: + messagebox.showwarning("Advertencia", "Por favor ingresa un ID de PDB.") + + fetch_window = tk.Toplevel() + fetch_window.title("Descargar proteína") + tk.Label(fetch_window, text="ID de la proteína (PDB):").pack(pady=5) + entry = tk.Entry(fetch_window, width=20) + entry.pack(pady=5) + tk.Button(fetch_window, text="Descargar", command=fetch_pdb).pack(pady=5) + +# ========================================================== +# >>>>>>>> FUNCIÓN: ANADIR_HIDROGENOS <<<<<<<< +# ========================================================== + +""" +anadir_hidrogenos() +---------------------------------------------------------- +Descripción: + Añade átomos de hidrógeno a todas las moléculas cargadas + en la sesión actual de PyMOL. Una vez añadidos, la + estructura se reorganiza para optimizar la visualización + y se muestra usando el estilo "licorice". Si no hay + ningún archivo PDB cargado previamente, se muestra una + advertencia al usuario. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> anadir_hidrogenos() +---------------------------------------------------------- +""" + +def anadir_hidrogenos(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + cmd.h_add("all") + cmd.sort("all extend 1") + cmd.show("licorice", "all") + messagebox.showinfo("Hidrógenos", "Hidrógenos añadidos y mostrados como licorice.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: ELIMINAR_SOLVENTES <<<<<<<< +# ========================================================== + +""" +eliminar_solventes() +---------------------------------------------------------- +Descripción: + Elimina todas las moléculas de solvente presentes en la + estructura cargada en PyMOL. Tras la eliminación, se + muestra un mensaje informativo al usuario confirmando + la acción realizada. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> eliminar_solventes() +---------------------------------------------------------- +""" + +def eliminar_solventes(): + cmd.remove("solvent") + messagebox.showinfo("Solventes", "Solventes eliminados.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: OCULTAR_SIDE_CHAINS <<<<<<<< +# ========================================================== + +""" +ocultar_side_chains() +---------------------------------------------------------- +Descripción: + Oculta todas las cadenas laterales de los residuos de + la proteína cargada en PyMOL, dejando visibles únicamente + los átomos del esqueleto principal (backbone: N, CA, C, O). + Esta función permite centrar la visualización en la + estructura principal de la proteína sin distracciones. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> ocultar_side_chains() +---------------------------------------------------------- +""" + +def ocultar_side_chains(): + cmd.hide("everything", "proteina and not name N+CA+C+O") + messagebox.showinfo("Backbone", "Cadenas laterales ocultas (solo backbone).") + +# ========================================================== +# >>>>>>>> FUNCIÓN: SEPARAR_CADENAS <<<<<<<< +# ========================================================== + +""" +separar_cadenas() +---------------------------------------------------------- +Descripción: + Separa cada cadena de la proteína cargada en PyMOL + en objetos individuales. Cada nueva cadena se guarda + como un objeto separado con un nombre identificador + en el formato "cadena_X", donde X es el identificador + de la cadena original. Esta función facilita la + manipulación y análisis individual de las cadenas. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> separar_cadenas() +---------------------------------------------------------- +""" + +def separar_cadenas(): + stored.chains = [] + cmd.iterate("proteina", "stored.chains.append(chain)") + for cadena in set(stored.chains): + nuevo_objeto = f"cadena_{cadena}" + cmd.create(nuevo_objeto, f"proteina and chain {cadena}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_ANGULOS_PHI_PSI_POR_CADENA <<<<<<<< +# ========================================================== + +""" +obtener_angulos_phi_psi_por_cadena(objeto="proteina") +---------------------------------------------------------- +Descripción: + Calcula los ángulos torsionales phi (ϕ) y psi (ψ) de cada + residuo en todas las cadenas del objeto especificado + cargado en PyMOL. Los resultados se devuelven en un + diccionario que asocia cada residuo con su nombre, número + y los valores de los ángulos phi y psi correspondientes. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el cual se + calcularán los ángulos (por defecto "proteina"). + +Retorno: + dict + Diccionario con las claves como tuplas (cadena, número + de residuo) y valores como tuplas (nombre del residuo, + ángulo phi, ángulo psi). + +Ejemplo de uso: + >>> resultados = obtener_angulos_phi_psi_por_cadena() + >>> print(resultados) +---------------------------------------------------------- +""" + +def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): + resultados = {} + chains = cmd.get_chains(objeto) + for chain in chains: + sel_ca = f"{objeto} and chain {chain} and name CA" + phipsi = cmd.get_phipsi(sel_ca) + if not phipsi: + continue + for (obj, idx), (phi, psi) in sorted(phipsi.items()): + if phi is None or psi is None: + continue + stored.info = [] + cmd.iterate(f"({obj}`{idx})", "stored.info.append((chain, resn, resi))", space={'stored': stored}) + if not stored.info: + continue + ch, resn, resi = stored.info[0] + resultados[(ch, resi)] = (resn, phi, psi) + return resultados + +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_CSV_ANGULOS_PHI_PSI <<<<<<<< +# ========================================================== + +""" +guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con los ángulos torsionales phi (ϕ) + y psi (ψ) calculados para cada residuo de la proteína + cargada en PyMOL. El archivo se guarda en una carpeta + de resultados con el nombre "angulos_phi_psi.csv" e + incluye información de la cadena, el residuo, su número + y los valores de los ángulos. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +""" + +def guardar_csv_angulos_phi_psi(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_map = obtener_angulos_phi_psi_por_cadena("proteina") + + stored.res_list = [] + cmd.iterate("proteina and name CA", "stored.res_list.append((chain, resn, resi))") + + datos_csv = [("Cadena", "Residuo", "Número", "Phi", "Psi")] + + for chain, resn, resi in sorted(stored.res_list, key=lambda x: (x[0], int(x[2]))): + key = (chain, resi) + if key in phi_map: + resn_val, phi, psi = phi_map[key] + datos_csv.append((chain, resn, resi, f"{phi:.2f}", f"{psi:.2f}")) + carpeta= obtener_carpeta_resultados() + # Guardar en CSV con separador ; + if len(datos_csv) > 1: + ruta_csv = os.path.join(os.getcwd(), carpeta/"angulos_phi_psi.csv") + with open(ruta_csv, mode="w", newline="", encoding="utf-8") as file: + writer = csv.writer(file, delimiter=";") + writer.writerows(datos_csv) + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GENERAR_REPORTE_CSV <<<<<<<< +# ========================================================== + +""" +generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con un informe detallado sobre los + segmentos de hélices PPII detectados en la proteína cargada + en PyMOL. El reporte incluye pares de átomos Cα-O colindantes, + sus distancias, los ángulos CA-H-O calculados y los valores + validados según los rangos definidos. El archivo se guarda + en una carpeta de resultados con el nombre especificado. + +Parámetros: + segmentos_ppii : list + Lista de segmentos PPII detectados en la proteína. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los + pares de átomos colindantes. Por defecto es 5.0 Å. + nombre_archivo : str, opcional + Nombre del archivo CSV que se generará. Por defecto + es "reporte_ppii.csv". + min_ang : float, opcional + Ángulo mínimo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> generar_reporte_csv(segmentos_ppii) +---------------------------------------------------------- +""" + +def generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0): + if not segmentos_ppii: + messagebox.showwarning("Advertencia", "No hay segmentos PPII detectados.") + return + + objetos_por_segmento = localizar_atomos + pares_ca_o=distancias + angulos_validos=angulos_v + if objetos_por_segmento ==None: + objetos_por_segmento = localizar_atomicos_clave_segmentos(segmentos_ppii) + if pares_ca_o is None: + pares_ca_o = calcular_distancias_colindantes(objetos_por_segmento, max_dist=max_dist) + + # PASAMOS LOS ÁNGULOS AQUÍ + if angulos_validos is None: + angulos_validos = calcular_angulos_ca_h_o(pares_ca_o, min_ang=min_ang, max_ang=max_ang) + + dist_dict = {} + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): + continue + coord_ca = cmd.get_coords(ca)[0] + coord_o = cmd.get_coords(o)[0] + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coord_ca, coord_o))) + dist_dict[(ca, o)] = dist + + ang_dict = {(ca, o): ang for ca, _, o, ang in angulos_validos} + + datos_csv = [["Átomo Cα", "Átomo O", "Distancia (Å)", "Ángulo CA-H-O (°)"]] + for (ca, o), dist in dist_dict.items(): + ang = ang_dict.get((ca, o), None) + if ang is not None: + datos_csv.append([ca, o, f"{dist:.2f}", f"{ang:.2f}"]) + + ruta_csv = obtener_carpeta_resultados() / nombre_archivo + with open(ruta_csv, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f, delimiter=';') + writer.writerows(datos_csv) + + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: LOCALIZAR_ATOMICOS_CLAVE_SEGMENTOS <<<<<<<< +# ========================================================== + +""" +localizar_atomicos_clave_segmentos(segmentos) +---------------------------------------------------------- +Descripción: + Localiza y marca en PyMOL los átomos clave (Cα y O) de + cada segmento PPII detectado. Crea pseudoátomos en las + posiciones correspondientes para facilitar la visualización + y análisis. Los átomos Cα se marcan en color azul y los O + en color rojo, agrupándolos bajo el nombre "atomos_clave". + +Parámetros: + segmentos : list + Lista de segmentos PPII detectados en la proteína. + +Retorno: + list + Lista de listas, donde cada sublista contiene los + nombres de los pseudoátomos creados para un segmento. + +Ejemplo de uso: + >>> localizar_atomicos_clave_segmentos(segmentos_ppii) +---------------------------------------------------------- +""" + +def localizar_atomicos_clave_segmentos(segmentos): + global localizar_atomos + cmd.delete("esfera_*") + objetos_por_segmento = [] + cmd.h_add() + + for idx, seg in enumerate(segmentos, start=1): + objetos_segmento = [] + + for (resn, resi, chain, _, _) in seg: + sele_base = f"proteina and chain {chain} and resi {resi}" + stored.coords = [] + + # Átomo CA (carbono alfa) + stored.ca_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name CA", + "stored.ca_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Oxígeno carbonilo + stored.o_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name O", + "stored.o_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Crear pseudoatomos con nombres consistentes + if stored.ca_coords: + x, y, z = stored.ca_coords[0] + esfera_name = f"esfera_s{idx}_CA_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("blue", esfera_name) + objetos_segmento.append(esfera_name) + + if stored.o_coords: + x, y, z = stored.o_coords[0] + esfera_name = f"esfera_s{idx}_O_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("red", esfera_name) + objetos_segmento.append(esfera_name) + + objetos_por_segmento.append(objetos_segmento) + + cmd.group("atomos_clave", "esfera_*") + localizar_atomos = objetos_por_segmento + return objetos_por_segmento + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_DISTANCIAS_COLINDANTES <<<<<<<< +# ========================================================== + +""" +calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt") +---------------------------------------------------------- +Descripción: + Calcula las distancias entre los átomos Cα y O de segmentos + PPII consecutivos o cercanos. Identifica los pares cuya + distancia es menor al valor máximo especificado. Guarda los + resultados en un archivo de texto y devuelve la lista de + pares de átomos que cumplen el criterio. + +Parámetros: + objetos_por_segmento : list + Lista de listas con los nombres de los pseudoátomos + (Cα y O) de cada segmento. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán las + distancias calculadas. Por defecto es + "distancias_colindantes.txt". + +Retorno: + list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) cuya distancia es menor a max_dist. + +Ejemplo de uso: + >>> pares = calcular_distancias_colindantes(objetos_por_segmento) +---------------------------------------------------------- +""" + +def calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt"): + global distancias + pares_ca_o = [] + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / archivo_salida + coordenadas = {} + + for segmento in objetos_por_segmento: + for obj in segmento: + coords = cmd.get_coords(obj) + if coords is None or len(coords) == 0: + continue + coordenadas[obj] = coords[0] + + def es_CA(obj): return "_CA_" in obj + def es_O(obj): return "_O_" in obj + + max_dist_sq = max_dist ** 2 + + for salto in [1, 2]: + for i in range(len(objetos_por_segmento) - salto): + seg1 = objetos_por_segmento[i] + seg2 = objetos_por_segmento[i + salto] + + ca1 = [obj for obj in seg1 if es_CA(obj)] + o2 = [obj for obj in seg2 if es_O(obj)] + ca2 = [obj for obj in seg2 if es_CA(obj)] + o1 = [obj for obj in seg1 if es_O(obj)] + + # Comparar ca1 con o2 + for ca in ca1: + coord_ca = coordenadas.get(ca) + if coord_ca is None: + continue + for o in o2: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) + + # Comparar ca2 con o1 + for ca in ca2: + coord_ca = coordenadas.get(ca) + if coord_ca is None: + continue + for o in o1: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) + + with open(archivo_salida, "w") as f: + f.write(f"Pares CA-O con distancia < {max_dist} Å:\n") + for i, (ca, o) in enumerate(pares_ca_o): + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coordenadas[ca], coordenadas[o]))) + f.write(f"{i:03d} | {ca} - {o} : {dist:.2f} Å\n") + + print(f"[DEBUG] Total de pares CA-O guardados: {len(pares_ca_o)}") + distancias= pares_ca_o + return pares_ca_o + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_ANGULOS_CA_H_O <<<<<<<< +# ========================================================== + +""" +calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt") +---------------------------------------------------------- +Descripción: + Calcula los ángulos formados entre los átomos Cα-H-O para + cada par de átomos Cα-O proporcionado. Filtra y guarda + únicamente los ángulos que estén dentro del rango definido + por min_ang y max_ang. Los resultados se almacenan en un + archivo de texto y se devuelven como una lista. + +Parámetros: + pares_ca_o : list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) sobre los que se calcularán los ángulos. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 180.0°. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán los + ángulos calculados. Por defecto es "angulos_ca_h_o.txt". + +Retorno: + list + Lista de tuplas con la información de cada ángulo válido + en el formato (Cα, H, O, ángulo). + +Ejemplo de uso: + >>> angulos = calcular_angulos_ca_h_o(pares_ca_o) +---------------------------------------------------------- +""" + +def calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt"): + global angulos_v + angulos_validos = [] + archivo_salida = obtener_carpeta_resultados() / archivo_salida + archivo_salida.parent.mkdir(parents=True, exist_ok=True) + + with open(archivo_salida, "w") as f: + f.write("CA - H - O : Ángulo (grados)\n") + + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): + continue + + # Extraer resn, resi y chain + partes = ca.split("_") + if len(partes) < 4: + f.write(f"# Nombre inválido para {ca}\n") + continue + + resn = partes[-3] + resi = partes[-2] + chain = partes[-1] + + # Buscar Hs sin usar "model" + seleccion = f"resn {resn} and resi {resi} and chain {chain} and name H*" + try: + hidrogenos = cmd.get_model(seleccion).atom + except Exception as e: + f.write(f"# Error al buscar H para {ca} ({seleccion}): {e}\n") + continue + + if not hidrogenos: + f.write(f"# No se encontraron H para {ca} (búsqueda: {seleccion})\n") + continue + + for h in hidrogenos: + h_sel = f"/{h.model}//{h.chain}/{h.resi}/{h.name}" + h_nombre = f"{h.resn}-{h.resi}{h.chain}_{h.name}" + try: + ang = cmd.get_angle(ca, h_sel, o) + if min_ang <= abs(ang) <= max_ang: + angulos_validos.append((ca, h_nombre, o, ang)) + f.write(f"{ca} - {h_nombre} - {o} : {ang:.2f}°\n") + except Exception as e: + f.write(f"# Error al calcular ángulo para {ca}, {h_sel}, {o} : {e}\n") + angulos_v= angulos_validos + return angulos_validos + + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_GUARDAR_ANGULOS <<<<<<<< +# ========================================================== + +""" +calcular_y_guardar_angulos() +---------------------------------------------------------- +Descripción: + Calcula los ángulos CA-H-O para los segmentos PPII previamente + detectados en la proteína cargada en PyMOL. Localiza los átomos + clave, obtiene las distancias colindantes y calcula los ángulos + que cumplen con el rango de validez (110°-180°). Los resultados + se guardan en un archivo de texto llamado "angulos_ca_h_o.txt" + dentro de la carpeta de resultados. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_guardar_angulos() +---------------------------------------------------------- +""" + + +def calcular_y_guardar_angulos(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + # Obtener los objetos atómicos clave + objetos = localizar_atomos + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + + # Calcular distancias para obtener las tripletas + tripletas = calcular_distancias_colindantes(objetos) + + # Calcular y guardar ángulos + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / "angulos_ca_h_o.txt" + angulos = calcular_angulos_ca_h_o(tripletas, str(archivo_salida)) # Convertir a string para PyMOL + + if angulos: + messagebox.showinfo("Éxito", f"Ángulos CA-H-O guardados en:\n{archivo_salida}") + else: + messagebox.showwarning("Aviso", "No se encontraron ángulos válidos (110°-180°)") + + +# ========================================================== +# >>>>>>>> FUNCIÓN: VISUALIZAR_DISTANCIAS_PARES <<<<<<<< +# ========================================================== +""" +visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +Descripción: + Dibuja y visualiza en PyMOL las distancias entre los pares + de átomos proporcionados. Cada distancia se representa como + una línea discontinua (dashed line) de color cian para + facilitar la identificación de interacciones potenciales. + +Parámetros: + pares_candidatos : list + Lista de tuplas con los nombres de los pares de átomos + (atomo1, atomo2) que se desean visualizar. + +Retorno: + None + +Ejemplo de uso: + >>> visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +""" + + +def visualizar_distancias_pares(pares_candidatos): + if not pares_candidatos: + messagebox.showinfo("Visualización", "No hay pares de átomos para visualizar.") + return + + cmd.delete("distancia_ppii") + + for i, (at1, at2) in enumerate(pares_candidatos, start=1): + nombre_dist = f"distancia_ppii_{i}" + cmd.distance(nombre_dist, at1, at2) + cmd.set("dash_width", 4, nombre_dist) + cmd.set("dash_length", 0.5, nombre_dist) + cmd.color("cyan", nombre_dist) + + messagebox.showinfo("Visualización", f"Visualizados {len(pares_candidatos)} pares de distancias colindantes.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: DETECTAR_SEGMENTOS_PPII <<<<<<<< +# ========================================================== + +""" +detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0) +---------------------------------------------------------- +Descripción: + Detecta segmentos de hélices de tipo poliprolina II (PPII) + en la proteína cargada en PyMOL. Analiza los ángulos + torsionales phi (ϕ) y psi (ψ) de cada residuo para + identificar regiones consecutivas compatibles con la + conformación PPII según los criterios de tolerancia + definidos. Permite configurar la longitud mínima de los + segmentos y el número máximo de saltos permitidos entre + residuos consecutivos. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el que se + realizará la detección. Por defecto es "proteina". + min_length : int, opcional + Longitud mínima (en número de residuos) que debe tener + un segmento para ser considerado PPII. Por defecto es 3. + tol_phi : float, opcional + Tolerancia en grados para el ángulo phi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + tol_psi : float, opcional + Tolerancia en grados para el ángulo psi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + max_saltos : int, opcional + Número máximo de residuos consecutivos que pueden + incumplir los criterios de PPII dentro de un segmento + sin interrumpirlo. Por defecto es 0. + +Retorno: + None + +Ejemplo de uso: + >>> detectar_segmentos_ppii() +---------------------------------------------------------- +""" + +def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0): + global segmentos_ppii_global + + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_psi_map = obtener_angulos_phi_psi_por_cadena(objeto) + lista_residuos = [] + for (chain, resi), (resn, phi, psi) in phi_psi_map.items(): + try: + resi_num = int(resi) + except: + continue + lista_residuos.append((chain, resi_num, resn, phi, psi)) + lista_residuos.sort(key=lambda x: (x[0], x[1])) + + segmentos = [] + segmento_actual = [] + saltos_restantes = max_saltos # Contador de saltos permitidos + + def en_rango_ppii(phi, psi, tol_phi=20.0, tol_psi=20.0): + # Valores IDEALES para PPII: φ = -75°, ψ = +145° + return (abs(phi - (-75)) <= tol_phi) and (abs(psi - 145) <= tol_psi) + + for i, (chain, resi, resn, phi, psi) in enumerate(lista_residuos): + if en_rango_ppii(phi, psi, tol_phi, tol_psi): + # Si cumple, reiniciamos el contador de saltos + saltos_restantes = max_saltos + if not segmento_actual: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [(resn, resi, chain, phi, psi)] + else: + # Si no cumple pero hay saltos disponibles + if segmento_actual and saltos_restantes > 0: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + saltos_restantes -= 1 + continue + + # Si no hay saltos disponibles o no es consecutivo + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [] + saltos_restantes = max_saltos + + # Añadir el último segmento si cumple con la longitud mínima + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + + if not segmentos: + messagebox.showinfo("Resultado", "No se encontraron segmentos PPII.") + return + + cmd.delete("ppii_segmento*") + + salida = f"Segmentos candidatos a hélices PPII (saltos permitidos: {max_saltos}):\n" + for idx, seg in enumerate(segmentos, start=1): + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + salida += f"\nSegmento {idx} (Cadena {chain}, residuos {start_resi}-{end_resi}, longitud {len(seg)}):\n" + for (resn, resi, _, phi, psi) in seg: + salida += f" {resn}-{resi}{chain}: (phi={phi:.1f}, psi={psi:.1f})\n" + + sel_str = f"proteina and chain {chain} and resi {start_resi}-{end_resi}" + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + cmd.create(obj_name, sel_str) + cmd.color("red", obj_name) + cmd.show("cartoon", obj_name) + + carpeta = obtener_carpeta_resultados() + ruta_archivo = carpeta / "segmentos_ppii.txt" + with open(ruta_archivo, "w") as f: + f.write(salida) + + segmentos_ppii_global = segmentos + + messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados (con {max_saltos} saltos permitidos).\n" + f"Átomos clave visualizados en PyMOL.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_SEGMENTO_PPII_PDB <<<<<<<< +# ========================================================== + +""" +guardar_segmento_ppii_pdb(segmento, nombre_archivo="segmento_ppii.pdb") +---------------------------------------------------------- +Descripción: + Guarda en un archivo PDB un segmento específico de hélice + PPII previamente detectado en la proteína cargada en PyMOL. + El archivo se genera con el nombre especificado dentro de la + carpeta de resultados. + +Parámetros: + segmento : list + Lista con los residuos que forman el segmento PPII a guardar. + nombre_archivo : str, opcional + Nombre del archivo PDB que se generará. Por defecto es + "segmento_ppii.pdb". + +Retorno: + None + +Ejemplo de uso: + >>> guardar_segmento_ppii_pdb(segmentos_ppii[0]) +---------------------------------------------------------- +""" + +def guardar_segmentos_ppii_pdb(): + global segmentos_ppii_global + carpeta = obtener_carpeta_resultados() + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + # Para cada segmento almacenado, construimos el nombre de objeto y hacemos cmd.save + count = 0 + for seg in segmentos_ppii_global: + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + # Comprobamos que el objeto efectivamente exista en PyMOL + if cmd.count_atoms(f"{obj_name}") > 0: + + filename = carpeta / f"{obj_name}.pdb" + try: + cmd.save(filename, obj_name) + count += 1 + except Exception as e: + print(f"Error guardando {obj_name}: {e}") + else: + print(f"Objeto {obj_name} no encontrado en la sesión de PyMOL.") + + if count > 0: + messagebox.showinfo("Éxito", f"Se guardaron {count} archivos PDB de segmentos PPII:\n" + f"{os.getcwd()}") + else: + messagebox.showwarning("Atención", "No se guardó ningún segmento (quizá no existan objetos en PyMOL).") + +# ========================================================== +# >>>>>>>> FUNCIÓN: CONVERTIR_A_SELECCIONES_PYMOL <<<<<<<< +# ========================================================== + +""" +convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +Descripción: + Convierte una lista de pares de átomos con sus distancias en + selecciones de PyMOL. Cada par se representa como una selección + que facilita la visualización y el análisis de las interacciones + detectadas. + +Parámetros: + pares_con_distancias : list + Lista de tuplas en el formato (atomo1, atomo2, distancia) + que serán convertidas en selecciones de PyMOL. + +Retorno: + list + Lista con los nombres de las selecciones creadas en PyMOL. + +Ejemplo de uso: + >>> selecciones = convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +""" + +def convertir_a_selecciones_pymol(pares_con_distancias): + selecciones = [] + for at1, at2, _ in pares_con_distancias: + sele1 = f"id {cmd.index(at1)[0][1]}" + sele2 = f"id {cmd.index(at2)[0][1]}" + selecciones.append((sele1, sele2)) + return selecciones + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_VISUALIZAR_DISTANCIAS <<<<<<<< +# ========================================================== + +""" +calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Calcula las distancias entre átomos Cα-O de segmentos PPII + consecutivos y los ángulos CA-H-O asociados. Visualiza en + PyMOL los pares de átomos que cumplen con los criterios de + distancia y ángulo definidos por el usuario. Los pares + válidos se representan con líneas discontinuas (dashed lines) + en color cian para facilitar el análisis. + +Parámetros: + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_visualizar_distancias() +---------------------------------------------------------- +""" + +def calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0): + global segmentos_ppii_global + global objetos, pares + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + if pares is None: + pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) + + calcular_angulos_ca_h_o(pares, min_ang=min_ang, max_ang=max_ang) + +# ========================================================== +# >>>>>>>> FUNCIÓN: DISTANCIAS_P <<<<<<<< +# ========================================================== + +""" +distancias_p() +---------------------------------------------------------- +Descripción: + Visualiza en PyMOL todas las distancias entre los pares + de átomos Cα-O previamente calculados y almacenados. + Cada distancia se dibuja como una línea discontinua + (dashed line) para facilitar el análisis estructural + de la proteína. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> distancias_p() +---------------------------------------------------------- +""" + +def distancias_p(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + pares = calcular_distancias_colindantes(objetos) + visualizar_distancias_pares(pares) + +# ========================================================== +# >>>>>>>> FUNCIÓN: LANZAR_INTERFAZ <<<<<<<< +# ========================================================== + +""" +lanzar_interfaz() +---------------------------------------------------------- +Descripción: + Inicia la interfaz gráfica de usuario (GUI) desarrollada + con Tkinter para facilitar la interacción con el programa. + Permite acceder a las funciones principales como cargar + archivos PDB, detectar hélices PPII, añadir hidrógenos, + eliminar solventes y generar reportes, todo desde un menú + visual. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> lanzar_interfaz() +---------------------------------------------------------- +""" + +def lanzar_interfaz(): + root = tk.Tk() + root.title("PPIIMoL: PPII Detect") + root.geometry("450x1000") + root.resizable(False, False) + + style = ttk.Style(root) + style.theme_use('classic') # Puedes probar: 'alt', 'clam', 'default', 'classic' + + main_frame = ttk.Frame(root, padding=5) + main_frame.pack(fill="both", expand=True) + + def wrapper_detectar_segmentos_ppii(): + try: + tol_phi = float(entrada_tol_phi.get()) + tol_psi = float(entrada_tol_psi.get()) + max_saltos = int(entrada_saltos.get()) # Obtener el valor del campo de saltos + if max_saltos < 0 or max_saltos > 5: # Validar rango (0-5) + raise ValueError + except ValueError: + messagebox.showerror("Error", "¡Saltos debe ser un entero entre 0 y 5!") + return + + detectar_segmentos_ppii(tol_phi=tol_phi, tol_psi=tol_psi, max_saltos=max_saltos) # Pasar el parámetro + +# En la función lanzar_interfaz(), añade este frame antes del frame de ángulos CA-H-O + # Frame para parámetros phi/psi + phi_psi_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulos phi/psi", padding=10) + phi_psi_frame.pack(fill="x", pady=5) + ttk.Label(phi_psi_frame, text="Actualmente sin la tolerancia los angulos ").pack(anchor="w") + ttk.Label(phi_psi_frame, text="por defecto que se miden son hasta: phi 75 y psi 145").pack(anchor="w") + + ttk.Label(phi_psi_frame, text="Tolerancia para phi (±°):").pack(anchor="w") + entrada_tol_phi = ttk.Entry(phi_psi_frame) + entrada_tol_phi.insert(0, "20.0") + entrada_tol_phi.pack(fill="x", pady=2) + + ttk.Label(phi_psi_frame, text="Tolerancia para psi (±°): ").pack(anchor="w") + entrada_tol_psi = ttk.Entry(phi_psi_frame) + entrada_tol_psi.insert(0, "20.0") + entrada_tol_psi.pack(fill="x", pady=2) + + + saltos_frame = ttk.LabelFrame(main_frame, text="Parámetros de saltos", padding=5) + saltos_frame.pack(fill="x", pady=3) + + ttk.Label(saltos_frame, text="Saltos permitidos (0-5):").pack(anchor="w") + entrada_saltos = ttk.Entry(saltos_frame) + entrada_saltos.insert(0, "0") # Valor por defecto: 0 saltos + entrada_saltos.pack(fill="x", pady=1) + + + def wrapper_generar_reporte_csv(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + generar_reporte_csv(segmentos_ppii_global, min_ang=min_ang, max_ang=max_ang) + #cambiar por otra funcion envoltorio diferente + + # Función envoltorio para pasar los valores + def wrapper_calcular_y_visualizar(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + calcular_y_visualizar_distancias(min_ang=min_ang, max_ang=max_ang) + + # Botones funcionales + botones = [ + ("Seleccionar archivo PDB", seleccionar_archivo), + ("Descargar proteína", descargar_molecula), + ("Eliminar solventes", eliminar_solventes), + ("Añadir hidrógenos", anadir_hidrogenos), + ("Ocultar cadenas laterales", ocultar_side_chains), + ("Guardar ángulos phi/psi en archivo", guardar_csv_angulos_phi_psi), + ("Detectar segmentos PPII y resaltarlos", wrapper_detectar_segmentos_ppii), + ("Guardar segmentos PPII en PDB", guardar_segmentos_ppii_pdb), + ("Visualizar distancias", distancias_p), + ("Angulos entre CA-O-H colindantes", wrapper_calcular_y_visualizar), + ("Generar reporte completo (CSV)", wrapper_generar_reporte_csv), + ] + + for texto, accion in botones: + ttk.Button(main_frame, text=texto, command=accion).pack(fill="x", pady=1) + + # Entradas de ángulos + ang_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulo CA-H-O", padding=3) + ang_frame.pack(fill="x", pady=3) + + ttk.Label(ang_frame, text="Ángulo mínimo (°):").pack(anchor="w") + entrada_min_ang = ttk.Entry(ang_frame) + entrada_min_ang.insert(0, "110.0") + entrada_min_ang.pack(fill="x", pady=1) + + ttk.Label(ang_frame, text="Ángulo máximo (°):").pack(anchor="w") + entrada_max_ang = ttk.Entry(ang_frame) + entrada_max_ang.insert(0, "180.0") + entrada_max_ang.pack(fill="x", pady=1) + + root.mainloop() + + +lanzar_interfaz() diff --git a/PPIIMol/PPIIMoL.py b/PPIIMol/PPIIMoL.py index a9d82a6..afe7457 100644 --- a/PPIIMol/PPIIMoL.py +++ b/PPIIMol/PPIIMoL.py @@ -1,15 +1,99 @@ from pymol import cmd, stored import tkinter as tk import math +from pathlib import Path +from datetime import datetime +import os from tkinter import filedialog, messagebox -from tkinter.scrolledtext import ScrolledText +import tkinter as tk +from tkinter import ttk, messagebox import os import csv + +# Valores por defecto (se actualizarán cuando el usuario los cambie) +tol_phi_global = 20.0 +tol_psi_global = 20.0 +min_ang_global = 110.0 +max_ang_global = 180.0 + + pdb_file = None segmentos_ppii_global = [] # Variable global para guardar segmentos PPII detectados +localizar_atomos=None +distancias=None +angulos_v=None +objetos = None +pares = None + +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_CARPETA_RESULTADOS <<<<<<<< +# ========================================================== + +""" +obtener_carpeta_resultados(nombre_carpeta='Resultados_PyMOL') +---------------------------------------------------------- +Descripción: + Crea una carpeta con fecha actual dentro del directorio Documentos + (o equivalente) del usuario para guardar los resultados generados + por el módulo. + +Parámetros: + nombre_carpeta : str, opcional + Nombre base para la carpeta de resultados (por defecto 'Resultados_PyMOL'). + +Retorno: + pathlib.Path + Ruta completa a la carpeta creada. + +Ejemplo de uso: + >>> carpeta = obtener_carpeta_resultados("Resultados_PPIIMoL") + >>> print(carpeta) + /home/usuario/Documentos/Resultados_PPIIMoL_2025-07-05 +---------------------------------------------------------- +""" + +def obtener_carpeta_resultados(nombre_carpeta="Resultados_PyMOL"): + # Formatear fecha y hora actual (compatible con ambos sistemas) + fecha_hora = datetime.now().strftime("%Y-%m-%d") + + # Usar ~/Documents o ~/Documentos según el sistema + documentos = Path.home() / ("Documentos" if os.getenv('LANG', '').startswith('es_') else "Documents") + + # Si no existe, crear directorio en HOME directamente + if not documentos.exists(): + documentos = Path.home() + + # Crear nombre de carpeta combinado + nombre_completo = f"{nombre_carpeta}_{fecha_hora}" + carpeta_resultados = documentos / nombre_completo + + # Crear la carpeta (con parents=True por si faltan directorios padres) + carpeta_resultados.mkdir(parents=True, exist_ok=True) + + return carpeta_resultados + +# ========================================================== +# >>>>>>>> FUNCIÓN: SELECCIONAR_ARCHIVO <<<<<<<< +# ========================================================== + +""" +seleccionar_archivo() +---------------------------------------------------------- +Descripción: + Abre un cuadro de diálogo para que el usuario seleccione un archivo PDB + de entrada. La ruta seleccionada se guarda en la variable global pdb_file. - # global para controlar si hay proteína cargada +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> seleccionar_archivo() +---------------------------------------------------------- +""" def seleccionar_archivo(): global pdb_file @@ -24,6 +108,33 @@ def seleccionar_archivo(): cmd.show("licorice", "proteina") # Mostrar como licorice messagebox.showinfo("Archivo cargado", f"Se cargó:\n{pdb_file}") +# ========================================================== +# >>>>>>>> FUNCIÓN: DESCARGAR_MOLECULA <<<<<<<< +# ========================================================== + +""" +descargar_molecula() +---------------------------------------------------------- +Descripción: + Abre una ventana emergente para permitir al usuario introducir un ID + de proteína del Protein Data Bank (PDB) y descargar automáticamente + la estructura correspondiente. Una vez descargada, la molécula se + carga en PyMOL, se ocultan todas las representaciones gráficas + actuales y se muestra la proteína usando el estilo "licorice". + La ruta o ID de la proteína descargada se guarda en la variable global + pdb_file. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> descargar_molecula() +---------------------------------------------------------- +""" + def descargar_molecula(): global pdb_file def fetch_pdb(): @@ -51,8 +162,31 @@ def fetch_pdb(): entry.pack(pady=5) tk.Button(fetch_window, text="Descargar", command=fetch_pdb).pack(pady=5) +# ========================================================== +# >>>>>>>> FUNCIÓN: ANADIR_HIDROGENOS <<<<<<<< +# ========================================================== + +""" +anadir_hidrogenos() +---------------------------------------------------------- +Descripción: + Añade átomos de hidrógeno a todas las moléculas cargadas + en la sesión actual de PyMOL. Una vez añadidos, la + estructura se reorganiza para optimizar la visualización + y se muestra usando el estilo "licorice". Si no hay + ningún archivo PDB cargado previamente, se muestra una + advertencia al usuario. +Parámetros: + Ninguno +Retorno: + None + +Ejemplo de uso: + >>> anadir_hidrogenos() +---------------------------------------------------------- +""" def anadir_hidrogenos(): if not pdb_file: @@ -63,16 +197,88 @@ def anadir_hidrogenos(): cmd.show("licorice", "all") messagebox.showinfo("Hidrógenos", "Hidrógenos añadidos y mostrados como licorice.") +# ========================================================== +# >>>>>>>> FUNCIÓN: ELIMINAR_SOLVENTES <<<<<<<< +# ========================================================== + +""" +eliminar_solventes() +---------------------------------------------------------- +Descripción: + Elimina todas las moléculas de solvente presentes en la + estructura cargada en PyMOL. Tras la eliminación, se + muestra un mensaje informativo al usuario confirmando + la acción realizada. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> eliminar_solventes() +---------------------------------------------------------- +""" def eliminar_solventes(): cmd.remove("solvent") messagebox.showinfo("Solventes", "Solventes eliminados.") +# ========================================================== +# >>>>>>>> FUNCIÓN: OCULTAR_SIDE_CHAINS <<<<<<<< +# ========================================================== + +""" +ocultar_side_chains() +---------------------------------------------------------- +Descripción: + Oculta todas las cadenas laterales de los residuos de + la proteína cargada en PyMOL, dejando visibles únicamente + los átomos del esqueleto principal (backbone: N, CA, C, O). + Esta función permite centrar la visualización en la + estructura principal de la proteína sin distracciones. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> ocultar_side_chains() +---------------------------------------------------------- +""" def ocultar_side_chains(): cmd.hide("everything", "proteina and not name N+CA+C+O") messagebox.showinfo("Backbone", "Cadenas laterales ocultas (solo backbone).") +# ========================================================== +# >>>>>>>> FUNCIÓN: SEPARAR_CADENAS <<<<<<<< +# ========================================================== + +""" +separar_cadenas() +---------------------------------------------------------- +Descripción: + Separa cada cadena de la proteína cargada en PyMOL + en objetos individuales. Cada nueva cadena se guarda + como un objeto separado con un nombre identificador + en el formato "cadena_X", donde X es el identificador + de la cadena original. Esta función facilita la + manipulación y análisis individual de las cadenas. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> separar_cadenas() +---------------------------------------------------------- +""" def separar_cadenas(): stored.chains = [] @@ -81,6 +287,36 @@ def separar_cadenas(): nuevo_objeto = f"cadena_{cadena}" cmd.create(nuevo_objeto, f"proteina and chain {cadena}") +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_ANGULOS_PHI_PSI_POR_CADENA <<<<<<<< +# ========================================================== + +""" +obtener_angulos_phi_psi_por_cadena(objeto="proteina") +---------------------------------------------------------- +Descripción: + Calcula los ángulos torsionales phi (ϕ) y psi (ψ) de cada + residuo en todas las cadenas del objeto especificado + cargado en PyMOL. Los resultados se devuelven en un + diccionario que asocia cada residuo con su nombre, número + y los valores de los ángulos phi y psi correspondientes. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el cual se + calcularán los ángulos (por defecto "proteina"). + +Retorno: + dict + Diccionario con las claves como tuplas (cadena, número + de residuo) y valores como tuplas (nombre del residuo, + ángulo phi, ángulo psi). + +Ejemplo de uso: + >>> resultados = obtener_angulos_phi_psi_por_cadena() + >>> print(resultados) +---------------------------------------------------------- +""" def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): resultados = {} @@ -101,6 +337,31 @@ def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): resultados[(ch, resi)] = (resn, phi, psi) return resultados +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_CSV_ANGULOS_PHI_PSI <<<<<<<< +# ========================================================== + +""" +guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con los ángulos torsionales phi (ϕ) + y psi (ψ) calculados para cada residuo de la proteína + cargada en PyMOL. El archivo se guarda en una carpeta + de resultados con el nombre "angulos_phi_psi.csv" e + incluye información de la cadena, el residuo, su número + y los valores de los ángulos. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +""" def guardar_csv_angulos_phi_psi(): if not pdb_file: @@ -119,16 +380,125 @@ def guardar_csv_angulos_phi_psi(): if key in phi_map: resn_val, phi, psi = phi_map[key] datos_csv.append((chain, resn, resi, f"{phi:.2f}", f"{psi:.2f}")) - + carpeta= obtener_carpeta_resultados() # Guardar en CSV con separador ; if len(datos_csv) > 1: - ruta_csv = os.path.join(os.getcwd(), "angulos_phi_psi.csv") + ruta_csv = os.path.join(os.getcwd(), carpeta/"angulos_phi_psi.csv") with open(ruta_csv, mode="w", newline="", encoding="utf-8") as file: writer = csv.writer(file, delimiter=";") writer.writerows(datos_csv) + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GENERAR_REPORTE_CSV <<<<<<<< +# ========================================================== + +""" +generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con un informe detallado sobre los + segmentos de hélices PPII detectados en la proteína cargada + en PyMOL. El reporte incluye pares de átomos Cα-O colindantes, + sus distancias, los ángulos CA-H-O calculados y los valores + validados según los rangos definidos. El archivo se guarda + en una carpeta de resultados con el nombre especificado. + +Parámetros: + segmentos_ppii : list + Lista de segmentos PPII detectados en la proteína. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los + pares de átomos colindantes. Por defecto es 5.0 Å. + nombre_archivo : str, opcional + Nombre del archivo CSV que se generará. Por defecto + es "reporte_ppii.csv". + min_ang : float, opcional + Ángulo mínimo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> generar_reporte_csv(segmentos_ppii) +---------------------------------------------------------- +""" + +def generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0): + if not segmentos_ppii: + messagebox.showwarning("Advertencia", "No hay segmentos PPII detectados.") + return + objetos_por_segmento = localizar_atomos + pares_ca_o=distancias + angulos_validos=angulos_v + if objetos_por_segmento ==None: + objetos_por_segmento = localizar_atomicos_clave_segmentos(segmentos_ppii) + if pares_ca_o is None: + pares_ca_o = calcular_distancias_colindantes(objetos_por_segmento, max_dist=max_dist) + + # PASAMOS LOS ÁNGULOS AQUÍ + if angulos_validos is None: + angulos_validos = calcular_angulos_ca_h_o(pares_ca_o, min_ang=min_ang, max_ang=max_ang) + + dist_dict = {} + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): + continue + coord_ca = cmd.get_coords(ca)[0] + coord_o = cmd.get_coords(o)[0] + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coord_ca, coord_o))) + dist_dict[(ca, o)] = dist + + ang_dict = {(ca, o): ang for ca, _, o, ang in angulos_validos} + + datos_csv = [["Átomo Cα", "Átomo O", "Distancia (Å)", "Ángulo CA-H-O (°)"]] + for (ca, o), dist in dist_dict.items(): + ang = ang_dict.get((ca, o), None) + if ang is not None: + datos_csv.append([ca, o, f"{dist:.2f}", f"{ang:.2f}"]) + + ruta_csv = obtener_carpeta_resultados() / nombre_archivo + with open(ruta_csv, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f, delimiter=';') + writer.writerows(datos_csv) + + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: LOCALIZAR_ATOMICOS_CLAVE_SEGMENTOS <<<<<<<< +# ========================================================== + +""" +localizar_atomicos_clave_segmentos(segmentos) +---------------------------------------------------------- +Descripción: + Localiza y marca en PyMOL los átomos clave (Cα y O) de + cada segmento PPII detectado. Crea pseudoátomos en las + posiciones correspondientes para facilitar la visualización + y análisis. Los átomos Cα se marcan en color azul y los O + en color rojo, agrupándolos bajo el nombre "atomos_clave". + +Parámetros: + segmentos : list + Lista de segmentos PPII detectados en la proteína. + +Retorno: + list + Lista de listas, donde cada sublista contiene los + nombres de los pseudoátomos creados para un segmento. + +Ejemplo de uso: + >>> localizar_atomicos_clave_segmentos(segmentos_ppii) +---------------------------------------------------------- +""" def localizar_atomicos_clave_segmentos(segmentos): + global localizar_atomos cmd.delete("esfera_*") objetos_por_segmento = [] cmd.h_add() @@ -158,15 +528,6 @@ def localizar_atomicos_clave_segmentos(segmentos): space={'stored': stored} ) - # Hidrógeno unido al CA (usar nombre fijo "H" en lugar del nombre real) - stored.h_coords = [] - cmd.iterate_state( - 1, - f"(neighbor ({sele_base} and name CA)) and elem H", - "stored.h_coords.append((x, y, z))", # Solo coordenadas, no nombre - space={'stored': stored} - ) - # Crear pseudoatomos con nombres consistentes if stored.ca_coords: x, y, z = stored.ca_coords[0] @@ -184,194 +545,286 @@ def localizar_atomicos_clave_segmentos(segmentos): cmd.color("red", esfera_name) objetos_segmento.append(esfera_name) - if stored.h_coords: - x, y, z = stored.h_coords[0] - # NOMBRE FIJADO COMO "H" (en lugar del nombre real del átomo) - esfera_name = f"esfera_s{idx}_H_{resn}_{resi}_{chain}" - cmd.pseudoatom(esfera_name, pos=[x, y, z]) - cmd.set("sphere_scale", 0.3, esfera_name) - cmd.color("green", esfera_name) - objetos_segmento.append(esfera_name) - objetos_por_segmento.append(objetos_segmento) cmd.group("atomos_clave", "esfera_*") + localizar_atomos = objetos_por_segmento return objetos_por_segmento - - +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_DISTANCIAS_COLINDANTES <<<<<<<< +# ========================================================== + +""" +calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt") +---------------------------------------------------------- +Descripción: + Calcula las distancias entre los átomos Cα y O de segmentos + PPII consecutivos o cercanos. Identifica los pares cuya + distancia es menor al valor máximo especificado. Guarda los + resultados en un archivo de texto y devuelve la lista de + pares de átomos que cumplen el criterio. + +Parámetros: + objetos_por_segmento : list + Lista de listas con los nombres de los pseudoátomos + (Cα y O) de cada segmento. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán las + distancias calculadas. Por defecto es + "distancias_colindantes.txt". + +Retorno: + list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) cuya distancia es menor a max_dist. + +Ejemplo de uso: + >>> pares = calcular_distancias_colindantes(objetos_por_segmento) +---------------------------------------------------------- +""" def calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt"): - tripletas_candidatas = [] - - # Precalcular coordenadas de todos los objetos + global distancias + pares_ca_o = [] + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / archivo_salida coordenadas = {} + for segmento in objetos_por_segmento: for obj in segmento: coords = cmd.get_coords(obj) - if coords is not None and len(coords) > 0: - coordenadas[obj] = coords[0] # Tomamos la primera posición - - # Funciones de utilidad - def es_CA(obj_name): return "_CA_" in obj_name - def es_O(obj_name): return "_O_" in obj_name - def es_H(obj_name): return any(x in obj_name for x in ["_HA_", "_H_H_", "_H_CA_", "_H_"]) - - def extraer_resi_chain(obj_name): - partes = obj_name.rsplit('_', 2) - if len(partes) >= 3: - return partes[1], partes[2] # resi, chain - return None, None + if coords is None or len(coords) == 0: + continue + coordenadas[obj] = coords[0] - def distancia(p1, p2): - return math.sqrt(sum((a - b) ** 2 for a, b in zip(p1, p2))) + def es_CA(obj): return "_CA_" in obj + def es_O(obj): return "_O_" in obj max_dist_sq = max_dist ** 2 - for i in range(len(objetos_por_segmento) - 1): - seg_actual = objetos_por_segmento[i] - seg_siguiente = objetos_por_segmento[i + 1] + for salto in [1, 2]: + for i in range(len(objetos_por_segmento) - salto): + seg1 = objetos_por_segmento[i] + seg2 = objetos_por_segmento[i + salto] - ca_actual = [obj for obj in seg_actual if es_CA(obj)] - o_actual = [obj for obj in seg_actual if es_O(obj)] - - ca_siguiente = [obj for obj in seg_siguiente if es_CA(obj)] - o_siguiente = [obj for obj in seg_siguiente if es_O(obj)] + ca1 = [obj for obj in seg1 if es_CA(obj)] + o2 = [obj for obj in seg2 if es_O(obj)] + ca2 = [obj for obj in seg2 if es_CA(obj)] + o1 = [obj for obj in seg1 if es_O(obj)] - mapa_h_actual = {} - mapa_h_siguiente = {} - - for obj in seg_actual: - if es_H(obj): - resi, chain = extraer_resi_chain(obj) - if resi and chain and obj in coordenadas: - mapa_h_actual[(resi, chain)] = obj - - for obj in seg_siguiente: - if es_H(obj): - resi, chain = extraer_resi_chain(obj) - if resi and chain and obj in coordenadas: - mapa_h_siguiente[(resi, chain)] = obj - - for ca in ca_actual: - if ca not in coordenadas: - continue - - resi_ca, chain_ca = extraer_resi_chain(ca) - h_asociado = mapa_h_actual.get((resi_ca, chain_ca), None) - coord_ca = coordenadas[ca] - - for o in o_siguiente: - if o not in coordenadas: + # Comparar ca1 con o2 + for ca in ca1: + coord_ca = coordenadas.get(ca) + if coord_ca is None: continue - - coord_o = coordenadas[o] - dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) - - if dist_sq < max_dist_sq: - dist = math.sqrt(dist_sq) - tripletas_candidatas.append((ca, o, h_asociado, dist)) - - for o in o_actual: - if o not in coordenadas: - continue - - coord_o = coordenadas[o] - - for ca in ca_siguiente: - if ca not in coordenadas: + for o in o2: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) + + # Comparar ca2 con o1 + for ca in ca2: + coord_ca = coordenadas.get(ca) + if coord_ca is None: continue - - resi_ca, chain_ca = extraer_resi_chain(ca) - h_asociado = mapa_h_siguiente.get((resi_ca, chain_ca), None) - coord_ca = coordenadas[ca] - - dist_sq = sum((a - b) ** 2 for a, b in zip(coord_o, coord_ca)) - - if dist_sq < max_dist_sq: - dist = math.sqrt(dist_sq) - tripletas_candidatas.append((o, ca, h_asociado, dist)) - - # Guardar reporte en archivo - with open(archivo_salida, "w") as f: - if tripletas_candidatas: - f.write(f"Tripletas colindantes con distancia < {max_dist:.1f} Å:\n") - for a1, a2, a3, dist in tripletas_candidatas: - f.write(f" {a1} - {a2} - {a3 if a3 else 'Sin H'} : {dist:.2f} Å\n") - else: - f.write(f"No se encontraron pares con distancia < {max_dist:.1f} Å.\n") - - return tripletas_candidatas - + for o in o1: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) - -def calcular_angulos_ca_h_o(tripletas_candidatas, archivo_salida="angulos_ca_h_o.txt"): - """ - Para cada tripleta (a1, a2, h, distancia) en tripletas_candidatas, - calcula el ángulo CA–H–O y lo guarda en un .txt **solo** si el ángulo está entre 110° y 180°. - - Parámetros: - - tripletas_candidatas: lista de tuplas (obj1, obj2, objH, distancia) - - archivo_salida: ruta del archivo donde se guardan los ángulos - - Retorna: - - lista de tuplas (CA, H, O, angulo_en_grados) filtrada con ángulos [110, 180] - """ - angulos = [] + with open(archivo_salida, "w") as f: + f.write(f"Pares CA-O con distancia < {max_dist} Å:\n") + for i, (ca, o) in enumerate(pares_ca_o): + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coordenadas[ca], coordenadas[o]))) + f.write(f"{i:03d} | {ca} - {o} : {dist:.2f} Å\n") + + print(f"[DEBUG] Total de pares CA-O guardados: {len(pares_ca_o)}") + distancias= pares_ca_o + return pares_ca_o + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_ANGULOS_CA_H_O <<<<<<<< +# ========================================================== + +""" +calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt") +---------------------------------------------------------- +Descripción: + Calcula los ángulos formados entre los átomos Cα-H-O para + cada par de átomos Cα-O proporcionado. Filtra y guarda + únicamente los ángulos que estén dentro del rango definido + por min_ang y max_ang. Los resultados se almacenan en un + archivo de texto y se devuelven como una lista. + +Parámetros: + pares_ca_o : list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) sobre los que se calcularán los ángulos. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 180.0°. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán los + ángulos calculados. Por defecto es "angulos_ca_h_o.txt". + +Retorno: + list + Lista de tuplas con la información de cada ángulo válido + en el formato (Cα, H, O, ángulo). + +Ejemplo de uso: + >>> angulos = calcular_angulos_ca_h_o(pares_ca_o) +---------------------------------------------------------- +""" + +def calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt"): + global angulos_v + angulos_validos = [] + archivo_salida = obtener_carpeta_resultados() / archivo_salida + archivo_salida.parent.mkdir(parents=True, exist_ok=True) with open(archivo_salida, "w") as f: f.write("CA - H - O : Ángulo (grados)\n") - for obj1, obj2, objH, dist in tripletas_candidatas: - # Saltar si no hay hidrógeno asociado - if objH is None: + + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): continue - # Determinar cuál es CA y cuál es O - if "_CA_" in obj1: - ca, o = obj1, obj2 - else: - ca, o = obj2, obj1 + # Extraer resn, resi y chain + partes = ca.split("_") + if len(partes) < 4: + f.write(f"# Nombre inválido para {ca}\n") + continue + + resn = partes[-3] + resi = partes[-2] + chain = partes[-1] - # Calcular ángulo CA–H–O + # Buscar Hs sin usar "model" + seleccion = f"resn {resn} and resi {resi} and chain {chain} and name H*" try: - ang = cmd.get_angle(ca, objH, o) + hidrogenos = cmd.get_model(seleccion).atom except Exception as e: - # Si falla PyMOL en la selección, lo informamos y seguimos - print(f"Error calculando ángulo para {ca}, {objH}, {o}: {e}") + f.write(f"# Error al buscar H para {ca} ({seleccion}): {e}\n") continue - # Filtrar solo ángulos entre 110° y 180° - if 110.0 <= ang <= 180.0: - angulos.append((ca, objH, o, ang)) - f.write(f"{ca} - {objH} - {o} : {ang:.2f}°\n") - - return angulos - + if not hidrogenos: + f.write(f"# No se encontraron H para {ca} (búsqueda: {seleccion})\n") + continue -def calcular_y_visualizar_distancias(max_dist=5.0): + for h in hidrogenos: + h_sel = f"/{h.model}//{h.chain}/{h.resi}/{h.name}" + h_nombre = f"{h.resn}-{h.resi}{h.chain}_{h.name}" + try: + ang = cmd.get_angle(ca, h_sel, o) + if min_ang <= abs(ang) <= max_ang: + angulos_validos.append((ca, h_nombre, o, ang)) + f.write(f"{ca} - {h_nombre} - {o} : {ang:.2f}°\n") + except Exception as e: + f.write(f"# Error al calcular ángulo para {ca}, {h_sel}, {o} : {e}\n") + angulos_v= angulos_validos + return angulos_validos + + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_GUARDAR_ANGULOS <<<<<<<< +# ========================================================== + +""" +calcular_y_guardar_angulos() +---------------------------------------------------------- +Descripción: + Calcula los ángulos CA-H-O para los segmentos PPII previamente + detectados en la proteína cargada en PyMOL. Localiza los átomos + clave, obtiene las distancias colindantes y calcula los ángulos + que cumplen con el rango de validez (110°-180°). Los resultados + se guardan en un archivo de texto llamado "angulos_ca_h_o.txt" + dentro de la carpeta de resultados. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_guardar_angulos() +---------------------------------------------------------- +""" + + +def calcular_y_guardar_angulos(): global segmentos_ppii_global if not segmentos_ppii_global: messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") return + + # Obtener los objetos atómicos clave + objetos = localizar_atomos + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + + # Calcular distancias para obtener las tripletas + tripletas = calcular_distancias_colindantes(objetos) + + # Calcular y guardar ángulos + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / "angulos_ca_h_o.txt" + angulos = calcular_angulos_ca_h_o(tripletas, str(archivo_salida)) # Convertir a string para PyMOL + + if angulos: + messagebox.showinfo("Éxito", f"Ángulos CA-H-O guardados en:\n{archivo_salida}") + else: + messagebox.showwarning("Aviso", "No se encontraron ángulos válidos (110°-180°)") - objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) - pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) - visualizar_distancias_pares(pares) - calcular_angulos_ca_h_o(pares) +# ========================================================== +# >>>>>>>> FUNCIÓN: VISUALIZAR_DISTANCIAS_PARES <<<<<<<< +# ========================================================== +""" +visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +Descripción: + Dibuja y visualiza en PyMOL las distancias entre los pares + de átomos proporcionados. Cada distancia se representa como + una línea discontinua (dashed line) de color cian para + facilitar la identificación de interacciones potenciales. + +Parámetros: + pares_candidatos : list + Lista de tuplas con los nombres de los pares de átomos + (atomo1, atomo2) que se desean visualizar. + +Retorno: + None + +Ejemplo de uso: + >>> visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +""" def visualizar_distancias_pares(pares_candidatos): - """ - Recibe lista de tuplas (atomo1, atomo2, atomo_H, distancia) y crea objetos distancia en PyMOL. - """ if not pares_candidatos: messagebox.showinfo("Visualización", "No hay pares de átomos para visualizar.") return cmd.delete("distancia_ppii") - for i, (at1, at2, _, dist) in enumerate(pares_candidatos, start=1): + for i, (at1, at2) in enumerate(pares_candidatos, start=1): nombre_dist = f"distancia_ppii_{i}" cmd.distance(nombre_dist, at1, at2) cmd.set("dash_width", 4, nombre_dist) @@ -380,9 +833,52 @@ def visualizar_distancias_pares(pares_candidatos): messagebox.showinfo("Visualización", f"Visualizados {len(pares_candidatos)} pares de distancias colindantes.") - - -def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol=20.0): +# ========================================================== +# >>>>>>>> FUNCIÓN: DETECTAR_SEGMENTOS_PPII <<<<<<<< +# ========================================================== + +""" +detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0) +---------------------------------------------------------- +Descripción: + Detecta segmentos de hélices de tipo poliprolina II (PPII) + en la proteína cargada en PyMOL. Analiza los ángulos + torsionales phi (ϕ) y psi (ψ) de cada residuo para + identificar regiones consecutivas compatibles con la + conformación PPII según los criterios de tolerancia + definidos. Permite configurar la longitud mínima de los + segmentos y el número máximo de saltos permitidos entre + residuos consecutivos. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el que se + realizará la detección. Por defecto es "proteina". + min_length : int, opcional + Longitud mínima (en número de residuos) que debe tener + un segmento para ser considerado PPII. Por defecto es 3. + tol_phi : float, opcional + Tolerancia en grados para el ángulo phi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + tol_psi : float, opcional + Tolerancia en grados para el ángulo psi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + max_saltos : int, opcional + Número máximo de residuos consecutivos que pueden + incumplir los criterios de PPII dentro de un segmento + sin interrumpirlo. Por defecto es 0. + +Retorno: + None + +Ejemplo de uso: + >>> detectar_segmentos_ppii() +---------------------------------------------------------- +""" + +def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0): global segmentos_ppii_global if not pdb_file: @@ -401,12 +897,16 @@ def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol=20.0): segmentos = [] segmento_actual = [] + saltos_restantes = max_saltos # Contador de saltos permitidos - def en_rango_ppii(phi, psi, tol=tol): - return abs(phi + 75) <= tol and abs(psi - 145) <= tol + def en_rango_ppii(phi, psi, tol_phi=20.0, tol_psi=20.0): + # Valores IDEALES para PPII: φ = -75°, ψ = +145° + return (abs(phi - (-75)) <= tol_phi) and (abs(psi - 145) <= tol_psi) for i, (chain, resi, resn, phi, psi) in enumerate(lista_residuos): - if en_rango_ppii(phi, psi): + if en_rango_ppii(phi, psi, tol_phi, tol_psi): + # Si cumple, reiniciamos el contador de saltos + saltos_restantes = max_saltos if not segmento_actual: segmento_actual.append((resn, resi, chain, phi, psi)) else: @@ -418,9 +918,21 @@ def en_rango_ppii(phi, psi, tol=tol): segmentos.append(segmento_actual) segmento_actual = [(resn, resi, chain, phi, psi)] else: + # Si no cumple pero hay saltos disponibles + if segmento_actual and saltos_restantes > 0: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + saltos_restantes -= 1 + continue + + # Si no hay saltos disponibles o no es consecutivo if len(segmento_actual) >= min_length: segmentos.append(segmento_actual) segmento_actual = [] + saltos_restantes = max_saltos + + # Añadir el último segmento si cumple con la longitud mínima if len(segmento_actual) >= min_length: segmentos.append(segmento_actual) @@ -430,7 +942,7 @@ def en_rango_ppii(phi, psi, tol=tol): cmd.delete("ppii_segmento*") - salida = "Segmentos candidatos a hélices PPII:\n" + salida = f"Segmentos candidatos a hélices PPII (saltos permitidos: {max_saltos}):\n" for idx, seg in enumerate(segmentos, start=1): start_resi = seg[0][1] end_resi = seg[-1][1] @@ -445,19 +957,47 @@ def en_rango_ppii(phi, psi, tol=tol): cmd.color("red", obj_name) cmd.show("cartoon", obj_name) - ruta_archivo = os.path.join(os.getcwd(), "segmentos_ppii.txt") + carpeta = obtener_carpeta_resultados() + ruta_archivo = carpeta / "segmentos_ppii.txt" with open(ruta_archivo, "w") as f: f.write(salida) - # Guardar global segmentos_ppii_global = segmentos - messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados, resaltados.\n" - f"Átomos clave también visualizados en PyMOL.") - + messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados (con {max_saltos} saltos permitidos).\n" + f"Átomos clave visualizados en PyMOL.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_SEGMENTO_PPII_PDB <<<<<<<< +# ========================================================== + +""" +guardar_segmento_ppii_pdb(segmento, nombre_archivo="segmento_ppii.pdb") +---------------------------------------------------------- +Descripción: + Guarda en un archivo PDB un segmento específico de hélice + PPII previamente detectado en la proteína cargada en PyMOL. + El archivo se genera con el nombre especificado dentro de la + carpeta de resultados. + +Parámetros: + segmento : list + Lista con los residuos que forman el segmento PPII a guardar. + nombre_archivo : str, opcional + Nombre del archivo PDB que se generará. Por defecto es + "segmento_ppii.pdb". + +Retorno: + None + +Ejemplo de uso: + >>> guardar_segmento_ppii_pdb(segmentos_ppii[0]) +---------------------------------------------------------- +""" def guardar_segmentos_ppii_pdb(): global segmentos_ppii_global + carpeta = obtener_carpeta_resultados() if not segmentos_ppii_global: messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") return @@ -471,7 +1011,8 @@ def guardar_segmentos_ppii_pdb(): obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" # Comprobamos que el objeto efectivamente exista en PyMOL if cmd.count_atoms(f"{obj_name}") > 0: - filename = os.path.join(os.getcwd(), f"{obj_name}.pdb") + + filename = carpeta / f"{obj_name}.pdb" try: cmd.save(filename, obj_name) count += 1 @@ -486,6 +1027,32 @@ def guardar_segmentos_ppii_pdb(): else: messagebox.showwarning("Atención", "No se guardó ningún segmento (quizá no existan objetos en PyMOL).") +# ========================================================== +# >>>>>>>> FUNCIÓN: CONVERTIR_A_SELECCIONES_PYMOL <<<<<<<< +# ========================================================== + +""" +convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +Descripción: + Convierte una lista de pares de átomos con sus distancias en + selecciones de PyMOL. Cada par se representa como una selección + que facilita la visualización y el análisis de las interacciones + detectadas. + +Parámetros: + pares_con_distancias : list + Lista de tuplas en el formato (atomo1, atomo2, distancia) + que serán convertidas en selecciones de PyMOL. + +Retorno: + list + Lista con los nombres de las selecciones creadas en PyMOL. + +Ejemplo de uso: + >>> selecciones = convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +""" def convertir_a_selecciones_pymol(pares_con_distancias): selecciones = [] @@ -495,22 +1062,222 @@ def convertir_a_selecciones_pymol(pares_con_distancias): selecciones.append((sele1, sele2)) return selecciones +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_VISUALIZAR_DISTANCIAS <<<<<<<< +# ========================================================== + +""" +calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Calcula las distancias entre átomos Cα-O de segmentos PPII + consecutivos y los ángulos CA-H-O asociados. Visualiza en + PyMOL los pares de átomos que cumplen con los criterios de + distancia y ángulo definidos por el usuario. Los pares + válidos se representan con líneas discontinuas (dashed lines) + en color cian para facilitar el análisis. + +Parámetros: + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_visualizar_distancias() +---------------------------------------------------------- +""" + +def calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0): + global segmentos_ppii_global + global objetos, pares + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + if pares is None: + pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) + + calcular_angulos_ca_h_o(pares, min_ang=min_ang, max_ang=max_ang) + +# ========================================================== +# >>>>>>>> FUNCIÓN: DISTANCIAS_P <<<<<<<< +# ========================================================== + +""" +distancias_p() +---------------------------------------------------------- +Descripción: + Visualiza en PyMOL todas las distancias entre los pares + de átomos Cα-O previamente calculados y almacenados. + Cada distancia se dibuja como una línea discontinua + (dashed line) para facilitar el análisis estructural + de la proteína. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> distancias_p() +---------------------------------------------------------- +""" + +def distancias_p(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + pares = calcular_distancias_colindantes(objetos) + visualizar_distancias_pares(pares) +# ========================================================== +# >>>>>>>> FUNCIÓN: LANZAR_INTERFAZ <<<<<<<< +# ========================================================== +""" +lanzar_interfaz() +---------------------------------------------------------- +Descripción: + Inicia la interfaz gráfica de usuario (GUI) desarrollada + con Tkinter para facilitar la interacción con el programa. + Permite acceder a las funciones principales como cargar + archivos PDB, detectar hélices PPII, añadir hidrógenos, + eliminar solventes y generar reportes, todo desde un menú + visual. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> lanzar_interfaz() +---------------------------------------------------------- +""" def lanzar_interfaz(): root = tk.Tk() - root.title("Análisis de Proteína en PyMOL") - tk.Button(root, text="Seleccionar archivo PDB", command=seleccionar_archivo, width=40).pack(pady=10) - tk.Button(root, text="Descargar proteina", command=descargar_molecula, width=40).pack(pady=10) - tk.Button(root, text="Añadir hidrógenos", command=anadir_hidrogenos, width=40).pack(pady=10) - tk.Button(root, text="Eliminar solventes", command=eliminar_solventes, width=40).pack(pady=10) - tk.Button(root, text="Ocultar cadenas laterales", command=ocultar_side_chains, width=40).pack(pady=10) - tk.Button(root, text="Guardar ángulos phi/psi en archivo", command=guardar_csv_angulos_phi_psi, width=40).pack(pady=10) - tk.Button(root, text="Detectar segmentos PPII y resaltarlos", command=detectar_segmentos_ppii, width=40).pack(pady=10) - tk.Button(root, text="Guardar segmentos PPII en PDB", command=guardar_segmentos_ppii_pdb, width=40).pack(pady=10) - tk.Button(root, text="Calcular y visualizar distancias entre CA-O colindantes",command=calcular_y_visualizar_distancias, width=40).pack(pady=10) + root.title("PPIIMoL: PPII Detect") + root.geometry("450x1000") + root.resizable(False, False) + + style = ttk.Style(root) + style.theme_use('classic') # Puedes probar: 'alt', 'clam', 'default', 'classic' + main_frame = ttk.Frame(root, padding=5) + main_frame.pack(fill="both", expand=True) + + def wrapper_detectar_segmentos_ppii(): + try: + tol_phi = float(entrada_tol_phi.get()) + tol_psi = float(entrada_tol_psi.get()) + max_saltos = int(entrada_saltos.get()) # Obtener el valor del campo de saltos + if max_saltos < 0 or max_saltos > 5: # Validar rango (0-5) + raise ValueError + except ValueError: + messagebox.showerror("Error", "¡Saltos debe ser un entero entre 0 y 5!") + return + + detectar_segmentos_ppii(tol_phi=tol_phi, tol_psi=tol_psi, max_saltos=max_saltos) # Pasar el parámetro + +# En la función lanzar_interfaz(), añade este frame antes del frame de ángulos CA-H-O + # Frame para parámetros phi/psi + phi_psi_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulos phi/psi", padding=10) + phi_psi_frame.pack(fill="x", pady=5) + ttk.Label(phi_psi_frame, text="Actualmente sin la tolerancia los angulos ").pack(anchor="w") + ttk.Label(phi_psi_frame, text="por defecto que se miden son hasta: phi 75 y psi 145").pack(anchor="w") + + ttk.Label(phi_psi_frame, text="Tolerancia para phi (±°):").pack(anchor="w") + entrada_tol_phi = ttk.Entry(phi_psi_frame) + entrada_tol_phi.insert(0, "20.0") + entrada_tol_phi.pack(fill="x", pady=2) + + ttk.Label(phi_psi_frame, text="Tolerancia para psi (±°): ").pack(anchor="w") + entrada_tol_psi = ttk.Entry(phi_psi_frame) + entrada_tol_psi.insert(0, "20.0") + entrada_tol_psi.pack(fill="x", pady=2) + + + saltos_frame = ttk.LabelFrame(main_frame, text="Parámetros de saltos", padding=5) + saltos_frame.pack(fill="x", pady=3) + + ttk.Label(saltos_frame, text="Saltos permitidos (0-5):").pack(anchor="w") + entrada_saltos = ttk.Entry(saltos_frame) + entrada_saltos.insert(0, "0") # Valor por defecto: 0 saltos + entrada_saltos.pack(fill="x", pady=1) + + + def wrapper_generar_reporte_csv(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + generar_reporte_csv(segmentos_ppii_global, min_ang=min_ang, max_ang=max_ang) + #cambiar por otra funcion envoltorio diferente + + # Función envoltorio para pasar los valores + def wrapper_calcular_y_visualizar(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + calcular_y_visualizar_distancias(min_ang=min_ang, max_ang=max_ang) + + # Botones funcionales + botones = [ + ("Seleccionar archivo PDB", seleccionar_archivo), + ("Descargar proteína", descargar_molecula), + ("Eliminar solventes", eliminar_solventes), + ("Añadir hidrógenos", anadir_hidrogenos), + ("Ocultar cadenas laterales", ocultar_side_chains), + ("Guardar ángulos phi/psi en archivo", guardar_csv_angulos_phi_psi), + ("Detectar segmentos PPII y resaltarlos", wrapper_detectar_segmentos_ppii), + ("Guardar segmentos PPII en PDB", guardar_segmentos_ppii_pdb), + ("Visualizar distancias", distancias_p), + ("Angulos entre CA-O-H colindantes", wrapper_calcular_y_visualizar), + ("Generar reporte completo (CSV)", wrapper_generar_reporte_csv), + ] + + for texto, accion in botones: + ttk.Button(main_frame, text=texto, command=accion).pack(fill="x", pady=1) + + # Entradas de ángulos + ang_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulo CA-H-O", padding=3) + ang_frame.pack(fill="x", pady=3) + + ttk.Label(ang_frame, text="Ángulo mínimo (°):").pack(anchor="w") + entrada_min_ang = ttk.Entry(ang_frame) + entrada_min_ang.insert(0, "110.0") + entrada_min_ang.pack(fill="x", pady=1) + + ttk.Label(ang_frame, text="Ángulo máximo (°):").pack(anchor="w") + entrada_max_ang = ttk.Entry(ang_frame) + entrada_max_ang.insert(0, "180.0") + entrada_max_ang.pack(fill="x", pady=1) root.mainloop() From ccb33bfc77325d4cea4ac0fd48b5b0f16bd54544 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Mon, 7 Jul 2025 22:57:10 +0200 Subject: [PATCH 03/19] =?UTF-8?q?A=C3=B1adir=20.gitattributes=20y=20.gitig?= =?UTF-8?q?nore=20para=20normalizar=20line=20endings=20y=20excluir=20tempo?= =?UTF-8?q?rales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | Bin 0 -> 374 bytes .gitignore | Bin 452 -> 628 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..89b50d9cc2dd63a0307751d67f32b0d16bcefe2d GIT binary patch literal 374 zcmaivI}XA?3`D0!;tng(QE&rVDoQS}3CSvmd;}>7*G>;SFDMAaio!pQ$1}d)7INCC z)k#-*veQwO8WnT}bswjoGxL^Br!MY!x?E$#1N^yWT51J4fb?)LWV_LFaZQxdP7Ukd zPXFSkFPWmb-{IP$^>m*_zt&eMo9mof6HedQ+jFDvfe(|{10SWZfJT|%b^Mt9z)bT2 Dl*K)Q literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index 9ac0016a28f900b475569e18ddfb79830f204a6a..225306c4a30b1c6752f406636e9b2c6e2a818531 100644 GIT binary patch literal 628 zcmaix%}T^T5QOV2_znx+Jj7=ZJt_(-UPNSn#x zuKD^r(M0EJwa|-7ZB;9Ryz?(K18)@65gA<)PjpMQqr28d$2!szUjycTvxJ14rP`^A zRC}OObN_J{2*f(f8^l5bDS~t4-wNGS@@7aM0eXF0W!H2ot(CPZ$+#L*S-1COR zK6&>F4BbBN@C%Th;z&UDw0+4W;djS-pk>ta;W2Puob+%SKR8+%@7V>)=o@ra)IkEZSf zxnZy*LKURIl4m^%G7%HJsT&5&4s>J+(w3p(`eRVLy)I73s=`p#40Z`oAmr9CS7Ht& zLn4`qYhG_p3YWanw=!&ID^{&#<0))@AB#T^b|`3(ubf|k(bia+BEPpFltHN7`Dp-8 znJS7#dcMlPiK>+8_rvQTrP9vOB-4^C_s*KbSn{2D`TgI0=>&_%CV$vJT+%dsaD?U? D7>j&W From de599dc87b8ee5a8f91c91b5d799eb8494581751 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Mon, 7 Jul 2025 23:50:31 +0200 Subject: [PATCH 04/19] Eliminar carpeta PPIIMoL duplicada (repositorio independiente ya disponible) --- PPIIMol/PPIIMoL - copia.py | 1285 ------------------------------------ PPIIMol/PPIIMoL.py | 1285 ------------------------------------ PPIIMol/README_EN.md | 47 -- PPIIMol/README_ES.md | 47 -- 4 files changed, 2664 deletions(-) delete mode 100644 PPIIMol/PPIIMoL - copia.py delete mode 100644 PPIIMol/PPIIMoL.py delete mode 100644 PPIIMol/README_EN.md delete mode 100644 PPIIMol/README_ES.md diff --git a/PPIIMol/PPIIMoL - copia.py b/PPIIMol/PPIIMoL - copia.py deleted file mode 100644 index afe7457..0000000 --- a/PPIIMol/PPIIMoL - copia.py +++ /dev/null @@ -1,1285 +0,0 @@ -from pymol import cmd, stored -import tkinter as tk -import math -from pathlib import Path -from datetime import datetime -import os -from tkinter import filedialog, messagebox -import tkinter as tk -from tkinter import ttk, messagebox -import os -import csv - - -# Valores por defecto (se actualizarán cuando el usuario los cambie) -tol_phi_global = 20.0 -tol_psi_global = 20.0 -min_ang_global = 110.0 -max_ang_global = 180.0 - - -pdb_file = None -segmentos_ppii_global = [] # Variable global para guardar segmentos PPII detectados -localizar_atomos=None -distancias=None -angulos_v=None -objetos = None -pares = None - -# ========================================================== -# >>>>>>>> FUNCIÓN: OBTENER_CARPETA_RESULTADOS <<<<<<<< -# ========================================================== - -""" -obtener_carpeta_resultados(nombre_carpeta='Resultados_PyMOL') ----------------------------------------------------------- -Descripción: - Crea una carpeta con fecha actual dentro del directorio Documentos - (o equivalente) del usuario para guardar los resultados generados - por el módulo. - -Parámetros: - nombre_carpeta : str, opcional - Nombre base para la carpeta de resultados (por defecto 'Resultados_PyMOL'). - -Retorno: - pathlib.Path - Ruta completa a la carpeta creada. - -Ejemplo de uso: - >>> carpeta = obtener_carpeta_resultados("Resultados_PPIIMoL") - >>> print(carpeta) - /home/usuario/Documentos/Resultados_PPIIMoL_2025-07-05 ----------------------------------------------------------- -""" - -def obtener_carpeta_resultados(nombre_carpeta="Resultados_PyMOL"): - # Formatear fecha y hora actual (compatible con ambos sistemas) - fecha_hora = datetime.now().strftime("%Y-%m-%d") - - # Usar ~/Documents o ~/Documentos según el sistema - documentos = Path.home() / ("Documentos" if os.getenv('LANG', '').startswith('es_') else "Documents") - - # Si no existe, crear directorio en HOME directamente - if not documentos.exists(): - documentos = Path.home() - - # Crear nombre de carpeta combinado - nombre_completo = f"{nombre_carpeta}_{fecha_hora}" - carpeta_resultados = documentos / nombre_completo - - # Crear la carpeta (con parents=True por si faltan directorios padres) - carpeta_resultados.mkdir(parents=True, exist_ok=True) - - return carpeta_resultados - -# ========================================================== -# >>>>>>>> FUNCIÓN: SELECCIONAR_ARCHIVO <<<<<<<< -# ========================================================== - -""" -seleccionar_archivo() ----------------------------------------------------------- -Descripción: - Abre un cuadro de diálogo para que el usuario seleccione un archivo PDB - de entrada. La ruta seleccionada se guarda en la variable global pdb_file. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> seleccionar_archivo() ----------------------------------------------------------- -""" - -def seleccionar_archivo(): - global pdb_file - pdb_file = filedialog.askopenfilename( - title="Selecciona un archivo PDB", - filetypes=[("Archivos PDB", "*.pdb")] - ) - if pdb_file: - cmd.reinitialize() - cmd.load(pdb_file, "proteina") - cmd.hide("everything", "proteina") # Ocultar todo lo visible - cmd.show("licorice", "proteina") # Mostrar como licorice - messagebox.showinfo("Archivo cargado", f"Se cargó:\n{pdb_file}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: DESCARGAR_MOLECULA <<<<<<<< -# ========================================================== - -""" -descargar_molecula() ----------------------------------------------------------- -Descripción: - Abre una ventana emergente para permitir al usuario introducir un ID - de proteína del Protein Data Bank (PDB) y descargar automáticamente - la estructura correspondiente. Una vez descargada, la molécula se - carga en PyMOL, se ocultan todas las representaciones gráficas - actuales y se muestra la proteína usando el estilo "licorice". - La ruta o ID de la proteína descargada se guarda en la variable global - pdb_file. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> descargar_molecula() ----------------------------------------------------------- -""" - -def descargar_molecula(): - global pdb_file - def fetch_pdb(): - global pdb_file - pdb_id = entry.get().strip() - if pdb_id: - try: - cmd.reinitialize() - cmd.fetch(pdb_id, name="proteina") - cmd.hide("everything", "proteina") - cmd.show("licorice", "proteina") - # Aquí asignamos pdb_file con un valor para simular archivo cargado - pdb_file = pdb_id # Solo guardamos el ID para que funcione la lógica de “selección” - messagebox.showinfo("Descarga completa", f"Molécula {pdb_id.upper()} descargada y cargada.") - fetch_window.destroy() - except Exception as e: - messagebox.showerror("Error", f"No se pudo descargar {pdb_id}: {e}") - else: - messagebox.showwarning("Advertencia", "Por favor ingresa un ID de PDB.") - - fetch_window = tk.Toplevel() - fetch_window.title("Descargar proteína") - tk.Label(fetch_window, text="ID de la proteína (PDB):").pack(pady=5) - entry = tk.Entry(fetch_window, width=20) - entry.pack(pady=5) - tk.Button(fetch_window, text="Descargar", command=fetch_pdb).pack(pady=5) - -# ========================================================== -# >>>>>>>> FUNCIÓN: ANADIR_HIDROGENOS <<<<<<<< -# ========================================================== - -""" -anadir_hidrogenos() ----------------------------------------------------------- -Descripción: - Añade átomos de hidrógeno a todas las moléculas cargadas - en la sesión actual de PyMOL. Una vez añadidos, la - estructura se reorganiza para optimizar la visualización - y se muestra usando el estilo "licorice". Si no hay - ningún archivo PDB cargado previamente, se muestra una - advertencia al usuario. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> anadir_hidrogenos() ----------------------------------------------------------- -""" - -def anadir_hidrogenos(): - if not pdb_file: - messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") - return - cmd.h_add("all") - cmd.sort("all extend 1") - cmd.show("licorice", "all") - messagebox.showinfo("Hidrógenos", "Hidrógenos añadidos y mostrados como licorice.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: ELIMINAR_SOLVENTES <<<<<<<< -# ========================================================== - -""" -eliminar_solventes() ----------------------------------------------------------- -Descripción: - Elimina todas las moléculas de solvente presentes en la - estructura cargada en PyMOL. Tras la eliminación, se - muestra un mensaje informativo al usuario confirmando - la acción realizada. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> eliminar_solventes() ----------------------------------------------------------- -""" - -def eliminar_solventes(): - cmd.remove("solvent") - messagebox.showinfo("Solventes", "Solventes eliminados.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: OCULTAR_SIDE_CHAINS <<<<<<<< -# ========================================================== - -""" -ocultar_side_chains() ----------------------------------------------------------- -Descripción: - Oculta todas las cadenas laterales de los residuos de - la proteína cargada en PyMOL, dejando visibles únicamente - los átomos del esqueleto principal (backbone: N, CA, C, O). - Esta función permite centrar la visualización en la - estructura principal de la proteína sin distracciones. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> ocultar_side_chains() ----------------------------------------------------------- -""" - -def ocultar_side_chains(): - cmd.hide("everything", "proteina and not name N+CA+C+O") - messagebox.showinfo("Backbone", "Cadenas laterales ocultas (solo backbone).") - -# ========================================================== -# >>>>>>>> FUNCIÓN: SEPARAR_CADENAS <<<<<<<< -# ========================================================== - -""" -separar_cadenas() ----------------------------------------------------------- -Descripción: - Separa cada cadena de la proteína cargada en PyMOL - en objetos individuales. Cada nueva cadena se guarda - como un objeto separado con un nombre identificador - en el formato "cadena_X", donde X es el identificador - de la cadena original. Esta función facilita la - manipulación y análisis individual de las cadenas. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> separar_cadenas() ----------------------------------------------------------- -""" - -def separar_cadenas(): - stored.chains = [] - cmd.iterate("proteina", "stored.chains.append(chain)") - for cadena in set(stored.chains): - nuevo_objeto = f"cadena_{cadena}" - cmd.create(nuevo_objeto, f"proteina and chain {cadena}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: OBTENER_ANGULOS_PHI_PSI_POR_CADENA <<<<<<<< -# ========================================================== - -""" -obtener_angulos_phi_psi_por_cadena(objeto="proteina") ----------------------------------------------------------- -Descripción: - Calcula los ángulos torsionales phi (ϕ) y psi (ψ) de cada - residuo en todas las cadenas del objeto especificado - cargado en PyMOL. Los resultados se devuelven en un - diccionario que asocia cada residuo con su nombre, número - y los valores de los ángulos phi y psi correspondientes. - -Parámetros: - objeto : str, opcional - Nombre del objeto cargado en PyMOL sobre el cual se - calcularán los ángulos (por defecto "proteina"). - -Retorno: - dict - Diccionario con las claves como tuplas (cadena, número - de residuo) y valores como tuplas (nombre del residuo, - ángulo phi, ángulo psi). - -Ejemplo de uso: - >>> resultados = obtener_angulos_phi_psi_por_cadena() - >>> print(resultados) ----------------------------------------------------------- -""" - -def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): - resultados = {} - chains = cmd.get_chains(objeto) - for chain in chains: - sel_ca = f"{objeto} and chain {chain} and name CA" - phipsi = cmd.get_phipsi(sel_ca) - if not phipsi: - continue - for (obj, idx), (phi, psi) in sorted(phipsi.items()): - if phi is None or psi is None: - continue - stored.info = [] - cmd.iterate(f"({obj}`{idx})", "stored.info.append((chain, resn, resi))", space={'stored': stored}) - if not stored.info: - continue - ch, resn, resi = stored.info[0] - resultados[(ch, resi)] = (resn, phi, psi) - return resultados - -# ========================================================== -# >>>>>>>> FUNCIÓN: GUARDAR_CSV_ANGULOS_PHI_PSI <<<<<<<< -# ========================================================== - -""" -guardar_csv_angulos_phi_psi() ----------------------------------------------------------- -Descripción: - Genera un archivo CSV con los ángulos torsionales phi (ϕ) - y psi (ψ) calculados para cada residuo de la proteína - cargada en PyMOL. El archivo se guarda en una carpeta - de resultados con el nombre "angulos_phi_psi.csv" e - incluye información de la cadena, el residuo, su número - y los valores de los ángulos. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> guardar_csv_angulos_phi_psi() ----------------------------------------------------------- -""" - -def guardar_csv_angulos_phi_psi(): - if not pdb_file: - messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") - return - - phi_map = obtener_angulos_phi_psi_por_cadena("proteina") - - stored.res_list = [] - cmd.iterate("proteina and name CA", "stored.res_list.append((chain, resn, resi))") - - datos_csv = [("Cadena", "Residuo", "Número", "Phi", "Psi")] - - for chain, resn, resi in sorted(stored.res_list, key=lambda x: (x[0], int(x[2]))): - key = (chain, resi) - if key in phi_map: - resn_val, phi, psi = phi_map[key] - datos_csv.append((chain, resn, resi, f"{phi:.2f}", f"{psi:.2f}")) - carpeta= obtener_carpeta_resultados() - # Guardar en CSV con separador ; - if len(datos_csv) > 1: - ruta_csv = os.path.join(os.getcwd(), carpeta/"angulos_phi_psi.csv") - with open(ruta_csv, mode="w", newline="", encoding="utf-8") as file: - writer = csv.writer(file, delimiter=";") - writer.writerows(datos_csv) - messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: GENERAR_REPORTE_CSV <<<<<<<< -# ========================================================== - -""" -generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0) ----------------------------------------------------------- -Descripción: - Genera un archivo CSV con un informe detallado sobre los - segmentos de hélices PPII detectados en la proteína cargada - en PyMOL. El reporte incluye pares de átomos Cα-O colindantes, - sus distancias, los ángulos CA-H-O calculados y los valores - validados según los rangos definidos. El archivo se guarda - en una carpeta de resultados con el nombre especificado. - -Parámetros: - segmentos_ppii : list - Lista de segmentos PPII detectados en la proteína. - max_dist : float, opcional - Distancia máxima (en Ångström) para considerar los - pares de átomos colindantes. Por defecto es 5.0 Å. - nombre_archivo : str, opcional - Nombre del archivo CSV que se generará. Por defecto - es "reporte_ppii.csv". - min_ang : float, opcional - Ángulo mínimo en grados para filtrar los ángulos CA-H-O - válidos. Por defecto es 110.0°. - max_ang : float, opcional - Ángulo máximo en grados para filtrar los ángulos CA-H-O - válidos. Por defecto es 180.0°. - -Retorno: - None - -Ejemplo de uso: - >>> generar_reporte_csv(segmentos_ppii) ----------------------------------------------------------- -""" - -def generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0): - if not segmentos_ppii: - messagebox.showwarning("Advertencia", "No hay segmentos PPII detectados.") - return - - objetos_por_segmento = localizar_atomos - pares_ca_o=distancias - angulos_validos=angulos_v - if objetos_por_segmento ==None: - objetos_por_segmento = localizar_atomicos_clave_segmentos(segmentos_ppii) - if pares_ca_o is None: - pares_ca_o = calcular_distancias_colindantes(objetos_por_segmento, max_dist=max_dist) - - # PASAMOS LOS ÁNGULOS AQUÍ - if angulos_validos is None: - angulos_validos = calcular_angulos_ca_h_o(pares_ca_o, min_ang=min_ang, max_ang=max_ang) - - dist_dict = {} - for ca, o in pares_ca_o: - if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): - continue - coord_ca = cmd.get_coords(ca)[0] - coord_o = cmd.get_coords(o)[0] - dist = math.sqrt(sum((a - b)**2 for a, b in zip(coord_ca, coord_o))) - dist_dict[(ca, o)] = dist - - ang_dict = {(ca, o): ang for ca, _, o, ang in angulos_validos} - - datos_csv = [["Átomo Cα", "Átomo O", "Distancia (Å)", "Ángulo CA-H-O (°)"]] - for (ca, o), dist in dist_dict.items(): - ang = ang_dict.get((ca, o), None) - if ang is not None: - datos_csv.append([ca, o, f"{dist:.2f}", f"{ang:.2f}"]) - - ruta_csv = obtener_carpeta_resultados() / nombre_archivo - with open(ruta_csv, 'w', newline='', encoding='utf-8') as f: - writer = csv.writer(f, delimiter=';') - writer.writerows(datos_csv) - - messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: LOCALIZAR_ATOMICOS_CLAVE_SEGMENTOS <<<<<<<< -# ========================================================== - -""" -localizar_atomicos_clave_segmentos(segmentos) ----------------------------------------------------------- -Descripción: - Localiza y marca en PyMOL los átomos clave (Cα y O) de - cada segmento PPII detectado. Crea pseudoátomos en las - posiciones correspondientes para facilitar la visualización - y análisis. Los átomos Cα se marcan en color azul y los O - en color rojo, agrupándolos bajo el nombre "atomos_clave". - -Parámetros: - segmentos : list - Lista de segmentos PPII detectados en la proteína. - -Retorno: - list - Lista de listas, donde cada sublista contiene los - nombres de los pseudoátomos creados para un segmento. - -Ejemplo de uso: - >>> localizar_atomicos_clave_segmentos(segmentos_ppii) ----------------------------------------------------------- -""" - -def localizar_atomicos_clave_segmentos(segmentos): - global localizar_atomos - cmd.delete("esfera_*") - objetos_por_segmento = [] - cmd.h_add() - - for idx, seg in enumerate(segmentos, start=1): - objetos_segmento = [] - - for (resn, resi, chain, _, _) in seg: - sele_base = f"proteina and chain {chain} and resi {resi}" - stored.coords = [] - - # Átomo CA (carbono alfa) - stored.ca_coords = [] - cmd.iterate_state( - 1, - f"{sele_base} and name CA", - "stored.ca_coords.append((x, y, z))", - space={'stored': stored} - ) - - # Oxígeno carbonilo - stored.o_coords = [] - cmd.iterate_state( - 1, - f"{sele_base} and name O", - "stored.o_coords.append((x, y, z))", - space={'stored': stored} - ) - - # Crear pseudoatomos con nombres consistentes - if stored.ca_coords: - x, y, z = stored.ca_coords[0] - esfera_name = f"esfera_s{idx}_CA_{resn}_{resi}_{chain}" - cmd.pseudoatom(esfera_name, pos=[x, y, z]) - cmd.set("sphere_scale", 0.3, esfera_name) - cmd.color("blue", esfera_name) - objetos_segmento.append(esfera_name) - - if stored.o_coords: - x, y, z = stored.o_coords[0] - esfera_name = f"esfera_s{idx}_O_{resn}_{resi}_{chain}" - cmd.pseudoatom(esfera_name, pos=[x, y, z]) - cmd.set("sphere_scale", 0.3, esfera_name) - cmd.color("red", esfera_name) - objetos_segmento.append(esfera_name) - - objetos_por_segmento.append(objetos_segmento) - - cmd.group("atomos_clave", "esfera_*") - localizar_atomos = objetos_por_segmento - return objetos_por_segmento - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_DISTANCIAS_COLINDANTES <<<<<<<< -# ========================================================== - -""" -calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt") ----------------------------------------------------------- -Descripción: - Calcula las distancias entre los átomos Cα y O de segmentos - PPII consecutivos o cercanos. Identifica los pares cuya - distancia es menor al valor máximo especificado. Guarda los - resultados en un archivo de texto y devuelve la lista de - pares de átomos que cumplen el criterio. - -Parámetros: - objetos_por_segmento : list - Lista de listas con los nombres de los pseudoátomos - (Cα y O) de cada segmento. - max_dist : float, opcional - Distancia máxima (en Ångström) para considerar los pares - Cα-O como colindantes. Por defecto es 5.0 Å. - archivo_salida : str, opcional - Nombre del archivo de texto donde se guardarán las - distancias calculadas. Por defecto es - "distancias_colindantes.txt". - -Retorno: - list - Lista de tuplas con los nombres de los pares de átomos - (Cα, O) cuya distancia es menor a max_dist. - -Ejemplo de uso: - >>> pares = calcular_distancias_colindantes(objetos_por_segmento) ----------------------------------------------------------- -""" - -def calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt"): - global distancias - pares_ca_o = [] - carpeta = obtener_carpeta_resultados() - archivo_salida = carpeta / archivo_salida - coordenadas = {} - - for segmento in objetos_por_segmento: - for obj in segmento: - coords = cmd.get_coords(obj) - if coords is None or len(coords) == 0: - continue - coordenadas[obj] = coords[0] - - def es_CA(obj): return "_CA_" in obj - def es_O(obj): return "_O_" in obj - - max_dist_sq = max_dist ** 2 - - for salto in [1, 2]: - for i in range(len(objetos_por_segmento) - salto): - seg1 = objetos_por_segmento[i] - seg2 = objetos_por_segmento[i + salto] - - ca1 = [obj for obj in seg1 if es_CA(obj)] - o2 = [obj for obj in seg2 if es_O(obj)] - ca2 = [obj for obj in seg2 if es_CA(obj)] - o1 = [obj for obj in seg1 if es_O(obj)] - - # Comparar ca1 con o2 - for ca in ca1: - coord_ca = coordenadas.get(ca) - if coord_ca is None: - continue - for o in o2: - coord_o = coordenadas.get(o) - if coord_o is None: - continue - dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) - if dist_sq < max_dist_sq: - pares_ca_o.append((ca, o)) - - # Comparar ca2 con o1 - for ca in ca2: - coord_ca = coordenadas.get(ca) - if coord_ca is None: - continue - for o in o1: - coord_o = coordenadas.get(o) - if coord_o is None: - continue - dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) - if dist_sq < max_dist_sq: - pares_ca_o.append((ca, o)) - - with open(archivo_salida, "w") as f: - f.write(f"Pares CA-O con distancia < {max_dist} Å:\n") - for i, (ca, o) in enumerate(pares_ca_o): - dist = math.sqrt(sum((a - b)**2 for a, b in zip(coordenadas[ca], coordenadas[o]))) - f.write(f"{i:03d} | {ca} - {o} : {dist:.2f} Å\n") - - print(f"[DEBUG] Total de pares CA-O guardados: {len(pares_ca_o)}") - distancias= pares_ca_o - return pares_ca_o - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_ANGULOS_CA_H_O <<<<<<<< -# ========================================================== - -""" -calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt") ----------------------------------------------------------- -Descripción: - Calcula los ángulos formados entre los átomos Cα-H-O para - cada par de átomos Cα-O proporcionado. Filtra y guarda - únicamente los ángulos que estén dentro del rango definido - por min_ang y max_ang. Los resultados se almacenan en un - archivo de texto y se devuelven como una lista. - -Parámetros: - pares_ca_o : list - Lista de tuplas con los nombres de los pares de átomos - (Cα, O) sobre los que se calcularán los ángulos. - min_ang : float, opcional - Ángulo mínimo (en grados) para considerar válido un - ángulo CA-H-O. Por defecto es 110.0°. - max_ang : float, opcional - Ángulo máximo (en grados) para considerar válido un - ángulo CA-H-O. Por defecto es 180.0°. - archivo_salida : str, opcional - Nombre del archivo de texto donde se guardarán los - ángulos calculados. Por defecto es "angulos_ca_h_o.txt". - -Retorno: - list - Lista de tuplas con la información de cada ángulo válido - en el formato (Cα, H, O, ángulo). - -Ejemplo de uso: - >>> angulos = calcular_angulos_ca_h_o(pares_ca_o) ----------------------------------------------------------- -""" - -def calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt"): - global angulos_v - angulos_validos = [] - archivo_salida = obtener_carpeta_resultados() / archivo_salida - archivo_salida.parent.mkdir(parents=True, exist_ok=True) - - with open(archivo_salida, "w") as f: - f.write("CA - H - O : Ángulo (grados)\n") - - for ca, o in pares_ca_o: - if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): - continue - - # Extraer resn, resi y chain - partes = ca.split("_") - if len(partes) < 4: - f.write(f"# Nombre inválido para {ca}\n") - continue - - resn = partes[-3] - resi = partes[-2] - chain = partes[-1] - - # Buscar Hs sin usar "model" - seleccion = f"resn {resn} and resi {resi} and chain {chain} and name H*" - try: - hidrogenos = cmd.get_model(seleccion).atom - except Exception as e: - f.write(f"# Error al buscar H para {ca} ({seleccion}): {e}\n") - continue - - if not hidrogenos: - f.write(f"# No se encontraron H para {ca} (búsqueda: {seleccion})\n") - continue - - for h in hidrogenos: - h_sel = f"/{h.model}//{h.chain}/{h.resi}/{h.name}" - h_nombre = f"{h.resn}-{h.resi}{h.chain}_{h.name}" - try: - ang = cmd.get_angle(ca, h_sel, o) - if min_ang <= abs(ang) <= max_ang: - angulos_validos.append((ca, h_nombre, o, ang)) - f.write(f"{ca} - {h_nombre} - {o} : {ang:.2f}°\n") - except Exception as e: - f.write(f"# Error al calcular ángulo para {ca}, {h_sel}, {o} : {e}\n") - angulos_v= angulos_validos - return angulos_validos - - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_Y_GUARDAR_ANGULOS <<<<<<<< -# ========================================================== - -""" -calcular_y_guardar_angulos() ----------------------------------------------------------- -Descripción: - Calcula los ángulos CA-H-O para los segmentos PPII previamente - detectados en la proteína cargada en PyMOL. Localiza los átomos - clave, obtiene las distancias colindantes y calcula los ángulos - que cumplen con el rango de validez (110°-180°). Los resultados - se guardan en un archivo de texto llamado "angulos_ca_h_o.txt" - dentro de la carpeta de resultados. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> calcular_y_guardar_angulos() ----------------------------------------------------------- -""" - - -def calcular_y_guardar_angulos(): - global segmentos_ppii_global - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - # Obtener los objetos atómicos clave - objetos = localizar_atomos - if objetos is None: - objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) - - # Calcular distancias para obtener las tripletas - tripletas = calcular_distancias_colindantes(objetos) - - # Calcular y guardar ángulos - carpeta = obtener_carpeta_resultados() - archivo_salida = carpeta / "angulos_ca_h_o.txt" - angulos = calcular_angulos_ca_h_o(tripletas, str(archivo_salida)) # Convertir a string para PyMOL - - if angulos: - messagebox.showinfo("Éxito", f"Ángulos CA-H-O guardados en:\n{archivo_salida}") - else: - messagebox.showwarning("Aviso", "No se encontraron ángulos válidos (110°-180°)") - - -# ========================================================== -# >>>>>>>> FUNCIÓN: VISUALIZAR_DISTANCIAS_PARES <<<<<<<< -# ========================================================== -""" -visualizar_distancias_pares(pares_candidatos) ----------------------------------------------------------- -Descripción: - Dibuja y visualiza en PyMOL las distancias entre los pares - de átomos proporcionados. Cada distancia se representa como - una línea discontinua (dashed line) de color cian para - facilitar la identificación de interacciones potenciales. - -Parámetros: - pares_candidatos : list - Lista de tuplas con los nombres de los pares de átomos - (atomo1, atomo2) que se desean visualizar. - -Retorno: - None - -Ejemplo de uso: - >>> visualizar_distancias_pares(pares_candidatos) ----------------------------------------------------------- -""" - - -def visualizar_distancias_pares(pares_candidatos): - if not pares_candidatos: - messagebox.showinfo("Visualización", "No hay pares de átomos para visualizar.") - return - - cmd.delete("distancia_ppii") - - for i, (at1, at2) in enumerate(pares_candidatos, start=1): - nombre_dist = f"distancia_ppii_{i}" - cmd.distance(nombre_dist, at1, at2) - cmd.set("dash_width", 4, nombre_dist) - cmd.set("dash_length", 0.5, nombre_dist) - cmd.color("cyan", nombre_dist) - - messagebox.showinfo("Visualización", f"Visualizados {len(pares_candidatos)} pares de distancias colindantes.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: DETECTAR_SEGMENTOS_PPII <<<<<<<< -# ========================================================== - -""" -detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0) ----------------------------------------------------------- -Descripción: - Detecta segmentos de hélices de tipo poliprolina II (PPII) - en la proteína cargada en PyMOL. Analiza los ángulos - torsionales phi (ϕ) y psi (ψ) de cada residuo para - identificar regiones consecutivas compatibles con la - conformación PPII según los criterios de tolerancia - definidos. Permite configurar la longitud mínima de los - segmentos y el número máximo de saltos permitidos entre - residuos consecutivos. - -Parámetros: - objeto : str, opcional - Nombre del objeto cargado en PyMOL sobre el que se - realizará la detección. Por defecto es "proteina". - min_length : int, opcional - Longitud mínima (en número de residuos) que debe tener - un segmento para ser considerado PPII. Por defecto es 3. - tol_phi : float, opcional - Tolerancia en grados para el ángulo phi respecto a - los valores característicos de la conformación PPII. - Por defecto es 20.0°. - tol_psi : float, opcional - Tolerancia en grados para el ángulo psi respecto a - los valores característicos de la conformación PPII. - Por defecto es 20.0°. - max_saltos : int, opcional - Número máximo de residuos consecutivos que pueden - incumplir los criterios de PPII dentro de un segmento - sin interrumpirlo. Por defecto es 0. - -Retorno: - None - -Ejemplo de uso: - >>> detectar_segmentos_ppii() ----------------------------------------------------------- -""" - -def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0): - global segmentos_ppii_global - - if not pdb_file: - messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") - return - - phi_psi_map = obtener_angulos_phi_psi_por_cadena(objeto) - lista_residuos = [] - for (chain, resi), (resn, phi, psi) in phi_psi_map.items(): - try: - resi_num = int(resi) - except: - continue - lista_residuos.append((chain, resi_num, resn, phi, psi)) - lista_residuos.sort(key=lambda x: (x[0], x[1])) - - segmentos = [] - segmento_actual = [] - saltos_restantes = max_saltos # Contador de saltos permitidos - - def en_rango_ppii(phi, psi, tol_phi=20.0, tol_psi=20.0): - # Valores IDEALES para PPII: φ = -75°, ψ = +145° - return (abs(phi - (-75)) <= tol_phi) and (abs(psi - 145) <= tol_psi) - - for i, (chain, resi, resn, phi, psi) in enumerate(lista_residuos): - if en_rango_ppii(phi, psi, tol_phi, tol_psi): - # Si cumple, reiniciamos el contador de saltos - saltos_restantes = max_saltos - if not segmento_actual: - segmento_actual.append((resn, resi, chain, phi, psi)) - else: - _, last_resi, last_chain, _, _ = segmento_actual[-1] - if chain == last_chain and resi == last_resi + 1: - segmento_actual.append((resn, resi, chain, phi, psi)) - else: - if len(segmento_actual) >= min_length: - segmentos.append(segmento_actual) - segmento_actual = [(resn, resi, chain, phi, psi)] - else: - # Si no cumple pero hay saltos disponibles - if segmento_actual and saltos_restantes > 0: - _, last_resi, last_chain, _, _ = segmento_actual[-1] - if chain == last_chain and resi == last_resi + 1: - segmento_actual.append((resn, resi, chain, phi, psi)) - saltos_restantes -= 1 - continue - - # Si no hay saltos disponibles o no es consecutivo - if len(segmento_actual) >= min_length: - segmentos.append(segmento_actual) - segmento_actual = [] - saltos_restantes = max_saltos - - # Añadir el último segmento si cumple con la longitud mínima - if len(segmento_actual) >= min_length: - segmentos.append(segmento_actual) - - if not segmentos: - messagebox.showinfo("Resultado", "No se encontraron segmentos PPII.") - return - - cmd.delete("ppii_segmento*") - - salida = f"Segmentos candidatos a hélices PPII (saltos permitidos: {max_saltos}):\n" - for idx, seg in enumerate(segmentos, start=1): - start_resi = seg[0][1] - end_resi = seg[-1][1] - chain = seg[0][2] - salida += f"\nSegmento {idx} (Cadena {chain}, residuos {start_resi}-{end_resi}, longitud {len(seg)}):\n" - for (resn, resi, _, phi, psi) in seg: - salida += f" {resn}-{resi}{chain}: (phi={phi:.1f}, psi={psi:.1f})\n" - - sel_str = f"proteina and chain {chain} and resi {start_resi}-{end_resi}" - obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" - cmd.create(obj_name, sel_str) - cmd.color("red", obj_name) - cmd.show("cartoon", obj_name) - - carpeta = obtener_carpeta_resultados() - ruta_archivo = carpeta / "segmentos_ppii.txt" - with open(ruta_archivo, "w") as f: - f.write(salida) - - segmentos_ppii_global = segmentos - - messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados (con {max_saltos} saltos permitidos).\n" - f"Átomos clave visualizados en PyMOL.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: GUARDAR_SEGMENTO_PPII_PDB <<<<<<<< -# ========================================================== - -""" -guardar_segmento_ppii_pdb(segmento, nombre_archivo="segmento_ppii.pdb") ----------------------------------------------------------- -Descripción: - Guarda en un archivo PDB un segmento específico de hélice - PPII previamente detectado en la proteína cargada en PyMOL. - El archivo se genera con el nombre especificado dentro de la - carpeta de resultados. - -Parámetros: - segmento : list - Lista con los residuos que forman el segmento PPII a guardar. - nombre_archivo : str, opcional - Nombre del archivo PDB que se generará. Por defecto es - "segmento_ppii.pdb". - -Retorno: - None - -Ejemplo de uso: - >>> guardar_segmento_ppii_pdb(segmentos_ppii[0]) ----------------------------------------------------------- -""" - -def guardar_segmentos_ppii_pdb(): - global segmentos_ppii_global - carpeta = obtener_carpeta_resultados() - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - # Para cada segmento almacenado, construimos el nombre de objeto y hacemos cmd.save - count = 0 - for seg in segmentos_ppii_global: - start_resi = seg[0][1] - end_resi = seg[-1][1] - chain = seg[0][2] - obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" - # Comprobamos que el objeto efectivamente exista en PyMOL - if cmd.count_atoms(f"{obj_name}") > 0: - - filename = carpeta / f"{obj_name}.pdb" - try: - cmd.save(filename, obj_name) - count += 1 - except Exception as e: - print(f"Error guardando {obj_name}: {e}") - else: - print(f"Objeto {obj_name} no encontrado en la sesión de PyMOL.") - - if count > 0: - messagebox.showinfo("Éxito", f"Se guardaron {count} archivos PDB de segmentos PPII:\n" - f"{os.getcwd()}") - else: - messagebox.showwarning("Atención", "No se guardó ningún segmento (quizá no existan objetos en PyMOL).") - -# ========================================================== -# >>>>>>>> FUNCIÓN: CONVERTIR_A_SELECCIONES_PYMOL <<<<<<<< -# ========================================================== - -""" -convertir_a_selecciones_pymol(pares_con_distancias) ----------------------------------------------------------- -Descripción: - Convierte una lista de pares de átomos con sus distancias en - selecciones de PyMOL. Cada par se representa como una selección - que facilita la visualización y el análisis de las interacciones - detectadas. - -Parámetros: - pares_con_distancias : list - Lista de tuplas en el formato (atomo1, atomo2, distancia) - que serán convertidas en selecciones de PyMOL. - -Retorno: - list - Lista con los nombres de las selecciones creadas en PyMOL. - -Ejemplo de uso: - >>> selecciones = convertir_a_selecciones_pymol(pares_con_distancias) ----------------------------------------------------------- -""" - -def convertir_a_selecciones_pymol(pares_con_distancias): - selecciones = [] - for at1, at2, _ in pares_con_distancias: - sele1 = f"id {cmd.index(at1)[0][1]}" - sele2 = f"id {cmd.index(at2)[0][1]}" - selecciones.append((sele1, sele2)) - return selecciones - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_Y_VISUALIZAR_DISTANCIAS <<<<<<<< -# ========================================================== - -""" -calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0) ----------------------------------------------------------- -Descripción: - Calcula las distancias entre átomos Cα-O de segmentos PPII - consecutivos y los ángulos CA-H-O asociados. Visualiza en - PyMOL los pares de átomos que cumplen con los criterios de - distancia y ángulo definidos por el usuario. Los pares - válidos se representan con líneas discontinuas (dashed lines) - en color cian para facilitar el análisis. - -Parámetros: - max_dist : float, opcional - Distancia máxima (en Ångström) para considerar los pares - Cα-O como colindantes. Por defecto es 5.0 Å. - min_ang : float, opcional - Ángulo mínimo (en grados) para considerar válido un ángulo - CA-H-O. Por defecto es 110.0°. - max_ang : float, opcional - Ángulo máximo (en grados) para considerar válido un ángulo - CA-H-O. Por defecto es 180.0°. - -Retorno: - None - -Ejemplo de uso: - >>> calcular_y_visualizar_distancias() ----------------------------------------------------------- -""" - -def calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0): - global segmentos_ppii_global - global objetos, pares - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - if objetos is None: - objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) - if pares is None: - pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) - - calcular_angulos_ca_h_o(pares, min_ang=min_ang, max_ang=max_ang) - -# ========================================================== -# >>>>>>>> FUNCIÓN: DISTANCIAS_P <<<<<<<< -# ========================================================== - -""" -distancias_p() ----------------------------------------------------------- -Descripción: - Visualiza en PyMOL todas las distancias entre los pares - de átomos Cα-O previamente calculados y almacenados. - Cada distancia se dibuja como una línea discontinua - (dashed line) para facilitar el análisis estructural - de la proteína. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> distancias_p() ----------------------------------------------------------- -""" - -def distancias_p(): - global segmentos_ppii_global - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) - pares = calcular_distancias_colindantes(objetos) - visualizar_distancias_pares(pares) - -# ========================================================== -# >>>>>>>> FUNCIÓN: LANZAR_INTERFAZ <<<<<<<< -# ========================================================== - -""" -lanzar_interfaz() ----------------------------------------------------------- -Descripción: - Inicia la interfaz gráfica de usuario (GUI) desarrollada - con Tkinter para facilitar la interacción con el programa. - Permite acceder a las funciones principales como cargar - archivos PDB, detectar hélices PPII, añadir hidrógenos, - eliminar solventes y generar reportes, todo desde un menú - visual. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> lanzar_interfaz() ----------------------------------------------------------- -""" - -def lanzar_interfaz(): - root = tk.Tk() - root.title("PPIIMoL: PPII Detect") - root.geometry("450x1000") - root.resizable(False, False) - - style = ttk.Style(root) - style.theme_use('classic') # Puedes probar: 'alt', 'clam', 'default', 'classic' - - main_frame = ttk.Frame(root, padding=5) - main_frame.pack(fill="both", expand=True) - - def wrapper_detectar_segmentos_ppii(): - try: - tol_phi = float(entrada_tol_phi.get()) - tol_psi = float(entrada_tol_psi.get()) - max_saltos = int(entrada_saltos.get()) # Obtener el valor del campo de saltos - if max_saltos < 0 or max_saltos > 5: # Validar rango (0-5) - raise ValueError - except ValueError: - messagebox.showerror("Error", "¡Saltos debe ser un entero entre 0 y 5!") - return - - detectar_segmentos_ppii(tol_phi=tol_phi, tol_psi=tol_psi, max_saltos=max_saltos) # Pasar el parámetro - -# En la función lanzar_interfaz(), añade este frame antes del frame de ángulos CA-H-O - # Frame para parámetros phi/psi - phi_psi_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulos phi/psi", padding=10) - phi_psi_frame.pack(fill="x", pady=5) - ttk.Label(phi_psi_frame, text="Actualmente sin la tolerancia los angulos ").pack(anchor="w") - ttk.Label(phi_psi_frame, text="por defecto que se miden son hasta: phi 75 y psi 145").pack(anchor="w") - - ttk.Label(phi_psi_frame, text="Tolerancia para phi (±°):").pack(anchor="w") - entrada_tol_phi = ttk.Entry(phi_psi_frame) - entrada_tol_phi.insert(0, "20.0") - entrada_tol_phi.pack(fill="x", pady=2) - - ttk.Label(phi_psi_frame, text="Tolerancia para psi (±°): ").pack(anchor="w") - entrada_tol_psi = ttk.Entry(phi_psi_frame) - entrada_tol_psi.insert(0, "20.0") - entrada_tol_psi.pack(fill="x", pady=2) - - - saltos_frame = ttk.LabelFrame(main_frame, text="Parámetros de saltos", padding=5) - saltos_frame.pack(fill="x", pady=3) - - ttk.Label(saltos_frame, text="Saltos permitidos (0-5):").pack(anchor="w") - entrada_saltos = ttk.Entry(saltos_frame) - entrada_saltos.insert(0, "0") # Valor por defecto: 0 saltos - entrada_saltos.pack(fill="x", pady=1) - - - def wrapper_generar_reporte_csv(): - try: - min_ang = float(entrada_min_ang.get()) - max_ang = float(entrada_max_ang.get()) - except ValueError: - messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") - return - - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - generar_reporte_csv(segmentos_ppii_global, min_ang=min_ang, max_ang=max_ang) - #cambiar por otra funcion envoltorio diferente - - # Función envoltorio para pasar los valores - def wrapper_calcular_y_visualizar(): - try: - min_ang = float(entrada_min_ang.get()) - max_ang = float(entrada_max_ang.get()) - except ValueError: - messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") - return - calcular_y_visualizar_distancias(min_ang=min_ang, max_ang=max_ang) - - # Botones funcionales - botones = [ - ("Seleccionar archivo PDB", seleccionar_archivo), - ("Descargar proteína", descargar_molecula), - ("Eliminar solventes", eliminar_solventes), - ("Añadir hidrógenos", anadir_hidrogenos), - ("Ocultar cadenas laterales", ocultar_side_chains), - ("Guardar ángulos phi/psi en archivo", guardar_csv_angulos_phi_psi), - ("Detectar segmentos PPII y resaltarlos", wrapper_detectar_segmentos_ppii), - ("Guardar segmentos PPII en PDB", guardar_segmentos_ppii_pdb), - ("Visualizar distancias", distancias_p), - ("Angulos entre CA-O-H colindantes", wrapper_calcular_y_visualizar), - ("Generar reporte completo (CSV)", wrapper_generar_reporte_csv), - ] - - for texto, accion in botones: - ttk.Button(main_frame, text=texto, command=accion).pack(fill="x", pady=1) - - # Entradas de ángulos - ang_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulo CA-H-O", padding=3) - ang_frame.pack(fill="x", pady=3) - - ttk.Label(ang_frame, text="Ángulo mínimo (°):").pack(anchor="w") - entrada_min_ang = ttk.Entry(ang_frame) - entrada_min_ang.insert(0, "110.0") - entrada_min_ang.pack(fill="x", pady=1) - - ttk.Label(ang_frame, text="Ángulo máximo (°):").pack(anchor="w") - entrada_max_ang = ttk.Entry(ang_frame) - entrada_max_ang.insert(0, "180.0") - entrada_max_ang.pack(fill="x", pady=1) - - root.mainloop() - - -lanzar_interfaz() diff --git a/PPIIMol/PPIIMoL.py b/PPIIMol/PPIIMoL.py deleted file mode 100644 index afe7457..0000000 --- a/PPIIMol/PPIIMoL.py +++ /dev/null @@ -1,1285 +0,0 @@ -from pymol import cmd, stored -import tkinter as tk -import math -from pathlib import Path -from datetime import datetime -import os -from tkinter import filedialog, messagebox -import tkinter as tk -from tkinter import ttk, messagebox -import os -import csv - - -# Valores por defecto (se actualizarán cuando el usuario los cambie) -tol_phi_global = 20.0 -tol_psi_global = 20.0 -min_ang_global = 110.0 -max_ang_global = 180.0 - - -pdb_file = None -segmentos_ppii_global = [] # Variable global para guardar segmentos PPII detectados -localizar_atomos=None -distancias=None -angulos_v=None -objetos = None -pares = None - -# ========================================================== -# >>>>>>>> FUNCIÓN: OBTENER_CARPETA_RESULTADOS <<<<<<<< -# ========================================================== - -""" -obtener_carpeta_resultados(nombre_carpeta='Resultados_PyMOL') ----------------------------------------------------------- -Descripción: - Crea una carpeta con fecha actual dentro del directorio Documentos - (o equivalente) del usuario para guardar los resultados generados - por el módulo. - -Parámetros: - nombre_carpeta : str, opcional - Nombre base para la carpeta de resultados (por defecto 'Resultados_PyMOL'). - -Retorno: - pathlib.Path - Ruta completa a la carpeta creada. - -Ejemplo de uso: - >>> carpeta = obtener_carpeta_resultados("Resultados_PPIIMoL") - >>> print(carpeta) - /home/usuario/Documentos/Resultados_PPIIMoL_2025-07-05 ----------------------------------------------------------- -""" - -def obtener_carpeta_resultados(nombre_carpeta="Resultados_PyMOL"): - # Formatear fecha y hora actual (compatible con ambos sistemas) - fecha_hora = datetime.now().strftime("%Y-%m-%d") - - # Usar ~/Documents o ~/Documentos según el sistema - documentos = Path.home() / ("Documentos" if os.getenv('LANG', '').startswith('es_') else "Documents") - - # Si no existe, crear directorio en HOME directamente - if not documentos.exists(): - documentos = Path.home() - - # Crear nombre de carpeta combinado - nombre_completo = f"{nombre_carpeta}_{fecha_hora}" - carpeta_resultados = documentos / nombre_completo - - # Crear la carpeta (con parents=True por si faltan directorios padres) - carpeta_resultados.mkdir(parents=True, exist_ok=True) - - return carpeta_resultados - -# ========================================================== -# >>>>>>>> FUNCIÓN: SELECCIONAR_ARCHIVO <<<<<<<< -# ========================================================== - -""" -seleccionar_archivo() ----------------------------------------------------------- -Descripción: - Abre un cuadro de diálogo para que el usuario seleccione un archivo PDB - de entrada. La ruta seleccionada se guarda en la variable global pdb_file. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> seleccionar_archivo() ----------------------------------------------------------- -""" - -def seleccionar_archivo(): - global pdb_file - pdb_file = filedialog.askopenfilename( - title="Selecciona un archivo PDB", - filetypes=[("Archivos PDB", "*.pdb")] - ) - if pdb_file: - cmd.reinitialize() - cmd.load(pdb_file, "proteina") - cmd.hide("everything", "proteina") # Ocultar todo lo visible - cmd.show("licorice", "proteina") # Mostrar como licorice - messagebox.showinfo("Archivo cargado", f"Se cargó:\n{pdb_file}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: DESCARGAR_MOLECULA <<<<<<<< -# ========================================================== - -""" -descargar_molecula() ----------------------------------------------------------- -Descripción: - Abre una ventana emergente para permitir al usuario introducir un ID - de proteína del Protein Data Bank (PDB) y descargar automáticamente - la estructura correspondiente. Una vez descargada, la molécula se - carga en PyMOL, se ocultan todas las representaciones gráficas - actuales y se muestra la proteína usando el estilo "licorice". - La ruta o ID de la proteína descargada se guarda en la variable global - pdb_file. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> descargar_molecula() ----------------------------------------------------------- -""" - -def descargar_molecula(): - global pdb_file - def fetch_pdb(): - global pdb_file - pdb_id = entry.get().strip() - if pdb_id: - try: - cmd.reinitialize() - cmd.fetch(pdb_id, name="proteina") - cmd.hide("everything", "proteina") - cmd.show("licorice", "proteina") - # Aquí asignamos pdb_file con un valor para simular archivo cargado - pdb_file = pdb_id # Solo guardamos el ID para que funcione la lógica de “selección” - messagebox.showinfo("Descarga completa", f"Molécula {pdb_id.upper()} descargada y cargada.") - fetch_window.destroy() - except Exception as e: - messagebox.showerror("Error", f"No se pudo descargar {pdb_id}: {e}") - else: - messagebox.showwarning("Advertencia", "Por favor ingresa un ID de PDB.") - - fetch_window = tk.Toplevel() - fetch_window.title("Descargar proteína") - tk.Label(fetch_window, text="ID de la proteína (PDB):").pack(pady=5) - entry = tk.Entry(fetch_window, width=20) - entry.pack(pady=5) - tk.Button(fetch_window, text="Descargar", command=fetch_pdb).pack(pady=5) - -# ========================================================== -# >>>>>>>> FUNCIÓN: ANADIR_HIDROGENOS <<<<<<<< -# ========================================================== - -""" -anadir_hidrogenos() ----------------------------------------------------------- -Descripción: - Añade átomos de hidrógeno a todas las moléculas cargadas - en la sesión actual de PyMOL. Una vez añadidos, la - estructura se reorganiza para optimizar la visualización - y se muestra usando el estilo "licorice". Si no hay - ningún archivo PDB cargado previamente, se muestra una - advertencia al usuario. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> anadir_hidrogenos() ----------------------------------------------------------- -""" - -def anadir_hidrogenos(): - if not pdb_file: - messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") - return - cmd.h_add("all") - cmd.sort("all extend 1") - cmd.show("licorice", "all") - messagebox.showinfo("Hidrógenos", "Hidrógenos añadidos y mostrados como licorice.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: ELIMINAR_SOLVENTES <<<<<<<< -# ========================================================== - -""" -eliminar_solventes() ----------------------------------------------------------- -Descripción: - Elimina todas las moléculas de solvente presentes en la - estructura cargada en PyMOL. Tras la eliminación, se - muestra un mensaje informativo al usuario confirmando - la acción realizada. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> eliminar_solventes() ----------------------------------------------------------- -""" - -def eliminar_solventes(): - cmd.remove("solvent") - messagebox.showinfo("Solventes", "Solventes eliminados.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: OCULTAR_SIDE_CHAINS <<<<<<<< -# ========================================================== - -""" -ocultar_side_chains() ----------------------------------------------------------- -Descripción: - Oculta todas las cadenas laterales de los residuos de - la proteína cargada en PyMOL, dejando visibles únicamente - los átomos del esqueleto principal (backbone: N, CA, C, O). - Esta función permite centrar la visualización en la - estructura principal de la proteína sin distracciones. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> ocultar_side_chains() ----------------------------------------------------------- -""" - -def ocultar_side_chains(): - cmd.hide("everything", "proteina and not name N+CA+C+O") - messagebox.showinfo("Backbone", "Cadenas laterales ocultas (solo backbone).") - -# ========================================================== -# >>>>>>>> FUNCIÓN: SEPARAR_CADENAS <<<<<<<< -# ========================================================== - -""" -separar_cadenas() ----------------------------------------------------------- -Descripción: - Separa cada cadena de la proteína cargada en PyMOL - en objetos individuales. Cada nueva cadena se guarda - como un objeto separado con un nombre identificador - en el formato "cadena_X", donde X es el identificador - de la cadena original. Esta función facilita la - manipulación y análisis individual de las cadenas. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> separar_cadenas() ----------------------------------------------------------- -""" - -def separar_cadenas(): - stored.chains = [] - cmd.iterate("proteina", "stored.chains.append(chain)") - for cadena in set(stored.chains): - nuevo_objeto = f"cadena_{cadena}" - cmd.create(nuevo_objeto, f"proteina and chain {cadena}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: OBTENER_ANGULOS_PHI_PSI_POR_CADENA <<<<<<<< -# ========================================================== - -""" -obtener_angulos_phi_psi_por_cadena(objeto="proteina") ----------------------------------------------------------- -Descripción: - Calcula los ángulos torsionales phi (ϕ) y psi (ψ) de cada - residuo en todas las cadenas del objeto especificado - cargado en PyMOL. Los resultados se devuelven en un - diccionario que asocia cada residuo con su nombre, número - y los valores de los ángulos phi y psi correspondientes. - -Parámetros: - objeto : str, opcional - Nombre del objeto cargado en PyMOL sobre el cual se - calcularán los ángulos (por defecto "proteina"). - -Retorno: - dict - Diccionario con las claves como tuplas (cadena, número - de residuo) y valores como tuplas (nombre del residuo, - ángulo phi, ángulo psi). - -Ejemplo de uso: - >>> resultados = obtener_angulos_phi_psi_por_cadena() - >>> print(resultados) ----------------------------------------------------------- -""" - -def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): - resultados = {} - chains = cmd.get_chains(objeto) - for chain in chains: - sel_ca = f"{objeto} and chain {chain} and name CA" - phipsi = cmd.get_phipsi(sel_ca) - if not phipsi: - continue - for (obj, idx), (phi, psi) in sorted(phipsi.items()): - if phi is None or psi is None: - continue - stored.info = [] - cmd.iterate(f"({obj}`{idx})", "stored.info.append((chain, resn, resi))", space={'stored': stored}) - if not stored.info: - continue - ch, resn, resi = stored.info[0] - resultados[(ch, resi)] = (resn, phi, psi) - return resultados - -# ========================================================== -# >>>>>>>> FUNCIÓN: GUARDAR_CSV_ANGULOS_PHI_PSI <<<<<<<< -# ========================================================== - -""" -guardar_csv_angulos_phi_psi() ----------------------------------------------------------- -Descripción: - Genera un archivo CSV con los ángulos torsionales phi (ϕ) - y psi (ψ) calculados para cada residuo de la proteína - cargada en PyMOL. El archivo se guarda en una carpeta - de resultados con el nombre "angulos_phi_psi.csv" e - incluye información de la cadena, el residuo, su número - y los valores de los ángulos. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> guardar_csv_angulos_phi_psi() ----------------------------------------------------------- -""" - -def guardar_csv_angulos_phi_psi(): - if not pdb_file: - messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") - return - - phi_map = obtener_angulos_phi_psi_por_cadena("proteina") - - stored.res_list = [] - cmd.iterate("proteina and name CA", "stored.res_list.append((chain, resn, resi))") - - datos_csv = [("Cadena", "Residuo", "Número", "Phi", "Psi")] - - for chain, resn, resi in sorted(stored.res_list, key=lambda x: (x[0], int(x[2]))): - key = (chain, resi) - if key in phi_map: - resn_val, phi, psi = phi_map[key] - datos_csv.append((chain, resn, resi, f"{phi:.2f}", f"{psi:.2f}")) - carpeta= obtener_carpeta_resultados() - # Guardar en CSV con separador ; - if len(datos_csv) > 1: - ruta_csv = os.path.join(os.getcwd(), carpeta/"angulos_phi_psi.csv") - with open(ruta_csv, mode="w", newline="", encoding="utf-8") as file: - writer = csv.writer(file, delimiter=";") - writer.writerows(datos_csv) - messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: GENERAR_REPORTE_CSV <<<<<<<< -# ========================================================== - -""" -generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0) ----------------------------------------------------------- -Descripción: - Genera un archivo CSV con un informe detallado sobre los - segmentos de hélices PPII detectados en la proteína cargada - en PyMOL. El reporte incluye pares de átomos Cα-O colindantes, - sus distancias, los ángulos CA-H-O calculados y los valores - validados según los rangos definidos. El archivo se guarda - en una carpeta de resultados con el nombre especificado. - -Parámetros: - segmentos_ppii : list - Lista de segmentos PPII detectados en la proteína. - max_dist : float, opcional - Distancia máxima (en Ångström) para considerar los - pares de átomos colindantes. Por defecto es 5.0 Å. - nombre_archivo : str, opcional - Nombre del archivo CSV que se generará. Por defecto - es "reporte_ppii.csv". - min_ang : float, opcional - Ángulo mínimo en grados para filtrar los ángulos CA-H-O - válidos. Por defecto es 110.0°. - max_ang : float, opcional - Ángulo máximo en grados para filtrar los ángulos CA-H-O - válidos. Por defecto es 180.0°. - -Retorno: - None - -Ejemplo de uso: - >>> generar_reporte_csv(segmentos_ppii) ----------------------------------------------------------- -""" - -def generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0): - if not segmentos_ppii: - messagebox.showwarning("Advertencia", "No hay segmentos PPII detectados.") - return - - objetos_por_segmento = localizar_atomos - pares_ca_o=distancias - angulos_validos=angulos_v - if objetos_por_segmento ==None: - objetos_por_segmento = localizar_atomicos_clave_segmentos(segmentos_ppii) - if pares_ca_o is None: - pares_ca_o = calcular_distancias_colindantes(objetos_por_segmento, max_dist=max_dist) - - # PASAMOS LOS ÁNGULOS AQUÍ - if angulos_validos is None: - angulos_validos = calcular_angulos_ca_h_o(pares_ca_o, min_ang=min_ang, max_ang=max_ang) - - dist_dict = {} - for ca, o in pares_ca_o: - if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): - continue - coord_ca = cmd.get_coords(ca)[0] - coord_o = cmd.get_coords(o)[0] - dist = math.sqrt(sum((a - b)**2 for a, b in zip(coord_ca, coord_o))) - dist_dict[(ca, o)] = dist - - ang_dict = {(ca, o): ang for ca, _, o, ang in angulos_validos} - - datos_csv = [["Átomo Cα", "Átomo O", "Distancia (Å)", "Ángulo CA-H-O (°)"]] - for (ca, o), dist in dist_dict.items(): - ang = ang_dict.get((ca, o), None) - if ang is not None: - datos_csv.append([ca, o, f"{dist:.2f}", f"{ang:.2f}"]) - - ruta_csv = obtener_carpeta_resultados() / nombre_archivo - with open(ruta_csv, 'w', newline='', encoding='utf-8') as f: - writer = csv.writer(f, delimiter=';') - writer.writerows(datos_csv) - - messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") - -# ========================================================== -# >>>>>>>> FUNCIÓN: LOCALIZAR_ATOMICOS_CLAVE_SEGMENTOS <<<<<<<< -# ========================================================== - -""" -localizar_atomicos_clave_segmentos(segmentos) ----------------------------------------------------------- -Descripción: - Localiza y marca en PyMOL los átomos clave (Cα y O) de - cada segmento PPII detectado. Crea pseudoátomos en las - posiciones correspondientes para facilitar la visualización - y análisis. Los átomos Cα se marcan en color azul y los O - en color rojo, agrupándolos bajo el nombre "atomos_clave". - -Parámetros: - segmentos : list - Lista de segmentos PPII detectados en la proteína. - -Retorno: - list - Lista de listas, donde cada sublista contiene los - nombres de los pseudoátomos creados para un segmento. - -Ejemplo de uso: - >>> localizar_atomicos_clave_segmentos(segmentos_ppii) ----------------------------------------------------------- -""" - -def localizar_atomicos_clave_segmentos(segmentos): - global localizar_atomos - cmd.delete("esfera_*") - objetos_por_segmento = [] - cmd.h_add() - - for idx, seg in enumerate(segmentos, start=1): - objetos_segmento = [] - - for (resn, resi, chain, _, _) in seg: - sele_base = f"proteina and chain {chain} and resi {resi}" - stored.coords = [] - - # Átomo CA (carbono alfa) - stored.ca_coords = [] - cmd.iterate_state( - 1, - f"{sele_base} and name CA", - "stored.ca_coords.append((x, y, z))", - space={'stored': stored} - ) - - # Oxígeno carbonilo - stored.o_coords = [] - cmd.iterate_state( - 1, - f"{sele_base} and name O", - "stored.o_coords.append((x, y, z))", - space={'stored': stored} - ) - - # Crear pseudoatomos con nombres consistentes - if stored.ca_coords: - x, y, z = stored.ca_coords[0] - esfera_name = f"esfera_s{idx}_CA_{resn}_{resi}_{chain}" - cmd.pseudoatom(esfera_name, pos=[x, y, z]) - cmd.set("sphere_scale", 0.3, esfera_name) - cmd.color("blue", esfera_name) - objetos_segmento.append(esfera_name) - - if stored.o_coords: - x, y, z = stored.o_coords[0] - esfera_name = f"esfera_s{idx}_O_{resn}_{resi}_{chain}" - cmd.pseudoatom(esfera_name, pos=[x, y, z]) - cmd.set("sphere_scale", 0.3, esfera_name) - cmd.color("red", esfera_name) - objetos_segmento.append(esfera_name) - - objetos_por_segmento.append(objetos_segmento) - - cmd.group("atomos_clave", "esfera_*") - localizar_atomos = objetos_por_segmento - return objetos_por_segmento - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_DISTANCIAS_COLINDANTES <<<<<<<< -# ========================================================== - -""" -calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt") ----------------------------------------------------------- -Descripción: - Calcula las distancias entre los átomos Cα y O de segmentos - PPII consecutivos o cercanos. Identifica los pares cuya - distancia es menor al valor máximo especificado. Guarda los - resultados en un archivo de texto y devuelve la lista de - pares de átomos que cumplen el criterio. - -Parámetros: - objetos_por_segmento : list - Lista de listas con los nombres de los pseudoátomos - (Cα y O) de cada segmento. - max_dist : float, opcional - Distancia máxima (en Ångström) para considerar los pares - Cα-O como colindantes. Por defecto es 5.0 Å. - archivo_salida : str, opcional - Nombre del archivo de texto donde se guardarán las - distancias calculadas. Por defecto es - "distancias_colindantes.txt". - -Retorno: - list - Lista de tuplas con los nombres de los pares de átomos - (Cα, O) cuya distancia es menor a max_dist. - -Ejemplo de uso: - >>> pares = calcular_distancias_colindantes(objetos_por_segmento) ----------------------------------------------------------- -""" - -def calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt"): - global distancias - pares_ca_o = [] - carpeta = obtener_carpeta_resultados() - archivo_salida = carpeta / archivo_salida - coordenadas = {} - - for segmento in objetos_por_segmento: - for obj in segmento: - coords = cmd.get_coords(obj) - if coords is None or len(coords) == 0: - continue - coordenadas[obj] = coords[0] - - def es_CA(obj): return "_CA_" in obj - def es_O(obj): return "_O_" in obj - - max_dist_sq = max_dist ** 2 - - for salto in [1, 2]: - for i in range(len(objetos_por_segmento) - salto): - seg1 = objetos_por_segmento[i] - seg2 = objetos_por_segmento[i + salto] - - ca1 = [obj for obj in seg1 if es_CA(obj)] - o2 = [obj for obj in seg2 if es_O(obj)] - ca2 = [obj for obj in seg2 if es_CA(obj)] - o1 = [obj for obj in seg1 if es_O(obj)] - - # Comparar ca1 con o2 - for ca in ca1: - coord_ca = coordenadas.get(ca) - if coord_ca is None: - continue - for o in o2: - coord_o = coordenadas.get(o) - if coord_o is None: - continue - dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) - if dist_sq < max_dist_sq: - pares_ca_o.append((ca, o)) - - # Comparar ca2 con o1 - for ca in ca2: - coord_ca = coordenadas.get(ca) - if coord_ca is None: - continue - for o in o1: - coord_o = coordenadas.get(o) - if coord_o is None: - continue - dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) - if dist_sq < max_dist_sq: - pares_ca_o.append((ca, o)) - - with open(archivo_salida, "w") as f: - f.write(f"Pares CA-O con distancia < {max_dist} Å:\n") - for i, (ca, o) in enumerate(pares_ca_o): - dist = math.sqrt(sum((a - b)**2 for a, b in zip(coordenadas[ca], coordenadas[o]))) - f.write(f"{i:03d} | {ca} - {o} : {dist:.2f} Å\n") - - print(f"[DEBUG] Total de pares CA-O guardados: {len(pares_ca_o)}") - distancias= pares_ca_o - return pares_ca_o - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_ANGULOS_CA_H_O <<<<<<<< -# ========================================================== - -""" -calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt") ----------------------------------------------------------- -Descripción: - Calcula los ángulos formados entre los átomos Cα-H-O para - cada par de átomos Cα-O proporcionado. Filtra y guarda - únicamente los ángulos que estén dentro del rango definido - por min_ang y max_ang. Los resultados se almacenan en un - archivo de texto y se devuelven como una lista. - -Parámetros: - pares_ca_o : list - Lista de tuplas con los nombres de los pares de átomos - (Cα, O) sobre los que se calcularán los ángulos. - min_ang : float, opcional - Ángulo mínimo (en grados) para considerar válido un - ángulo CA-H-O. Por defecto es 110.0°. - max_ang : float, opcional - Ángulo máximo (en grados) para considerar válido un - ángulo CA-H-O. Por defecto es 180.0°. - archivo_salida : str, opcional - Nombre del archivo de texto donde se guardarán los - ángulos calculados. Por defecto es "angulos_ca_h_o.txt". - -Retorno: - list - Lista de tuplas con la información de cada ángulo válido - en el formato (Cα, H, O, ángulo). - -Ejemplo de uso: - >>> angulos = calcular_angulos_ca_h_o(pares_ca_o) ----------------------------------------------------------- -""" - -def calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt"): - global angulos_v - angulos_validos = [] - archivo_salida = obtener_carpeta_resultados() / archivo_salida - archivo_salida.parent.mkdir(parents=True, exist_ok=True) - - with open(archivo_salida, "w") as f: - f.write("CA - H - O : Ángulo (grados)\n") - - for ca, o in pares_ca_o: - if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): - continue - - # Extraer resn, resi y chain - partes = ca.split("_") - if len(partes) < 4: - f.write(f"# Nombre inválido para {ca}\n") - continue - - resn = partes[-3] - resi = partes[-2] - chain = partes[-1] - - # Buscar Hs sin usar "model" - seleccion = f"resn {resn} and resi {resi} and chain {chain} and name H*" - try: - hidrogenos = cmd.get_model(seleccion).atom - except Exception as e: - f.write(f"# Error al buscar H para {ca} ({seleccion}): {e}\n") - continue - - if not hidrogenos: - f.write(f"# No se encontraron H para {ca} (búsqueda: {seleccion})\n") - continue - - for h in hidrogenos: - h_sel = f"/{h.model}//{h.chain}/{h.resi}/{h.name}" - h_nombre = f"{h.resn}-{h.resi}{h.chain}_{h.name}" - try: - ang = cmd.get_angle(ca, h_sel, o) - if min_ang <= abs(ang) <= max_ang: - angulos_validos.append((ca, h_nombre, o, ang)) - f.write(f"{ca} - {h_nombre} - {o} : {ang:.2f}°\n") - except Exception as e: - f.write(f"# Error al calcular ángulo para {ca}, {h_sel}, {o} : {e}\n") - angulos_v= angulos_validos - return angulos_validos - - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_Y_GUARDAR_ANGULOS <<<<<<<< -# ========================================================== - -""" -calcular_y_guardar_angulos() ----------------------------------------------------------- -Descripción: - Calcula los ángulos CA-H-O para los segmentos PPII previamente - detectados en la proteína cargada en PyMOL. Localiza los átomos - clave, obtiene las distancias colindantes y calcula los ángulos - que cumplen con el rango de validez (110°-180°). Los resultados - se guardan en un archivo de texto llamado "angulos_ca_h_o.txt" - dentro de la carpeta de resultados. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> calcular_y_guardar_angulos() ----------------------------------------------------------- -""" - - -def calcular_y_guardar_angulos(): - global segmentos_ppii_global - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - # Obtener los objetos atómicos clave - objetos = localizar_atomos - if objetos is None: - objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) - - # Calcular distancias para obtener las tripletas - tripletas = calcular_distancias_colindantes(objetos) - - # Calcular y guardar ángulos - carpeta = obtener_carpeta_resultados() - archivo_salida = carpeta / "angulos_ca_h_o.txt" - angulos = calcular_angulos_ca_h_o(tripletas, str(archivo_salida)) # Convertir a string para PyMOL - - if angulos: - messagebox.showinfo("Éxito", f"Ángulos CA-H-O guardados en:\n{archivo_salida}") - else: - messagebox.showwarning("Aviso", "No se encontraron ángulos válidos (110°-180°)") - - -# ========================================================== -# >>>>>>>> FUNCIÓN: VISUALIZAR_DISTANCIAS_PARES <<<<<<<< -# ========================================================== -""" -visualizar_distancias_pares(pares_candidatos) ----------------------------------------------------------- -Descripción: - Dibuja y visualiza en PyMOL las distancias entre los pares - de átomos proporcionados. Cada distancia se representa como - una línea discontinua (dashed line) de color cian para - facilitar la identificación de interacciones potenciales. - -Parámetros: - pares_candidatos : list - Lista de tuplas con los nombres de los pares de átomos - (atomo1, atomo2) que se desean visualizar. - -Retorno: - None - -Ejemplo de uso: - >>> visualizar_distancias_pares(pares_candidatos) ----------------------------------------------------------- -""" - - -def visualizar_distancias_pares(pares_candidatos): - if not pares_candidatos: - messagebox.showinfo("Visualización", "No hay pares de átomos para visualizar.") - return - - cmd.delete("distancia_ppii") - - for i, (at1, at2) in enumerate(pares_candidatos, start=1): - nombre_dist = f"distancia_ppii_{i}" - cmd.distance(nombre_dist, at1, at2) - cmd.set("dash_width", 4, nombre_dist) - cmd.set("dash_length", 0.5, nombre_dist) - cmd.color("cyan", nombre_dist) - - messagebox.showinfo("Visualización", f"Visualizados {len(pares_candidatos)} pares de distancias colindantes.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: DETECTAR_SEGMENTOS_PPII <<<<<<<< -# ========================================================== - -""" -detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0) ----------------------------------------------------------- -Descripción: - Detecta segmentos de hélices de tipo poliprolina II (PPII) - en la proteína cargada en PyMOL. Analiza los ángulos - torsionales phi (ϕ) y psi (ψ) de cada residuo para - identificar regiones consecutivas compatibles con la - conformación PPII según los criterios de tolerancia - definidos. Permite configurar la longitud mínima de los - segmentos y el número máximo de saltos permitidos entre - residuos consecutivos. - -Parámetros: - objeto : str, opcional - Nombre del objeto cargado en PyMOL sobre el que se - realizará la detección. Por defecto es "proteina". - min_length : int, opcional - Longitud mínima (en número de residuos) que debe tener - un segmento para ser considerado PPII. Por defecto es 3. - tol_phi : float, opcional - Tolerancia en grados para el ángulo phi respecto a - los valores característicos de la conformación PPII. - Por defecto es 20.0°. - tol_psi : float, opcional - Tolerancia en grados para el ángulo psi respecto a - los valores característicos de la conformación PPII. - Por defecto es 20.0°. - max_saltos : int, opcional - Número máximo de residuos consecutivos que pueden - incumplir los criterios de PPII dentro de un segmento - sin interrumpirlo. Por defecto es 0. - -Retorno: - None - -Ejemplo de uso: - >>> detectar_segmentos_ppii() ----------------------------------------------------------- -""" - -def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0): - global segmentos_ppii_global - - if not pdb_file: - messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") - return - - phi_psi_map = obtener_angulos_phi_psi_por_cadena(objeto) - lista_residuos = [] - for (chain, resi), (resn, phi, psi) in phi_psi_map.items(): - try: - resi_num = int(resi) - except: - continue - lista_residuos.append((chain, resi_num, resn, phi, psi)) - lista_residuos.sort(key=lambda x: (x[0], x[1])) - - segmentos = [] - segmento_actual = [] - saltos_restantes = max_saltos # Contador de saltos permitidos - - def en_rango_ppii(phi, psi, tol_phi=20.0, tol_psi=20.0): - # Valores IDEALES para PPII: φ = -75°, ψ = +145° - return (abs(phi - (-75)) <= tol_phi) and (abs(psi - 145) <= tol_psi) - - for i, (chain, resi, resn, phi, psi) in enumerate(lista_residuos): - if en_rango_ppii(phi, psi, tol_phi, tol_psi): - # Si cumple, reiniciamos el contador de saltos - saltos_restantes = max_saltos - if not segmento_actual: - segmento_actual.append((resn, resi, chain, phi, psi)) - else: - _, last_resi, last_chain, _, _ = segmento_actual[-1] - if chain == last_chain and resi == last_resi + 1: - segmento_actual.append((resn, resi, chain, phi, psi)) - else: - if len(segmento_actual) >= min_length: - segmentos.append(segmento_actual) - segmento_actual = [(resn, resi, chain, phi, psi)] - else: - # Si no cumple pero hay saltos disponibles - if segmento_actual and saltos_restantes > 0: - _, last_resi, last_chain, _, _ = segmento_actual[-1] - if chain == last_chain and resi == last_resi + 1: - segmento_actual.append((resn, resi, chain, phi, psi)) - saltos_restantes -= 1 - continue - - # Si no hay saltos disponibles o no es consecutivo - if len(segmento_actual) >= min_length: - segmentos.append(segmento_actual) - segmento_actual = [] - saltos_restantes = max_saltos - - # Añadir el último segmento si cumple con la longitud mínima - if len(segmento_actual) >= min_length: - segmentos.append(segmento_actual) - - if not segmentos: - messagebox.showinfo("Resultado", "No se encontraron segmentos PPII.") - return - - cmd.delete("ppii_segmento*") - - salida = f"Segmentos candidatos a hélices PPII (saltos permitidos: {max_saltos}):\n" - for idx, seg in enumerate(segmentos, start=1): - start_resi = seg[0][1] - end_resi = seg[-1][1] - chain = seg[0][2] - salida += f"\nSegmento {idx} (Cadena {chain}, residuos {start_resi}-{end_resi}, longitud {len(seg)}):\n" - for (resn, resi, _, phi, psi) in seg: - salida += f" {resn}-{resi}{chain}: (phi={phi:.1f}, psi={psi:.1f})\n" - - sel_str = f"proteina and chain {chain} and resi {start_resi}-{end_resi}" - obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" - cmd.create(obj_name, sel_str) - cmd.color("red", obj_name) - cmd.show("cartoon", obj_name) - - carpeta = obtener_carpeta_resultados() - ruta_archivo = carpeta / "segmentos_ppii.txt" - with open(ruta_archivo, "w") as f: - f.write(salida) - - segmentos_ppii_global = segmentos - - messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados (con {max_saltos} saltos permitidos).\n" - f"Átomos clave visualizados en PyMOL.") - -# ========================================================== -# >>>>>>>> FUNCIÓN: GUARDAR_SEGMENTO_PPII_PDB <<<<<<<< -# ========================================================== - -""" -guardar_segmento_ppii_pdb(segmento, nombre_archivo="segmento_ppii.pdb") ----------------------------------------------------------- -Descripción: - Guarda en un archivo PDB un segmento específico de hélice - PPII previamente detectado en la proteína cargada en PyMOL. - El archivo se genera con el nombre especificado dentro de la - carpeta de resultados. - -Parámetros: - segmento : list - Lista con los residuos que forman el segmento PPII a guardar. - nombre_archivo : str, opcional - Nombre del archivo PDB que se generará. Por defecto es - "segmento_ppii.pdb". - -Retorno: - None - -Ejemplo de uso: - >>> guardar_segmento_ppii_pdb(segmentos_ppii[0]) ----------------------------------------------------------- -""" - -def guardar_segmentos_ppii_pdb(): - global segmentos_ppii_global - carpeta = obtener_carpeta_resultados() - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - # Para cada segmento almacenado, construimos el nombre de objeto y hacemos cmd.save - count = 0 - for seg in segmentos_ppii_global: - start_resi = seg[0][1] - end_resi = seg[-1][1] - chain = seg[0][2] - obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" - # Comprobamos que el objeto efectivamente exista en PyMOL - if cmd.count_atoms(f"{obj_name}") > 0: - - filename = carpeta / f"{obj_name}.pdb" - try: - cmd.save(filename, obj_name) - count += 1 - except Exception as e: - print(f"Error guardando {obj_name}: {e}") - else: - print(f"Objeto {obj_name} no encontrado en la sesión de PyMOL.") - - if count > 0: - messagebox.showinfo("Éxito", f"Se guardaron {count} archivos PDB de segmentos PPII:\n" - f"{os.getcwd()}") - else: - messagebox.showwarning("Atención", "No se guardó ningún segmento (quizá no existan objetos en PyMOL).") - -# ========================================================== -# >>>>>>>> FUNCIÓN: CONVERTIR_A_SELECCIONES_PYMOL <<<<<<<< -# ========================================================== - -""" -convertir_a_selecciones_pymol(pares_con_distancias) ----------------------------------------------------------- -Descripción: - Convierte una lista de pares de átomos con sus distancias en - selecciones de PyMOL. Cada par se representa como una selección - que facilita la visualización y el análisis de las interacciones - detectadas. - -Parámetros: - pares_con_distancias : list - Lista de tuplas en el formato (atomo1, atomo2, distancia) - que serán convertidas en selecciones de PyMOL. - -Retorno: - list - Lista con los nombres de las selecciones creadas en PyMOL. - -Ejemplo de uso: - >>> selecciones = convertir_a_selecciones_pymol(pares_con_distancias) ----------------------------------------------------------- -""" - -def convertir_a_selecciones_pymol(pares_con_distancias): - selecciones = [] - for at1, at2, _ in pares_con_distancias: - sele1 = f"id {cmd.index(at1)[0][1]}" - sele2 = f"id {cmd.index(at2)[0][1]}" - selecciones.append((sele1, sele2)) - return selecciones - -# ========================================================== -# >>>>>>>> FUNCIÓN: CALCULAR_Y_VISUALIZAR_DISTANCIAS <<<<<<<< -# ========================================================== - -""" -calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0) ----------------------------------------------------------- -Descripción: - Calcula las distancias entre átomos Cα-O de segmentos PPII - consecutivos y los ángulos CA-H-O asociados. Visualiza en - PyMOL los pares de átomos que cumplen con los criterios de - distancia y ángulo definidos por el usuario. Los pares - válidos se representan con líneas discontinuas (dashed lines) - en color cian para facilitar el análisis. - -Parámetros: - max_dist : float, opcional - Distancia máxima (en Ångström) para considerar los pares - Cα-O como colindantes. Por defecto es 5.0 Å. - min_ang : float, opcional - Ángulo mínimo (en grados) para considerar válido un ángulo - CA-H-O. Por defecto es 110.0°. - max_ang : float, opcional - Ángulo máximo (en grados) para considerar válido un ángulo - CA-H-O. Por defecto es 180.0°. - -Retorno: - None - -Ejemplo de uso: - >>> calcular_y_visualizar_distancias() ----------------------------------------------------------- -""" - -def calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0): - global segmentos_ppii_global - global objetos, pares - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - if objetos is None: - objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) - if pares is None: - pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) - - calcular_angulos_ca_h_o(pares, min_ang=min_ang, max_ang=max_ang) - -# ========================================================== -# >>>>>>>> FUNCIÓN: DISTANCIAS_P <<<<<<<< -# ========================================================== - -""" -distancias_p() ----------------------------------------------------------- -Descripción: - Visualiza en PyMOL todas las distancias entre los pares - de átomos Cα-O previamente calculados y almacenados. - Cada distancia se dibuja como una línea discontinua - (dashed line) para facilitar el análisis estructural - de la proteína. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> distancias_p() ----------------------------------------------------------- -""" - -def distancias_p(): - global segmentos_ppii_global - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) - pares = calcular_distancias_colindantes(objetos) - visualizar_distancias_pares(pares) - -# ========================================================== -# >>>>>>>> FUNCIÓN: LANZAR_INTERFAZ <<<<<<<< -# ========================================================== - -""" -lanzar_interfaz() ----------------------------------------------------------- -Descripción: - Inicia la interfaz gráfica de usuario (GUI) desarrollada - con Tkinter para facilitar la interacción con el programa. - Permite acceder a las funciones principales como cargar - archivos PDB, detectar hélices PPII, añadir hidrógenos, - eliminar solventes y generar reportes, todo desde un menú - visual. - -Parámetros: - Ninguno - -Retorno: - None - -Ejemplo de uso: - >>> lanzar_interfaz() ----------------------------------------------------------- -""" - -def lanzar_interfaz(): - root = tk.Tk() - root.title("PPIIMoL: PPII Detect") - root.geometry("450x1000") - root.resizable(False, False) - - style = ttk.Style(root) - style.theme_use('classic') # Puedes probar: 'alt', 'clam', 'default', 'classic' - - main_frame = ttk.Frame(root, padding=5) - main_frame.pack(fill="both", expand=True) - - def wrapper_detectar_segmentos_ppii(): - try: - tol_phi = float(entrada_tol_phi.get()) - tol_psi = float(entrada_tol_psi.get()) - max_saltos = int(entrada_saltos.get()) # Obtener el valor del campo de saltos - if max_saltos < 0 or max_saltos > 5: # Validar rango (0-5) - raise ValueError - except ValueError: - messagebox.showerror("Error", "¡Saltos debe ser un entero entre 0 y 5!") - return - - detectar_segmentos_ppii(tol_phi=tol_phi, tol_psi=tol_psi, max_saltos=max_saltos) # Pasar el parámetro - -# En la función lanzar_interfaz(), añade este frame antes del frame de ángulos CA-H-O - # Frame para parámetros phi/psi - phi_psi_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulos phi/psi", padding=10) - phi_psi_frame.pack(fill="x", pady=5) - ttk.Label(phi_psi_frame, text="Actualmente sin la tolerancia los angulos ").pack(anchor="w") - ttk.Label(phi_psi_frame, text="por defecto que se miden son hasta: phi 75 y psi 145").pack(anchor="w") - - ttk.Label(phi_psi_frame, text="Tolerancia para phi (±°):").pack(anchor="w") - entrada_tol_phi = ttk.Entry(phi_psi_frame) - entrada_tol_phi.insert(0, "20.0") - entrada_tol_phi.pack(fill="x", pady=2) - - ttk.Label(phi_psi_frame, text="Tolerancia para psi (±°): ").pack(anchor="w") - entrada_tol_psi = ttk.Entry(phi_psi_frame) - entrada_tol_psi.insert(0, "20.0") - entrada_tol_psi.pack(fill="x", pady=2) - - - saltos_frame = ttk.LabelFrame(main_frame, text="Parámetros de saltos", padding=5) - saltos_frame.pack(fill="x", pady=3) - - ttk.Label(saltos_frame, text="Saltos permitidos (0-5):").pack(anchor="w") - entrada_saltos = ttk.Entry(saltos_frame) - entrada_saltos.insert(0, "0") # Valor por defecto: 0 saltos - entrada_saltos.pack(fill="x", pady=1) - - - def wrapper_generar_reporte_csv(): - try: - min_ang = float(entrada_min_ang.get()) - max_ang = float(entrada_max_ang.get()) - except ValueError: - messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") - return - - if not segmentos_ppii_global: - messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") - return - - generar_reporte_csv(segmentos_ppii_global, min_ang=min_ang, max_ang=max_ang) - #cambiar por otra funcion envoltorio diferente - - # Función envoltorio para pasar los valores - def wrapper_calcular_y_visualizar(): - try: - min_ang = float(entrada_min_ang.get()) - max_ang = float(entrada_max_ang.get()) - except ValueError: - messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") - return - calcular_y_visualizar_distancias(min_ang=min_ang, max_ang=max_ang) - - # Botones funcionales - botones = [ - ("Seleccionar archivo PDB", seleccionar_archivo), - ("Descargar proteína", descargar_molecula), - ("Eliminar solventes", eliminar_solventes), - ("Añadir hidrógenos", anadir_hidrogenos), - ("Ocultar cadenas laterales", ocultar_side_chains), - ("Guardar ángulos phi/psi en archivo", guardar_csv_angulos_phi_psi), - ("Detectar segmentos PPII y resaltarlos", wrapper_detectar_segmentos_ppii), - ("Guardar segmentos PPII en PDB", guardar_segmentos_ppii_pdb), - ("Visualizar distancias", distancias_p), - ("Angulos entre CA-O-H colindantes", wrapper_calcular_y_visualizar), - ("Generar reporte completo (CSV)", wrapper_generar_reporte_csv), - ] - - for texto, accion in botones: - ttk.Button(main_frame, text=texto, command=accion).pack(fill="x", pady=1) - - # Entradas de ángulos - ang_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulo CA-H-O", padding=3) - ang_frame.pack(fill="x", pady=3) - - ttk.Label(ang_frame, text="Ángulo mínimo (°):").pack(anchor="w") - entrada_min_ang = ttk.Entry(ang_frame) - entrada_min_ang.insert(0, "110.0") - entrada_min_ang.pack(fill="x", pady=1) - - ttk.Label(ang_frame, text="Ángulo máximo (°):").pack(anchor="w") - entrada_max_ang = ttk.Entry(ang_frame) - entrada_max_ang.insert(0, "180.0") - entrada_max_ang.pack(fill="x", pady=1) - - root.mainloop() - - -lanzar_interfaz() diff --git a/PPIIMol/README_EN.md b/PPIIMol/README_EN.md deleted file mode 100644 index 0e87023..0000000 --- a/PPIIMol/README_EN.md +++ /dev/null @@ -1,47 +0,0 @@ -# PPIIMoL – Automated Detection of PPII Helices in PyMOL - -**PPIIMoL** is a Python module designed to integrate directly into PyMOL for the automatic detection of Polyproline II (PPII) helices in protein structures. - -PPII helices, often present in glycine- and proline-rich proteins and relevant in neurobiological processes, are typically not explicitly annotated in PDB files. This tool identifies geometric patterns compatible with PPII helices and displays them directly in PyMOL, streamlining structural analysis. - -## 🔬 Key Features - -- Load local PDB files or fetch structures directly within PyMOL. -- Automatic removal of solvents and hydrogen addition. -- Calculation of φ and ψ angles for each residue. -- Detection of segments consistent with PPII conformation. -- Direct visualization of results in PyMOL using pseudoatoms. -- Export of detected segments to `.csv` and `.pdb` formats for external analysis. - -## 📦 Requirements - -- [PyMOL](https://pymol.org/) (version with Python scripting support) -- Python 3.8+ -- Standard Python libraries (`math`, `tkinter`, `csv`, `os`) - -## 🚀 Usage - -1. Launch PyMOL with Python scripting support. -2. Run the script `PPIIMoL.py` via the PyMOL command line or graphical interface. -3. Load or fetch a protein structure. -4. Add hydrogens and perform the analysis. -5. Visualize the detected PPII helices and/or export the results. - -## 🧪 Test Case - -The module has been validated using the 3BOG protein, a well-known model with a high presence of PPII helices, confirming its accuracy and utility for structural research. - -## 📄 License - -Released under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html). - -## ✍️ Author - -This module was developed as part of a Bachelor's Thesis in Computer Engineering (UNIR), in collaboration with the Protein Structure, Dynamics and Interactions Group at the Institute of Physical Chemistry “Blas Cabrera” (IQF-CSIC). - -Author: Silvia Enma Rodríguez Fernández -GitHub: [@silviaenma](https://github.com/silviaenma) - ---- - -*Suggestions, contributions, and improvements are welcome.* diff --git a/PPIIMol/README_ES.md b/PPIIMol/README_ES.md deleted file mode 100644 index e529698..0000000 --- a/PPIIMol/README_ES.md +++ /dev/null @@ -1,47 +0,0 @@ -# PPIIMoL - Detección automática de hélices PPII en PyMOL - -**PPIIMoL** es un módulo en Python diseñado para integrarse directamente con PyMOL y automatizar la detección de hélices de Poliprolina II (PPII) en estructuras proteicas. - -Estas hélices, relevantes en procesos neurobiológicos y en proteínas ricas en glicina y prolina, no suelen estar identificadas explícitamente en los archivos PDB. Este módulo permite detectar automáticamente patrones geométricos compatibles con hélices PPII y visualizarlos en PyMOL, facilitando su análisis estructural. - -## 🔬 Funcionalidades principales - -- Carga de archivos PDB o descarga directa desde PyMOL. -- Eliminación automática de solventes y adición de hidrógenos. -- Cálculo de ángulos φ y ψ para cada residuo. -- Detección de segmentos con conformación PPII. -- Visualización directa en PyMOL mediante pseudóatomos. -- Exportación de resultados a `.csv` y `.pdb` para análisis externo. - -## 📦 Requisitos - -- [PyMOL](https://pymol.org/) (versión con soporte de scripts Python) -- Python 3.8+ -- Módulos estándar de Python (`math`, `tkinter`, `csv`, `os`) - -## 🚀 Cómo usar - -1. Abre PyMOL con soporte de scripts. -2. Ejecuta el módulo `PPIIMoL.py` desde la consola de PyMOL o usa el GUI incluido. -3. Carga o descarga una proteína. -4. Añade hidrógenos y ejecuta el análisis. -5. Visualiza las hélices PPII detectadas y/o exporta los datos. - -## 🧪 Caso de prueba - -La herramienta ha sido validada utilizando la proteína 3BOG, una estructura rica en hélices PPII, demostrando su eficacia y utilidad para análisis sistemático. - -## 📄 Licencia - -Publicado bajo licencia [GPL v3.0](https://www.gnu.org/licenses/gpl-3.0.html). - -## ✍️ Autoría - -Este módulo ha sido desarrollado como parte de un Trabajo de Fin de Grado en Ingeniería Informática (UNIR), en colaboración con el grupo de investigación del Instituto de Química-Física “Blas Cabrera” (IQF-CSIC). - -Autora: Silvia Enma Rodríguez Fernández -GitHub: [@silviaenma](https://github.com/silviaenma) - ---- - -*Contribuciones, sugerencias y mejoras son bienvenidas.* From 62a03d317bcbca57913065265cfc1167fd5a40b9 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Tue, 22 Jul 2025 11:21:22 +0200 Subject: [PATCH 05/19] Create __init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añadir módulo PPIIMoL a modules/ --- modules/ppiimol/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules/ppiimol/__init__.py diff --git a/modules/ppiimol/__init__.py b/modules/ppiimol/__init__.py new file mode 100644 index 0000000..9ec846f --- /dev/null +++ b/modules/ppiimol/__init__.py @@ -0,0 +1 @@ +from .ppiimol import * From 1cf047242791aaa6c5807cf1b7fd8485bf4788bc Mon Sep 17 00:00:00 2001 From: silviaenma Date: Tue, 22 Jul 2025 11:31:34 +0200 Subject: [PATCH 06/19] Add files via upload --- modules/ppiimol/PPIIMoL.py | 1285 ++++++++++++++++++++++++++++++++++++ modules/ppiimol/README.md | 97 +++ 2 files changed, 1382 insertions(+) create mode 100644 modules/ppiimol/PPIIMoL.py create mode 100644 modules/ppiimol/README.md diff --git a/modules/ppiimol/PPIIMoL.py b/modules/ppiimol/PPIIMoL.py new file mode 100644 index 0000000..afe7457 --- /dev/null +++ b/modules/ppiimol/PPIIMoL.py @@ -0,0 +1,1285 @@ +from pymol import cmd, stored +import tkinter as tk +import math +from pathlib import Path +from datetime import datetime +import os +from tkinter import filedialog, messagebox +import tkinter as tk +from tkinter import ttk, messagebox +import os +import csv + + +# Valores por defecto (se actualizarán cuando el usuario los cambie) +tol_phi_global = 20.0 +tol_psi_global = 20.0 +min_ang_global = 110.0 +max_ang_global = 180.0 + + +pdb_file = None +segmentos_ppii_global = [] # Variable global para guardar segmentos PPII detectados +localizar_atomos=None +distancias=None +angulos_v=None +objetos = None +pares = None + +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_CARPETA_RESULTADOS <<<<<<<< +# ========================================================== + +""" +obtener_carpeta_resultados(nombre_carpeta='Resultados_PyMOL') +---------------------------------------------------------- +Descripción: + Crea una carpeta con fecha actual dentro del directorio Documentos + (o equivalente) del usuario para guardar los resultados generados + por el módulo. + +Parámetros: + nombre_carpeta : str, opcional + Nombre base para la carpeta de resultados (por defecto 'Resultados_PyMOL'). + +Retorno: + pathlib.Path + Ruta completa a la carpeta creada. + +Ejemplo de uso: + >>> carpeta = obtener_carpeta_resultados("Resultados_PPIIMoL") + >>> print(carpeta) + /home/usuario/Documentos/Resultados_PPIIMoL_2025-07-05 +---------------------------------------------------------- +""" + +def obtener_carpeta_resultados(nombre_carpeta="Resultados_PyMOL"): + # Formatear fecha y hora actual (compatible con ambos sistemas) + fecha_hora = datetime.now().strftime("%Y-%m-%d") + + # Usar ~/Documents o ~/Documentos según el sistema + documentos = Path.home() / ("Documentos" if os.getenv('LANG', '').startswith('es_') else "Documents") + + # Si no existe, crear directorio en HOME directamente + if not documentos.exists(): + documentos = Path.home() + + # Crear nombre de carpeta combinado + nombre_completo = f"{nombre_carpeta}_{fecha_hora}" + carpeta_resultados = documentos / nombre_completo + + # Crear la carpeta (con parents=True por si faltan directorios padres) + carpeta_resultados.mkdir(parents=True, exist_ok=True) + + return carpeta_resultados + +# ========================================================== +# >>>>>>>> FUNCIÓN: SELECCIONAR_ARCHIVO <<<<<<<< +# ========================================================== + +""" +seleccionar_archivo() +---------------------------------------------------------- +Descripción: + Abre un cuadro de diálogo para que el usuario seleccione un archivo PDB + de entrada. La ruta seleccionada se guarda en la variable global pdb_file. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> seleccionar_archivo() +---------------------------------------------------------- +""" + +def seleccionar_archivo(): + global pdb_file + pdb_file = filedialog.askopenfilename( + title="Selecciona un archivo PDB", + filetypes=[("Archivos PDB", "*.pdb")] + ) + if pdb_file: + cmd.reinitialize() + cmd.load(pdb_file, "proteina") + cmd.hide("everything", "proteina") # Ocultar todo lo visible + cmd.show("licorice", "proteina") # Mostrar como licorice + messagebox.showinfo("Archivo cargado", f"Se cargó:\n{pdb_file}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: DESCARGAR_MOLECULA <<<<<<<< +# ========================================================== + +""" +descargar_molecula() +---------------------------------------------------------- +Descripción: + Abre una ventana emergente para permitir al usuario introducir un ID + de proteína del Protein Data Bank (PDB) y descargar automáticamente + la estructura correspondiente. Una vez descargada, la molécula se + carga en PyMOL, se ocultan todas las representaciones gráficas + actuales y se muestra la proteína usando el estilo "licorice". + La ruta o ID de la proteína descargada se guarda en la variable global + pdb_file. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> descargar_molecula() +---------------------------------------------------------- +""" + +def descargar_molecula(): + global pdb_file + def fetch_pdb(): + global pdb_file + pdb_id = entry.get().strip() + if pdb_id: + try: + cmd.reinitialize() + cmd.fetch(pdb_id, name="proteina") + cmd.hide("everything", "proteina") + cmd.show("licorice", "proteina") + # Aquí asignamos pdb_file con un valor para simular archivo cargado + pdb_file = pdb_id # Solo guardamos el ID para que funcione la lógica de “selección” + messagebox.showinfo("Descarga completa", f"Molécula {pdb_id.upper()} descargada y cargada.") + fetch_window.destroy() + except Exception as e: + messagebox.showerror("Error", f"No se pudo descargar {pdb_id}: {e}") + else: + messagebox.showwarning("Advertencia", "Por favor ingresa un ID de PDB.") + + fetch_window = tk.Toplevel() + fetch_window.title("Descargar proteína") + tk.Label(fetch_window, text="ID de la proteína (PDB):").pack(pady=5) + entry = tk.Entry(fetch_window, width=20) + entry.pack(pady=5) + tk.Button(fetch_window, text="Descargar", command=fetch_pdb).pack(pady=5) + +# ========================================================== +# >>>>>>>> FUNCIÓN: ANADIR_HIDROGENOS <<<<<<<< +# ========================================================== + +""" +anadir_hidrogenos() +---------------------------------------------------------- +Descripción: + Añade átomos de hidrógeno a todas las moléculas cargadas + en la sesión actual de PyMOL. Una vez añadidos, la + estructura se reorganiza para optimizar la visualización + y se muestra usando el estilo "licorice". Si no hay + ningún archivo PDB cargado previamente, se muestra una + advertencia al usuario. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> anadir_hidrogenos() +---------------------------------------------------------- +""" + +def anadir_hidrogenos(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + cmd.h_add("all") + cmd.sort("all extend 1") + cmd.show("licorice", "all") + messagebox.showinfo("Hidrógenos", "Hidrógenos añadidos y mostrados como licorice.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: ELIMINAR_SOLVENTES <<<<<<<< +# ========================================================== + +""" +eliminar_solventes() +---------------------------------------------------------- +Descripción: + Elimina todas las moléculas de solvente presentes en la + estructura cargada en PyMOL. Tras la eliminación, se + muestra un mensaje informativo al usuario confirmando + la acción realizada. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> eliminar_solventes() +---------------------------------------------------------- +""" + +def eliminar_solventes(): + cmd.remove("solvent") + messagebox.showinfo("Solventes", "Solventes eliminados.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: OCULTAR_SIDE_CHAINS <<<<<<<< +# ========================================================== + +""" +ocultar_side_chains() +---------------------------------------------------------- +Descripción: + Oculta todas las cadenas laterales de los residuos de + la proteína cargada en PyMOL, dejando visibles únicamente + los átomos del esqueleto principal (backbone: N, CA, C, O). + Esta función permite centrar la visualización en la + estructura principal de la proteína sin distracciones. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> ocultar_side_chains() +---------------------------------------------------------- +""" + +def ocultar_side_chains(): + cmd.hide("everything", "proteina and not name N+CA+C+O") + messagebox.showinfo("Backbone", "Cadenas laterales ocultas (solo backbone).") + +# ========================================================== +# >>>>>>>> FUNCIÓN: SEPARAR_CADENAS <<<<<<<< +# ========================================================== + +""" +separar_cadenas() +---------------------------------------------------------- +Descripción: + Separa cada cadena de la proteína cargada en PyMOL + en objetos individuales. Cada nueva cadena se guarda + como un objeto separado con un nombre identificador + en el formato "cadena_X", donde X es el identificador + de la cadena original. Esta función facilita la + manipulación y análisis individual de las cadenas. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> separar_cadenas() +---------------------------------------------------------- +""" + +def separar_cadenas(): + stored.chains = [] + cmd.iterate("proteina", "stored.chains.append(chain)") + for cadena in set(stored.chains): + nuevo_objeto = f"cadena_{cadena}" + cmd.create(nuevo_objeto, f"proteina and chain {cadena}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: OBTENER_ANGULOS_PHI_PSI_POR_CADENA <<<<<<<< +# ========================================================== + +""" +obtener_angulos_phi_psi_por_cadena(objeto="proteina") +---------------------------------------------------------- +Descripción: + Calcula los ángulos torsionales phi (ϕ) y psi (ψ) de cada + residuo en todas las cadenas del objeto especificado + cargado en PyMOL. Los resultados se devuelven en un + diccionario que asocia cada residuo con su nombre, número + y los valores de los ángulos phi y psi correspondientes. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el cual se + calcularán los ángulos (por defecto "proteina"). + +Retorno: + dict + Diccionario con las claves como tuplas (cadena, número + de residuo) y valores como tuplas (nombre del residuo, + ángulo phi, ángulo psi). + +Ejemplo de uso: + >>> resultados = obtener_angulos_phi_psi_por_cadena() + >>> print(resultados) +---------------------------------------------------------- +""" + +def obtener_angulos_phi_psi_por_cadena(objeto="proteina"): + resultados = {} + chains = cmd.get_chains(objeto) + for chain in chains: + sel_ca = f"{objeto} and chain {chain} and name CA" + phipsi = cmd.get_phipsi(sel_ca) + if not phipsi: + continue + for (obj, idx), (phi, psi) in sorted(phipsi.items()): + if phi is None or psi is None: + continue + stored.info = [] + cmd.iterate(f"({obj}`{idx})", "stored.info.append((chain, resn, resi))", space={'stored': stored}) + if not stored.info: + continue + ch, resn, resi = stored.info[0] + resultados[(ch, resi)] = (resn, phi, psi) + return resultados + +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_CSV_ANGULOS_PHI_PSI <<<<<<<< +# ========================================================== + +""" +guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con los ángulos torsionales phi (ϕ) + y psi (ψ) calculados para cada residuo de la proteína + cargada en PyMOL. El archivo se guarda en una carpeta + de resultados con el nombre "angulos_phi_psi.csv" e + incluye información de la cadena, el residuo, su número + y los valores de los ángulos. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> guardar_csv_angulos_phi_psi() +---------------------------------------------------------- +""" + +def guardar_csv_angulos_phi_psi(): + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_map = obtener_angulos_phi_psi_por_cadena("proteina") + + stored.res_list = [] + cmd.iterate("proteina and name CA", "stored.res_list.append((chain, resn, resi))") + + datos_csv = [("Cadena", "Residuo", "Número", "Phi", "Psi")] + + for chain, resn, resi in sorted(stored.res_list, key=lambda x: (x[0], int(x[2]))): + key = (chain, resi) + if key in phi_map: + resn_val, phi, psi = phi_map[key] + datos_csv.append((chain, resn, resi, f"{phi:.2f}", f"{psi:.2f}")) + carpeta= obtener_carpeta_resultados() + # Guardar en CSV con separador ; + if len(datos_csv) > 1: + ruta_csv = os.path.join(os.getcwd(), carpeta/"angulos_phi_psi.csv") + with open(ruta_csv, mode="w", newline="", encoding="utf-8") as file: + writer = csv.writer(file, delimiter=";") + writer.writerows(datos_csv) + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GENERAR_REPORTE_CSV <<<<<<<< +# ========================================================== + +""" +generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Genera un archivo CSV con un informe detallado sobre los + segmentos de hélices PPII detectados en la proteína cargada + en PyMOL. El reporte incluye pares de átomos Cα-O colindantes, + sus distancias, los ángulos CA-H-O calculados y los valores + validados según los rangos definidos. El archivo se guarda + en una carpeta de resultados con el nombre especificado. + +Parámetros: + segmentos_ppii : list + Lista de segmentos PPII detectados en la proteína. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los + pares de átomos colindantes. Por defecto es 5.0 Å. + nombre_archivo : str, opcional + Nombre del archivo CSV que se generará. Por defecto + es "reporte_ppii.csv". + min_ang : float, opcional + Ángulo mínimo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo en grados para filtrar los ángulos CA-H-O + válidos. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> generar_reporte_csv(segmentos_ppii) +---------------------------------------------------------- +""" + +def generar_reporte_csv(segmentos_ppii, max_dist=5.0, nombre_archivo="reporte_ppii.csv", min_ang=110.0, max_ang=180.0): + if not segmentos_ppii: + messagebox.showwarning("Advertencia", "No hay segmentos PPII detectados.") + return + + objetos_por_segmento = localizar_atomos + pares_ca_o=distancias + angulos_validos=angulos_v + if objetos_por_segmento ==None: + objetos_por_segmento = localizar_atomicos_clave_segmentos(segmentos_ppii) + if pares_ca_o is None: + pares_ca_o = calcular_distancias_colindantes(objetos_por_segmento, max_dist=max_dist) + + # PASAMOS LOS ÁNGULOS AQUÍ + if angulos_validos is None: + angulos_validos = calcular_angulos_ca_h_o(pares_ca_o, min_ang=min_ang, max_ang=max_ang) + + dist_dict = {} + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): + continue + coord_ca = cmd.get_coords(ca)[0] + coord_o = cmd.get_coords(o)[0] + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coord_ca, coord_o))) + dist_dict[(ca, o)] = dist + + ang_dict = {(ca, o): ang for ca, _, o, ang in angulos_validos} + + datos_csv = [["Átomo Cα", "Átomo O", "Distancia (Å)", "Ángulo CA-H-O (°)"]] + for (ca, o), dist in dist_dict.items(): + ang = ang_dict.get((ca, o), None) + if ang is not None: + datos_csv.append([ca, o, f"{dist:.2f}", f"{ang:.2f}"]) + + ruta_csv = obtener_carpeta_resultados() / nombre_archivo + with open(ruta_csv, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f, delimiter=';') + writer.writerows(datos_csv) + + messagebox.showinfo("Éxito", f"CSV generado correctamente:\n{ruta_csv}") + +# ========================================================== +# >>>>>>>> FUNCIÓN: LOCALIZAR_ATOMICOS_CLAVE_SEGMENTOS <<<<<<<< +# ========================================================== + +""" +localizar_atomicos_clave_segmentos(segmentos) +---------------------------------------------------------- +Descripción: + Localiza y marca en PyMOL los átomos clave (Cα y O) de + cada segmento PPII detectado. Crea pseudoátomos en las + posiciones correspondientes para facilitar la visualización + y análisis. Los átomos Cα se marcan en color azul y los O + en color rojo, agrupándolos bajo el nombre "atomos_clave". + +Parámetros: + segmentos : list + Lista de segmentos PPII detectados en la proteína. + +Retorno: + list + Lista de listas, donde cada sublista contiene los + nombres de los pseudoátomos creados para un segmento. + +Ejemplo de uso: + >>> localizar_atomicos_clave_segmentos(segmentos_ppii) +---------------------------------------------------------- +""" + +def localizar_atomicos_clave_segmentos(segmentos): + global localizar_atomos + cmd.delete("esfera_*") + objetos_por_segmento = [] + cmd.h_add() + + for idx, seg in enumerate(segmentos, start=1): + objetos_segmento = [] + + for (resn, resi, chain, _, _) in seg: + sele_base = f"proteina and chain {chain} and resi {resi}" + stored.coords = [] + + # Átomo CA (carbono alfa) + stored.ca_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name CA", + "stored.ca_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Oxígeno carbonilo + stored.o_coords = [] + cmd.iterate_state( + 1, + f"{sele_base} and name O", + "stored.o_coords.append((x, y, z))", + space={'stored': stored} + ) + + # Crear pseudoatomos con nombres consistentes + if stored.ca_coords: + x, y, z = stored.ca_coords[0] + esfera_name = f"esfera_s{idx}_CA_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("blue", esfera_name) + objetos_segmento.append(esfera_name) + + if stored.o_coords: + x, y, z = stored.o_coords[0] + esfera_name = f"esfera_s{idx}_O_{resn}_{resi}_{chain}" + cmd.pseudoatom(esfera_name, pos=[x, y, z]) + cmd.set("sphere_scale", 0.3, esfera_name) + cmd.color("red", esfera_name) + objetos_segmento.append(esfera_name) + + objetos_por_segmento.append(objetos_segmento) + + cmd.group("atomos_clave", "esfera_*") + localizar_atomos = objetos_por_segmento + return objetos_por_segmento + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_DISTANCIAS_COLINDANTES <<<<<<<< +# ========================================================== + +""" +calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt") +---------------------------------------------------------- +Descripción: + Calcula las distancias entre los átomos Cα y O de segmentos + PPII consecutivos o cercanos. Identifica los pares cuya + distancia es menor al valor máximo especificado. Guarda los + resultados en un archivo de texto y devuelve la lista de + pares de átomos que cumplen el criterio. + +Parámetros: + objetos_por_segmento : list + Lista de listas con los nombres de los pseudoátomos + (Cα y O) de cada segmento. + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán las + distancias calculadas. Por defecto es + "distancias_colindantes.txt". + +Retorno: + list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) cuya distancia es menor a max_dist. + +Ejemplo de uso: + >>> pares = calcular_distancias_colindantes(objetos_por_segmento) +---------------------------------------------------------- +""" + +def calcular_distancias_colindantes(objetos_por_segmento, max_dist=5.0, archivo_salida="distancias_colindantes.txt"): + global distancias + pares_ca_o = [] + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / archivo_salida + coordenadas = {} + + for segmento in objetos_por_segmento: + for obj in segmento: + coords = cmd.get_coords(obj) + if coords is None or len(coords) == 0: + continue + coordenadas[obj] = coords[0] + + def es_CA(obj): return "_CA_" in obj + def es_O(obj): return "_O_" in obj + + max_dist_sq = max_dist ** 2 + + for salto in [1, 2]: + for i in range(len(objetos_por_segmento) - salto): + seg1 = objetos_por_segmento[i] + seg2 = objetos_por_segmento[i + salto] + + ca1 = [obj for obj in seg1 if es_CA(obj)] + o2 = [obj for obj in seg2 if es_O(obj)] + ca2 = [obj for obj in seg2 if es_CA(obj)] + o1 = [obj for obj in seg1 if es_O(obj)] + + # Comparar ca1 con o2 + for ca in ca1: + coord_ca = coordenadas.get(ca) + if coord_ca is None: + continue + for o in o2: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) + + # Comparar ca2 con o1 + for ca in ca2: + coord_ca = coordenadas.get(ca) + if coord_ca is None: + continue + for o in o1: + coord_o = coordenadas.get(o) + if coord_o is None: + continue + dist_sq = sum((a - b) ** 2 for a, b in zip(coord_ca, coord_o)) + if dist_sq < max_dist_sq: + pares_ca_o.append((ca, o)) + + with open(archivo_salida, "w") as f: + f.write(f"Pares CA-O con distancia < {max_dist} Å:\n") + for i, (ca, o) in enumerate(pares_ca_o): + dist = math.sqrt(sum((a - b)**2 for a, b in zip(coordenadas[ca], coordenadas[o]))) + f.write(f"{i:03d} | {ca} - {o} : {dist:.2f} Å\n") + + print(f"[DEBUG] Total de pares CA-O guardados: {len(pares_ca_o)}") + distancias= pares_ca_o + return pares_ca_o + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_ANGULOS_CA_H_O <<<<<<<< +# ========================================================== + +""" +calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt") +---------------------------------------------------------- +Descripción: + Calcula los ángulos formados entre los átomos Cα-H-O para + cada par de átomos Cα-O proporcionado. Filtra y guarda + únicamente los ángulos que estén dentro del rango definido + por min_ang y max_ang. Los resultados se almacenan en un + archivo de texto y se devuelven como una lista. + +Parámetros: + pares_ca_o : list + Lista de tuplas con los nombres de los pares de átomos + (Cα, O) sobre los que se calcularán los ángulos. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un + ángulo CA-H-O. Por defecto es 180.0°. + archivo_salida : str, opcional + Nombre del archivo de texto donde se guardarán los + ángulos calculados. Por defecto es "angulos_ca_h_o.txt". + +Retorno: + list + Lista de tuplas con la información de cada ángulo válido + en el formato (Cα, H, O, ángulo). + +Ejemplo de uso: + >>> angulos = calcular_angulos_ca_h_o(pares_ca_o) +---------------------------------------------------------- +""" + +def calcular_angulos_ca_h_o(pares_ca_o, min_ang=110.0, max_ang=180.0, archivo_salida="angulos_ca_h_o.txt"): + global angulos_v + angulos_validos = [] + archivo_salida = obtener_carpeta_resultados() / archivo_salida + archivo_salida.parent.mkdir(parents=True, exist_ok=True) + + with open(archivo_salida, "w") as f: + f.write("CA - H - O : Ángulo (grados)\n") + + for ca, o in pares_ca_o: + if not (cmd.count_atoms(ca) and cmd.count_atoms(o)): + continue + + # Extraer resn, resi y chain + partes = ca.split("_") + if len(partes) < 4: + f.write(f"# Nombre inválido para {ca}\n") + continue + + resn = partes[-3] + resi = partes[-2] + chain = partes[-1] + + # Buscar Hs sin usar "model" + seleccion = f"resn {resn} and resi {resi} and chain {chain} and name H*" + try: + hidrogenos = cmd.get_model(seleccion).atom + except Exception as e: + f.write(f"# Error al buscar H para {ca} ({seleccion}): {e}\n") + continue + + if not hidrogenos: + f.write(f"# No se encontraron H para {ca} (búsqueda: {seleccion})\n") + continue + + for h in hidrogenos: + h_sel = f"/{h.model}//{h.chain}/{h.resi}/{h.name}" + h_nombre = f"{h.resn}-{h.resi}{h.chain}_{h.name}" + try: + ang = cmd.get_angle(ca, h_sel, o) + if min_ang <= abs(ang) <= max_ang: + angulos_validos.append((ca, h_nombre, o, ang)) + f.write(f"{ca} - {h_nombre} - {o} : {ang:.2f}°\n") + except Exception as e: + f.write(f"# Error al calcular ángulo para {ca}, {h_sel}, {o} : {e}\n") + angulos_v= angulos_validos + return angulos_validos + + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_GUARDAR_ANGULOS <<<<<<<< +# ========================================================== + +""" +calcular_y_guardar_angulos() +---------------------------------------------------------- +Descripción: + Calcula los ángulos CA-H-O para los segmentos PPII previamente + detectados en la proteína cargada en PyMOL. Localiza los átomos + clave, obtiene las distancias colindantes y calcula los ángulos + que cumplen con el rango de validez (110°-180°). Los resultados + se guardan en un archivo de texto llamado "angulos_ca_h_o.txt" + dentro de la carpeta de resultados. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_guardar_angulos() +---------------------------------------------------------- +""" + + +def calcular_y_guardar_angulos(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + # Obtener los objetos atómicos clave + objetos = localizar_atomos + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + + # Calcular distancias para obtener las tripletas + tripletas = calcular_distancias_colindantes(objetos) + + # Calcular y guardar ángulos + carpeta = obtener_carpeta_resultados() + archivo_salida = carpeta / "angulos_ca_h_o.txt" + angulos = calcular_angulos_ca_h_o(tripletas, str(archivo_salida)) # Convertir a string para PyMOL + + if angulos: + messagebox.showinfo("Éxito", f"Ángulos CA-H-O guardados en:\n{archivo_salida}") + else: + messagebox.showwarning("Aviso", "No se encontraron ángulos válidos (110°-180°)") + + +# ========================================================== +# >>>>>>>> FUNCIÓN: VISUALIZAR_DISTANCIAS_PARES <<<<<<<< +# ========================================================== +""" +visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +Descripción: + Dibuja y visualiza en PyMOL las distancias entre los pares + de átomos proporcionados. Cada distancia se representa como + una línea discontinua (dashed line) de color cian para + facilitar la identificación de interacciones potenciales. + +Parámetros: + pares_candidatos : list + Lista de tuplas con los nombres de los pares de átomos + (atomo1, atomo2) que se desean visualizar. + +Retorno: + None + +Ejemplo de uso: + >>> visualizar_distancias_pares(pares_candidatos) +---------------------------------------------------------- +""" + + +def visualizar_distancias_pares(pares_candidatos): + if not pares_candidatos: + messagebox.showinfo("Visualización", "No hay pares de átomos para visualizar.") + return + + cmd.delete("distancia_ppii") + + for i, (at1, at2) in enumerate(pares_candidatos, start=1): + nombre_dist = f"distancia_ppii_{i}" + cmd.distance(nombre_dist, at1, at2) + cmd.set("dash_width", 4, nombre_dist) + cmd.set("dash_length", 0.5, nombre_dist) + cmd.color("cyan", nombre_dist) + + messagebox.showinfo("Visualización", f"Visualizados {len(pares_candidatos)} pares de distancias colindantes.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: DETECTAR_SEGMENTOS_PPII <<<<<<<< +# ========================================================== + +""" +detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0) +---------------------------------------------------------- +Descripción: + Detecta segmentos de hélices de tipo poliprolina II (PPII) + en la proteína cargada en PyMOL. Analiza los ángulos + torsionales phi (ϕ) y psi (ψ) de cada residuo para + identificar regiones consecutivas compatibles con la + conformación PPII según los criterios de tolerancia + definidos. Permite configurar la longitud mínima de los + segmentos y el número máximo de saltos permitidos entre + residuos consecutivos. + +Parámetros: + objeto : str, opcional + Nombre del objeto cargado en PyMOL sobre el que se + realizará la detección. Por defecto es "proteina". + min_length : int, opcional + Longitud mínima (en número de residuos) que debe tener + un segmento para ser considerado PPII. Por defecto es 3. + tol_phi : float, opcional + Tolerancia en grados para el ángulo phi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + tol_psi : float, opcional + Tolerancia en grados para el ángulo psi respecto a + los valores característicos de la conformación PPII. + Por defecto es 20.0°. + max_saltos : int, opcional + Número máximo de residuos consecutivos que pueden + incumplir los criterios de PPII dentro de un segmento + sin interrumpirlo. Por defecto es 0. + +Retorno: + None + +Ejemplo de uso: + >>> detectar_segmentos_ppii() +---------------------------------------------------------- +""" + +def detectar_segmentos_ppii(objeto="proteina", min_length=3, tol_phi=20.0, tol_psi=20.0, max_saltos=0): + global segmentos_ppii_global + + if not pdb_file: + messagebox.showwarning("Advertencia", "Primero selecciona un archivo.") + return + + phi_psi_map = obtener_angulos_phi_psi_por_cadena(objeto) + lista_residuos = [] + for (chain, resi), (resn, phi, psi) in phi_psi_map.items(): + try: + resi_num = int(resi) + except: + continue + lista_residuos.append((chain, resi_num, resn, phi, psi)) + lista_residuos.sort(key=lambda x: (x[0], x[1])) + + segmentos = [] + segmento_actual = [] + saltos_restantes = max_saltos # Contador de saltos permitidos + + def en_rango_ppii(phi, psi, tol_phi=20.0, tol_psi=20.0): + # Valores IDEALES para PPII: φ = -75°, ψ = +145° + return (abs(phi - (-75)) <= tol_phi) and (abs(psi - 145) <= tol_psi) + + for i, (chain, resi, resn, phi, psi) in enumerate(lista_residuos): + if en_rango_ppii(phi, psi, tol_phi, tol_psi): + # Si cumple, reiniciamos el contador de saltos + saltos_restantes = max_saltos + if not segmento_actual: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + else: + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [(resn, resi, chain, phi, psi)] + else: + # Si no cumple pero hay saltos disponibles + if segmento_actual and saltos_restantes > 0: + _, last_resi, last_chain, _, _ = segmento_actual[-1] + if chain == last_chain and resi == last_resi + 1: + segmento_actual.append((resn, resi, chain, phi, psi)) + saltos_restantes -= 1 + continue + + # Si no hay saltos disponibles o no es consecutivo + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + segmento_actual = [] + saltos_restantes = max_saltos + + # Añadir el último segmento si cumple con la longitud mínima + if len(segmento_actual) >= min_length: + segmentos.append(segmento_actual) + + if not segmentos: + messagebox.showinfo("Resultado", "No se encontraron segmentos PPII.") + return + + cmd.delete("ppii_segmento*") + + salida = f"Segmentos candidatos a hélices PPII (saltos permitidos: {max_saltos}):\n" + for idx, seg in enumerate(segmentos, start=1): + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + salida += f"\nSegmento {idx} (Cadena {chain}, residuos {start_resi}-{end_resi}, longitud {len(seg)}):\n" + for (resn, resi, _, phi, psi) in seg: + salida += f" {resn}-{resi}{chain}: (phi={phi:.1f}, psi={psi:.1f})\n" + + sel_str = f"proteina and chain {chain} and resi {start_resi}-{end_resi}" + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + cmd.create(obj_name, sel_str) + cmd.color("red", obj_name) + cmd.show("cartoon", obj_name) + + carpeta = obtener_carpeta_resultados() + ruta_archivo = carpeta / "segmentos_ppii.txt" + with open(ruta_archivo, "w") as f: + f.write(salida) + + segmentos_ppii_global = segmentos + + messagebox.showinfo("Éxito", f"{len(segmentos)} segmentos PPII detectados (con {max_saltos} saltos permitidos).\n" + f"Átomos clave visualizados en PyMOL.") + +# ========================================================== +# >>>>>>>> FUNCIÓN: GUARDAR_SEGMENTO_PPII_PDB <<<<<<<< +# ========================================================== + +""" +guardar_segmento_ppii_pdb(segmento, nombre_archivo="segmento_ppii.pdb") +---------------------------------------------------------- +Descripción: + Guarda en un archivo PDB un segmento específico de hélice + PPII previamente detectado en la proteína cargada en PyMOL. + El archivo se genera con el nombre especificado dentro de la + carpeta de resultados. + +Parámetros: + segmento : list + Lista con los residuos que forman el segmento PPII a guardar. + nombre_archivo : str, opcional + Nombre del archivo PDB que se generará. Por defecto es + "segmento_ppii.pdb". + +Retorno: + None + +Ejemplo de uso: + >>> guardar_segmento_ppii_pdb(segmentos_ppii[0]) +---------------------------------------------------------- +""" + +def guardar_segmentos_ppii_pdb(): + global segmentos_ppii_global + carpeta = obtener_carpeta_resultados() + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + # Para cada segmento almacenado, construimos el nombre de objeto y hacemos cmd.save + count = 0 + for seg in segmentos_ppii_global: + start_resi = seg[0][1] + end_resi = seg[-1][1] + chain = seg[0][2] + obj_name = f"ppii_segmento_{chain}_{start_resi}_{end_resi}" + # Comprobamos que el objeto efectivamente exista en PyMOL + if cmd.count_atoms(f"{obj_name}") > 0: + + filename = carpeta / f"{obj_name}.pdb" + try: + cmd.save(filename, obj_name) + count += 1 + except Exception as e: + print(f"Error guardando {obj_name}: {e}") + else: + print(f"Objeto {obj_name} no encontrado en la sesión de PyMOL.") + + if count > 0: + messagebox.showinfo("Éxito", f"Se guardaron {count} archivos PDB de segmentos PPII:\n" + f"{os.getcwd()}") + else: + messagebox.showwarning("Atención", "No se guardó ningún segmento (quizá no existan objetos en PyMOL).") + +# ========================================================== +# >>>>>>>> FUNCIÓN: CONVERTIR_A_SELECCIONES_PYMOL <<<<<<<< +# ========================================================== + +""" +convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +Descripción: + Convierte una lista de pares de átomos con sus distancias en + selecciones de PyMOL. Cada par se representa como una selección + que facilita la visualización y el análisis de las interacciones + detectadas. + +Parámetros: + pares_con_distancias : list + Lista de tuplas en el formato (atomo1, atomo2, distancia) + que serán convertidas en selecciones de PyMOL. + +Retorno: + list + Lista con los nombres de las selecciones creadas en PyMOL. + +Ejemplo de uso: + >>> selecciones = convertir_a_selecciones_pymol(pares_con_distancias) +---------------------------------------------------------- +""" + +def convertir_a_selecciones_pymol(pares_con_distancias): + selecciones = [] + for at1, at2, _ in pares_con_distancias: + sele1 = f"id {cmd.index(at1)[0][1]}" + sele2 = f"id {cmd.index(at2)[0][1]}" + selecciones.append((sele1, sele2)) + return selecciones + +# ========================================================== +# >>>>>>>> FUNCIÓN: CALCULAR_Y_VISUALIZAR_DISTANCIAS <<<<<<<< +# ========================================================== + +""" +calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0) +---------------------------------------------------------- +Descripción: + Calcula las distancias entre átomos Cα-O de segmentos PPII + consecutivos y los ángulos CA-H-O asociados. Visualiza en + PyMOL los pares de átomos que cumplen con los criterios de + distancia y ángulo definidos por el usuario. Los pares + válidos se representan con líneas discontinuas (dashed lines) + en color cian para facilitar el análisis. + +Parámetros: + max_dist : float, opcional + Distancia máxima (en Ångström) para considerar los pares + Cα-O como colindantes. Por defecto es 5.0 Å. + min_ang : float, opcional + Ángulo mínimo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 110.0°. + max_ang : float, opcional + Ángulo máximo (en grados) para considerar válido un ángulo + CA-H-O. Por defecto es 180.0°. + +Retorno: + None + +Ejemplo de uso: + >>> calcular_y_visualizar_distancias() +---------------------------------------------------------- +""" + +def calcular_y_visualizar_distancias(max_dist=5.0, min_ang=110.0, max_ang=180.0): + global segmentos_ppii_global + global objetos, pares + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + if objetos is None: + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + if pares is None: + pares = calcular_distancias_colindantes(objetos, max_dist=max_dist) + + calcular_angulos_ca_h_o(pares, min_ang=min_ang, max_ang=max_ang) + +# ========================================================== +# >>>>>>>> FUNCIÓN: DISTANCIAS_P <<<<<<<< +# ========================================================== + +""" +distancias_p() +---------------------------------------------------------- +Descripción: + Visualiza en PyMOL todas las distancias entre los pares + de átomos Cα-O previamente calculados y almacenados. + Cada distancia se dibuja como una línea discontinua + (dashed line) para facilitar el análisis estructural + de la proteína. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> distancias_p() +---------------------------------------------------------- +""" + +def distancias_p(): + global segmentos_ppii_global + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + objetos = localizar_atomicos_clave_segmentos(segmentos_ppii_global) + pares = calcular_distancias_colindantes(objetos) + visualizar_distancias_pares(pares) + +# ========================================================== +# >>>>>>>> FUNCIÓN: LANZAR_INTERFAZ <<<<<<<< +# ========================================================== + +""" +lanzar_interfaz() +---------------------------------------------------------- +Descripción: + Inicia la interfaz gráfica de usuario (GUI) desarrollada + con Tkinter para facilitar la interacción con el programa. + Permite acceder a las funciones principales como cargar + archivos PDB, detectar hélices PPII, añadir hidrógenos, + eliminar solventes y generar reportes, todo desde un menú + visual. + +Parámetros: + Ninguno + +Retorno: + None + +Ejemplo de uso: + >>> lanzar_interfaz() +---------------------------------------------------------- +""" + +def lanzar_interfaz(): + root = tk.Tk() + root.title("PPIIMoL: PPII Detect") + root.geometry("450x1000") + root.resizable(False, False) + + style = ttk.Style(root) + style.theme_use('classic') # Puedes probar: 'alt', 'clam', 'default', 'classic' + + main_frame = ttk.Frame(root, padding=5) + main_frame.pack(fill="both", expand=True) + + def wrapper_detectar_segmentos_ppii(): + try: + tol_phi = float(entrada_tol_phi.get()) + tol_psi = float(entrada_tol_psi.get()) + max_saltos = int(entrada_saltos.get()) # Obtener el valor del campo de saltos + if max_saltos < 0 or max_saltos > 5: # Validar rango (0-5) + raise ValueError + except ValueError: + messagebox.showerror("Error", "¡Saltos debe ser un entero entre 0 y 5!") + return + + detectar_segmentos_ppii(tol_phi=tol_phi, tol_psi=tol_psi, max_saltos=max_saltos) # Pasar el parámetro + +# En la función lanzar_interfaz(), añade este frame antes del frame de ángulos CA-H-O + # Frame para parámetros phi/psi + phi_psi_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulos phi/psi", padding=10) + phi_psi_frame.pack(fill="x", pady=5) + ttk.Label(phi_psi_frame, text="Actualmente sin la tolerancia los angulos ").pack(anchor="w") + ttk.Label(phi_psi_frame, text="por defecto que se miden son hasta: phi 75 y psi 145").pack(anchor="w") + + ttk.Label(phi_psi_frame, text="Tolerancia para phi (±°):").pack(anchor="w") + entrada_tol_phi = ttk.Entry(phi_psi_frame) + entrada_tol_phi.insert(0, "20.0") + entrada_tol_phi.pack(fill="x", pady=2) + + ttk.Label(phi_psi_frame, text="Tolerancia para psi (±°): ").pack(anchor="w") + entrada_tol_psi = ttk.Entry(phi_psi_frame) + entrada_tol_psi.insert(0, "20.0") + entrada_tol_psi.pack(fill="x", pady=2) + + + saltos_frame = ttk.LabelFrame(main_frame, text="Parámetros de saltos", padding=5) + saltos_frame.pack(fill="x", pady=3) + + ttk.Label(saltos_frame, text="Saltos permitidos (0-5):").pack(anchor="w") + entrada_saltos = ttk.Entry(saltos_frame) + entrada_saltos.insert(0, "0") # Valor por defecto: 0 saltos + entrada_saltos.pack(fill="x", pady=1) + + + def wrapper_generar_reporte_csv(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + + if not segmentos_ppii_global: + messagebox.showwarning("Advertencia", "Primero detecta los segmentos PPII.") + return + + generar_reporte_csv(segmentos_ppii_global, min_ang=min_ang, max_ang=max_ang) + #cambiar por otra funcion envoltorio diferente + + # Función envoltorio para pasar los valores + def wrapper_calcular_y_visualizar(): + try: + min_ang = float(entrada_min_ang.get()) + max_ang = float(entrada_max_ang.get()) + except ValueError: + messagebox.showerror("Error", "Introduce valores numéricos válidos para los ángulos.") + return + calcular_y_visualizar_distancias(min_ang=min_ang, max_ang=max_ang) + + # Botones funcionales + botones = [ + ("Seleccionar archivo PDB", seleccionar_archivo), + ("Descargar proteína", descargar_molecula), + ("Eliminar solventes", eliminar_solventes), + ("Añadir hidrógenos", anadir_hidrogenos), + ("Ocultar cadenas laterales", ocultar_side_chains), + ("Guardar ángulos phi/psi en archivo", guardar_csv_angulos_phi_psi), + ("Detectar segmentos PPII y resaltarlos", wrapper_detectar_segmentos_ppii), + ("Guardar segmentos PPII en PDB", guardar_segmentos_ppii_pdb), + ("Visualizar distancias", distancias_p), + ("Angulos entre CA-O-H colindantes", wrapper_calcular_y_visualizar), + ("Generar reporte completo (CSV)", wrapper_generar_reporte_csv), + ] + + for texto, accion in botones: + ttk.Button(main_frame, text=texto, command=accion).pack(fill="x", pady=1) + + # Entradas de ángulos + ang_frame = ttk.LabelFrame(main_frame, text="Parámetros de ángulo CA-H-O", padding=3) + ang_frame.pack(fill="x", pady=3) + + ttk.Label(ang_frame, text="Ángulo mínimo (°):").pack(anchor="w") + entrada_min_ang = ttk.Entry(ang_frame) + entrada_min_ang.insert(0, "110.0") + entrada_min_ang.pack(fill="x", pady=1) + + ttk.Label(ang_frame, text="Ángulo máximo (°):").pack(anchor="w") + entrada_max_ang = ttk.Entry(ang_frame) + entrada_max_ang.insert(0, "180.0") + entrada_max_ang.pack(fill="x", pady=1) + + root.mainloop() + + +lanzar_interfaz() diff --git a/modules/ppiimol/README.md b/modules/ppiimol/README.md new file mode 100644 index 0000000..acba116 --- /dev/null +++ b/modules/ppiimol/README.md @@ -0,0 +1,97 @@ +# 🧬 PPIIMoL + +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![Python](https://img.shields.io/badge/Python-3.9%2B-blue)](https://www.python.org/) +[![PyMOL](https://img.shields.io/badge/PyMOL-2.x-green)](https://pymol.org/) + +## 📖 Descripción + +**PPIIMoL** es un módulo en Python diseñado para integrarse con **PyMOL** y automatizar la detección de hélices de poliprolina II (PPII) en proteínas. Esta herramienta facilita el análisis estructural, identificando rápidamente ángulos torsionales (`phi` y `psi`) característicos, así como posibles enlaces de hidrógeno no canónicos (Cα-H···O=C). + +Desarrollado como parte de un **Trabajo de Fin de Grado en Ingeniería Informática** en colaboración con el laboratorio de neurociencia del CSIC. + +--- + +## 🚀 Características +- 🔍 Detección automática de segmentos PPII mediante análisis de ángulos phi y psi. +- 🧬 Identificación de interacciones Cα-H···O=C relevantes para la estabilidad estructural. +- 📊 Exportación de resultados en CSV para análisis adicionales. +- 🎨 Visualización directa en PyMOL con códigos de color personalizados. +- 🖱️ Interfaz gráfica sencilla basada en Tkinter. + +--- + +## 🛠️ Requisitos +- Python >= 3.9 +- [PyMOL](https://pymol.org/2/) (entorno gráfico) +- Tkinter (incluido en la mayoría de distribuciones de Python) + +--- + +## 📦 Instalación + +1. Clona este repositorio: + ```bash + git clone https://github.com/silviaenma/PPIIMoL.git + +Abre PyMOL y añade el módulo a la ruta de plugins o cárgalo manualmente: +run PPIIMoL/PPIIMoL.py + +🧪 Ejemplo de uso +# Cargar el módulo en PyMOL +run PPIIMoL/PPIIMoL.py + +# Detectar hélices PPII en un archivo PDB +load 3bog.pdb +ppii_detect() + +📂 Los resultados se exportarán automáticamente en una carpeta organizada con fecha. + +📜 Licencia +Este proyecto está bajo la licencia GNU GPLv3. + +🌐 Description (English) +PPIIMoL is a Python module designed to integrate with PyMOL for the automatic detection of polyproline II (PPII) helices in proteins. This tool simplifies structural analysis by quickly identifying characteristic torsional angles (phi and psi), as well as potential non-canonical hydrogen bonds (Cα-H···O=C). + +Developed as part of a Bachelor's Thesis in Computer Engineering in collaboration with the CSIC neuroscience lab. + +🚀 Features +🔍 Automatic detection of PPII segments via phi/psi angle analysis. + +🧬 Identification of Cα-H···O=C interactions relevant to structural stability. + +📊 Export of results in CSV for further analysis. + +🎨 Direct visualization in PyMOL with customizable color codes. + +🖱️ Simple GUI based on Tkinter. + +🛠️ Requirements +Python >= 3.9 + +PyMOL (graphical environment) + +Tkinter (usually included in Python distributions) + +📦 Installation +1. Clone this repository: +git clone https://github.com/silviaenma/PPIIMoL.git + +2. Open PyMOL and load the module: +run PPIIMoL/PPIIMoL.py + +🧪 Example usage +python +Copiar +Editar +# Load the module in PyMOL +run PPIIMoL/PPIIMoL.py + +# Detect PPII helices in a PDB file +load 3bog.pdb +ppii_detect() +📂 Results are automatically exported in a dated folder. + +📜 License +This project is licensed under the GNU GPLv3. + From 6ceeea402461c378acf373be3e9cd5012782ae69 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Tue, 22 Jul 2025 11:40:17 +0200 Subject: [PATCH 07/19] Create README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crear índice README.md y añadir PPIIMoL --- modules/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 modules/README.md diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 0000000..08f15b4 --- /dev/null +++ b/modules/README.md @@ -0,0 +1,7 @@ +# 📜 Módulos disponibles + +Este repositorio contiene scripts y módulos que pueden integrarse en PyMOL para ampliar su funcionalidad. + +## 🧬 Lista de módulos + +- **ppiimol**: Detección automática de hélices PPII en proteínas. [Ver módulo](ppiimol) From 3867e4051e99b2d99944064a0a9ce2457901dcc2 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Tue, 22 Jul 2025 11:42:05 +0200 Subject: [PATCH 08/19] Update __init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registrar módulo ppiimol en __init__.py --- modules/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/__init__.py b/modules/__init__.py index e69de29..47ba26f 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -0,0 +1 @@ +from . import ppiimol From daf901dc9f83c33372f2719df8c24403b934953c Mon Sep 17 00:00:00 2001 From: silviaenma Date: Wed, 23 Jul 2025 13:11:44 +0200 Subject: [PATCH 09/19] Rename modules/ppiimol/PPIIMoL.py to modules/ppiimol/plugins/ppiimol/PPIIMoL.py Move PPIIMoL.py to plugins/ppiimol --- modules/ppiimol/{ => plugins/ppiimol}/PPIIMoL.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/ppiimol/{ => plugins/ppiimol}/PPIIMoL.py (100%) diff --git a/modules/ppiimol/PPIIMoL.py b/modules/ppiimol/plugins/ppiimol/PPIIMoL.py similarity index 100% rename from modules/ppiimol/PPIIMoL.py rename to modules/ppiimol/plugins/ppiimol/PPIIMoL.py From fbbf220abfd1f0108e18b58b890c2d52ea50542f Mon Sep 17 00:00:00 2001 From: silviaenma Date: Wed, 23 Jul 2025 13:13:59 +0200 Subject: [PATCH 10/19] Rename modules/ppiimol/README.md to modules/ppiimol/plugins/ppiimol/README.md Move README.md to plugins/ppiimol --- modules/ppiimol/{ => plugins/ppiimol}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/ppiimol/{ => plugins/ppiimol}/README.md (100%) diff --git a/modules/ppiimol/README.md b/modules/ppiimol/plugins/ppiimol/README.md similarity index 100% rename from modules/ppiimol/README.md rename to modules/ppiimol/plugins/ppiimol/README.md From cd426ed91bb5cbad4dc4153a48a364ee72ccadc6 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Wed, 23 Jul 2025 13:15:23 +0200 Subject: [PATCH 11/19] Update and rename modules/__init__.py to modules/plugins/ppiimol/__init__.py move __init__.py to plugins/ppiimol/ --- modules/{ => plugins/ppiimol}/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/{ => plugins/ppiimol}/__init__.py (100%) diff --git a/modules/__init__.py b/modules/plugins/ppiimol/__init__.py similarity index 100% rename from modules/__init__.py rename to modules/plugins/ppiimol/__init__.py From cb746907ff732a27d9579d82536a0c9131e8394c Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 12:36:47 +0200 Subject: [PATCH 12/19] Replaced PPIIMoL.py Replaced PPIIMoL.py with version including English docstring --- PPIIMoL.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/PPIIMoL.py b/PPIIMoL.py index afe7457..0b89beb 100644 --- a/PPIIMoL.py +++ b/PPIIMoL.py @@ -1,3 +1,39 @@ +""" +PPIIMoL - PPII Helix Detector for PyMOL +--------------------------------------- + +This PyMOL plugin detects polyproline II (PPII) helices in protein structures using dihedral angles (phi/psi) +and helps visualize relevant interactions with non-canonical hydrogen bonds. + +Main Features: +- Load PDB files or fetch proteins by ID. +- Calculate phi/psi angles for backbone atoms. +- Automatically identify candidate PPII helices. +- Visualize key atom distances and angles (CA–H–O). +- Export CSV and PDB reports of detected segments. +- User-friendly interface with Tkinter. + +Usage: +- Run this script from PyMOL: `run PPIIMoL.py` +- Launch the GUI with: `lanzar_interfaz()` +- Alternatively, use core functions like: `detectar_segmentos_ppii("your_object")` + +Dependencies: +- PyMOL with Python support +- Python 3.x +- Tkinter + +Authors: +- Silvia Enma (2025), Instituto de Química-Física "Blas Cabrera" (CSIC) +- GitHub: https://github.com/silviaenma/ppii-detector-pymol + +License: +- GNU GPL v3 + +Note: +- This script was originally written in Spanish for internal lab use, but has been translated for the PyMOL community. +""" + from pymol import cmd, stored import tkinter as tk import math From d5bbac8ddb5689dd068e92a8e8d7636a51b2e572 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 12:42:25 +0200 Subject: [PATCH 13/19] Replaced PPIIMoL.py Replaced PPIIMoL.py with version including English docstring --- modules/ppiimol/plugins/ppiimol/PPIIMoL.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/modules/ppiimol/plugins/ppiimol/PPIIMoL.py b/modules/ppiimol/plugins/ppiimol/PPIIMoL.py index afe7457..0b89beb 100644 --- a/modules/ppiimol/plugins/ppiimol/PPIIMoL.py +++ b/modules/ppiimol/plugins/ppiimol/PPIIMoL.py @@ -1,3 +1,39 @@ +""" +PPIIMoL - PPII Helix Detector for PyMOL +--------------------------------------- + +This PyMOL plugin detects polyproline II (PPII) helices in protein structures using dihedral angles (phi/psi) +and helps visualize relevant interactions with non-canonical hydrogen bonds. + +Main Features: +- Load PDB files or fetch proteins by ID. +- Calculate phi/psi angles for backbone atoms. +- Automatically identify candidate PPII helices. +- Visualize key atom distances and angles (CA–H–O). +- Export CSV and PDB reports of detected segments. +- User-friendly interface with Tkinter. + +Usage: +- Run this script from PyMOL: `run PPIIMoL.py` +- Launch the GUI with: `lanzar_interfaz()` +- Alternatively, use core functions like: `detectar_segmentos_ppii("your_object")` + +Dependencies: +- PyMOL with Python support +- Python 3.x +- Tkinter + +Authors: +- Silvia Enma (2025), Instituto de Química-Física "Blas Cabrera" (CSIC) +- GitHub: https://github.com/silviaenma/ppii-detector-pymol + +License: +- GNU GPL v3 + +Note: +- This script was originally written in Spanish for internal lab use, but has been translated for the PyMOL community. +""" + from pymol import cmd, stored import tkinter as tk import math From 4b05cb4dec007db87f68abc1bc85129b1dc6c26e Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 12:51:46 +0200 Subject: [PATCH 14/19] Remove README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed README.md content — merged into module docstring --- modules/ppiimol/plugins/ppiimol/README.md | 96 ----------------------- 1 file changed, 96 deletions(-) diff --git a/modules/ppiimol/plugins/ppiimol/README.md b/modules/ppiimol/plugins/ppiimol/README.md index acba116..8b13789 100644 --- a/modules/ppiimol/plugins/ppiimol/README.md +++ b/modules/ppiimol/plugins/ppiimol/README.md @@ -1,97 +1 @@ -# 🧬 PPIIMoL - -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![Python](https://img.shields.io/badge/Python-3.9%2B-blue)](https://www.python.org/) -[![PyMOL](https://img.shields.io/badge/PyMOL-2.x-green)](https://pymol.org/) - -## 📖 Descripción - -**PPIIMoL** es un módulo en Python diseñado para integrarse con **PyMOL** y automatizar la detección de hélices de poliprolina II (PPII) en proteínas. Esta herramienta facilita el análisis estructural, identificando rápidamente ángulos torsionales (`phi` y `psi`) característicos, así como posibles enlaces de hidrógeno no canónicos (Cα-H···O=C). - -Desarrollado como parte de un **Trabajo de Fin de Grado en Ingeniería Informática** en colaboración con el laboratorio de neurociencia del CSIC. - ---- - -## 🚀 Características -- 🔍 Detección automática de segmentos PPII mediante análisis de ángulos phi y psi. -- 🧬 Identificación de interacciones Cα-H···O=C relevantes para la estabilidad estructural. -- 📊 Exportación de resultados en CSV para análisis adicionales. -- 🎨 Visualización directa en PyMOL con códigos de color personalizados. -- 🖱️ Interfaz gráfica sencilla basada en Tkinter. - ---- - -## 🛠️ Requisitos -- Python >= 3.9 -- [PyMOL](https://pymol.org/2/) (entorno gráfico) -- Tkinter (incluido en la mayoría de distribuciones de Python) - ---- - -## 📦 Instalación - -1. Clona este repositorio: - ```bash - git clone https://github.com/silviaenma/PPIIMoL.git - -Abre PyMOL y añade el módulo a la ruta de plugins o cárgalo manualmente: -run PPIIMoL/PPIIMoL.py - -🧪 Ejemplo de uso -# Cargar el módulo en PyMOL -run PPIIMoL/PPIIMoL.py - -# Detectar hélices PPII en un archivo PDB -load 3bog.pdb -ppii_detect() - -📂 Los resultados se exportarán automáticamente en una carpeta organizada con fecha. - -📜 Licencia -Este proyecto está bajo la licencia GNU GPLv3. - -🌐 Description (English) -PPIIMoL is a Python module designed to integrate with PyMOL for the automatic detection of polyproline II (PPII) helices in proteins. This tool simplifies structural analysis by quickly identifying characteristic torsional angles (phi and psi), as well as potential non-canonical hydrogen bonds (Cα-H···O=C). - -Developed as part of a Bachelor's Thesis in Computer Engineering in collaboration with the CSIC neuroscience lab. - -🚀 Features -🔍 Automatic detection of PPII segments via phi/psi angle analysis. - -🧬 Identification of Cα-H···O=C interactions relevant to structural stability. - -📊 Export of results in CSV for further analysis. - -🎨 Direct visualization in PyMOL with customizable color codes. - -🖱️ Simple GUI based on Tkinter. - -🛠️ Requirements -Python >= 3.9 - -PyMOL (graphical environment) - -Tkinter (usually included in Python distributions) - -📦 Installation -1. Clone this repository: -git clone https://github.com/silviaenma/PPIIMoL.git - -2. Open PyMOL and load the module: -run PPIIMoL/PPIIMoL.py - -🧪 Example usage -python -Copiar -Editar -# Load the module in PyMOL -run PPIIMoL/PPIIMoL.py - -# Detect PPII helices in a PDB file -load 3bog.pdb -ppii_detect() -📂 Results are automatically exported in a dated folder. - -📜 License -This project is licensed under the GNU GPLv3. From 108426ed0145b0e95783841e8e9392aa0de9547c Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 14:29:29 +0200 Subject: [PATCH 15/19] update ppiimol --- modules/ppiimol/plugins/ppiimol/PPIIMoL.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/ppiimol/plugins/ppiimol/PPIIMoL.py b/modules/ppiimol/plugins/ppiimol/PPIIMoL.py index 0b89beb..855e7f3 100644 --- a/modules/ppiimol/plugins/ppiimol/PPIIMoL.py +++ b/modules/ppiimol/plugins/ppiimol/PPIIMoL.py @@ -34,6 +34,17 @@ - This script was originally written in Spanish for internal lab use, but has been translated for the PyMOL community. """ +def __init_plugin__(app=None): + """ + PyMOL plugin loader function. Called when the plugin is loaded from the Plugin Manager. + """ + try: + from tkinter import messagebox + messagebox.showinfo("PPIIMoL", "PPIIMoL plugin loaded successfully.") + except ImportError: + print("PPIIMoL plugin loaded (no GUI message shown, Tkinter not available).") + + from pymol import cmd, stored import tkinter as tk import math From 1658325c0a6c3c9a1cd85cb695bcf64032e011d7 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 14:30:54 +0200 Subject: [PATCH 16/19] Delete modules/plugins/ppiimol directory --- modules/plugins/ppiimol/__init__.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 modules/plugins/ppiimol/__init__.py diff --git a/modules/plugins/ppiimol/__init__.py b/modules/plugins/ppiimol/__init__.py deleted file mode 100644 index 47ba26f..0000000 --- a/modules/plugins/ppiimol/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import ppiimol From cde05f4a887b8343e7b83011fc44e575c7490e02 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 14:39:05 +0200 Subject: [PATCH 17/19] delete __init__.py --- modules/ppiimol/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppiimol/__init__.py b/modules/ppiimol/__init__.py index 9ec846f..8b13789 100644 --- a/modules/ppiimol/__init__.py +++ b/modules/ppiimol/__init__.py @@ -1 +1 @@ -from .ppiimol import * + From dd1702a568737da256e6e5ad2763c7fb999b9604 Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 14:44:50 +0200 Subject: [PATCH 18/19] Update .gitattributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forzar finales de línea LF para normalizar archivos de texto --- .gitattributes | Bin 374 -> 210 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.gitattributes b/.gitattributes index 89b50d9cc2dd63a0307751d67f32b0d16bcefe2d..3f05ce19b6752cd32130b0af170534b548609d09 100644 GIT binary patch literal 210 zcmaFAd%v=RTYgbhVv#~lW?rg7YFDkDEe4Vo{<(^5M-Xnd$kwTv`exsTC!* ziKQj^3aR-ywmE4)F};FHurQKHZVI|cNks{&5HFXqLP$|!3DC}@%)G>+%=}`7L42Soe; literal 374 zcmaivI}XA?3`D0!;tng(QE&rVDoQS}3CSvmd;}>7*G>;SFDMAaio!pQ$1}d)7INCC z)k#-*veQwO8WnT}bswjoGxL^Br!MY!x?E$#1N^yWT51J4fb?)LWV_LFaZQxdP7Ukd zPXFSkFPWmb-{IP$^>m*_zt&eMo9mof6HedQ+jFDvfe(|{10SWZfJT|%b^Mt9z)bT2 Dl*K)Q From ae72da2f86cc97fb54f66241b6c10c1e73a2946a Mon Sep 17 00:00:00 2001 From: silviaenma Date: Thu, 24 Jul 2025 14:47:51 +0200 Subject: [PATCH 19/19] Update .gitignore --- .gitignore | Bin 628 -> 356 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.gitignore b/.gitignore index 225306c4a30b1c6752f406636e9b2c6e2a818531..705c392459e7867d99bb4e16f183f689959e5b49 100644 GIT binary patch literal 356 zcmZ8d%T5F#5WJJ{4ObzhJ{88of|m zm2_3nU+*6qaTCqt3MNs7(J?^QNmvlOnkV#}DHW>GW&)+IIlGdf9t_%-bu=sB3YIg@ zHe!qZ)g70pjr)A_%?o_S=qaMOm=`@m;!J`soZUU{$yJt4=poYPp47Ew_|vf0dU(3; zaS0mCcvW(I0uX^l&Z`kxkQSl(Az-`)QfDMr{z2*X-Cdr%D==yI%N#f hdH2thn(T)yT3VJR(Ltbn!-eWcMYl4ifX?Sc;y1=hZ@2&e literal 628 zcmaix%}T^T5QOV2_znx+Jj7=ZJt_(-UPNSn#x zuKD^r(M0EJwa|-7ZB;9Ryz?(K18)@65gA<)PjpMQqr28d$2!szUjycTvxJ14rP`^A zRC}OObN_J{2*f(f8^l5bDS~t4-wNGS@@7aM0eXF0W!H2ot(CPZ$+#L*S-1COR zK6&>F4