Upload 8 files
Browse files- UI.py +125 -0
- app.py +21 -0
- config.py +17 -0
- decorators.py +32 -0
- interface.py +306 -0
- model_app.py +138 -0
- models.py +195 -0
- requirements.txt +15 -0
UI.py
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# UI.py
|
2 |
+
import gradio as gr
|
3 |
+
# La importación de process_and_plot se maneja por modal_app.py
|
4 |
+
# from interface import process_and_plot
|
5 |
+
|
6 |
+
def create_interface():
|
7 |
+
"""
|
8 |
+
Esta función crea la interfaz de usuario y la devuelve para que pueda ser lanzada
|
9 |
+
desde app.py utilizando demo.launch().
|
10 |
+
"""
|
11 |
+
|
12 |
+
with gr.Blocks(theme='upsatwal/mlsc_tiet') as demo: # Puede que necesites un theme diferente o default
|
13 |
+
# with gr.Blocks(theme=gr.themes.Soft()) as demo: # Alternativa más común
|
14 |
+
gr.Markdown("# Modelado de Bioprocesos con Ecuaciones Personalizadas y Análisis por IA")
|
15 |
+
|
16 |
+
with gr.Row():
|
17 |
+
with gr.Column(scale=2):
|
18 |
+
gr.Markdown("### Carga de Datos y Configuración General")
|
19 |
+
file_input = gr.File(label="Subir archivo Excel (.xlsx)", file_types=[".xlsx"])
|
20 |
+
|
21 |
+
show_legend_ui = gr.Checkbox(label="Mostrar leyenda en gráficos", value=True)
|
22 |
+
show_params_ui = gr.Checkbox(label="Mostrar parámetros ajustados en gráficos", value=True)
|
23 |
+
legend_position_ui = gr.Dropdown(
|
24 |
+
label="Posición de la leyenda",
|
25 |
+
choices=['best', 'upper right', 'upper left', 'lower right', 'lower left', 'center left', 'center right', 'lower center', 'upper center', 'center'],
|
26 |
+
value='best'
|
27 |
+
)
|
28 |
+
with gr.Column(scale=1):
|
29 |
+
gr.Markdown("### Conteo de Ecuaciones")
|
30 |
+
biomass_eq_count_ui = gr.Number(label="Número de ecuaciones de Biomasa a probar (1-3)", value=1, minimum=1, maximum=3, precision=0, step=1)
|
31 |
+
substrate_eq_count_ui = gr.Number(label="Número de ecuaciones de Sustrato a probar (1-3)", value=1, minimum=1, maximum=3, precision=0, step=1)
|
32 |
+
product_eq_count_ui = gr.Number(label="Número de ecuaciones de Producto a probar (1-3)", value=1, minimum=1, maximum=3, precision=0, step=1)
|
33 |
+
|
34 |
+
|
35 |
+
# --- Sección de Biomasa ---
|
36 |
+
with gr.Accordion("Ecuaciones y Parámetros de Biomasa", open=True):
|
37 |
+
with gr.Row():
|
38 |
+
with gr.Column():
|
39 |
+
biomass_eq1_ui = gr.Textbox(label="Ecuación de Biomasa 1 (ej: Xm * (1 - exp(-um * t)))", value="Xm * (1 - exp(-um * t))", lines=2) # Modelo de Moser simplificado
|
40 |
+
biomass_param1_ui = gr.Textbox(label="Parámetros Biomasa 1 (coma sep., ej: Xm, um, Ks)", value="Xm, um, t_lag", info="Use 't' para tiempo. Use 'Ks' o 't_lag' para fases de retardo si es necesario.")
|
41 |
+
biomass_bound1_ui = gr.Textbox(label="Límites Biomasa 1 (ej: (0,inf),(0,5),(-10,10))", value="(0, inf), (0, inf), (0, inf)", info="Formato: (low,high) para cada param. Use np.inf.")
|
42 |
+
with gr.Column(visible=False) as biomass_col2: # Inicialmente oculto
|
43 |
+
biomass_eq2_ui = gr.Textbox(label="Ecuación de Biomasa 2", value="X0 * exp(um * t)", lines=2) # Crecimiento exponencial simple
|
44 |
+
biomass_param2_ui = gr.Textbox(label="Parámetros Biomasa 2", value="X0, um")
|
45 |
+
biomass_bound2_ui = gr.Textbox(label="Límites Biomasa 2", value="(0, inf), (0, inf)")
|
46 |
+
with gr.Column(visible=False) as biomass_col3: # Inicialmente oculto
|
47 |
+
biomass_eq3_ui = gr.Textbox(label="Ecuación de Biomasa 3", lines=2)
|
48 |
+
biomass_param3_ui = gr.Textbox(label="Parámetros Biomasa 3")
|
49 |
+
biomass_bound3_ui = gr.Textbox(label="Límites Biomasa 3")
|
50 |
+
|
51 |
+
# --- Sección de Sustrato ---
|
52 |
+
with gr.Accordion("Ecuaciones y Parámetros de Sustrato", open=True):
|
53 |
+
gr.Markdown("Para ecuaciones de Sustrato y Producto, usa `X_val` para representar la concentración de biomasa calculada X(t). Ejemplo: `S0 - (1/YXS) * (X_val - X0_biomass)`")
|
54 |
+
with gr.Row():
|
55 |
+
with gr.Column():
|
56 |
+
substrate_eq1_ui = gr.Textbox(label="Ecuación de Sustrato 1", value="S0 - (X_val / YXS) - mS * t", lines=2) # Luedeking-Piret simplificado
|
57 |
+
substrate_param1_ui = gr.Textbox(label="Parámetros Sustrato 1", value="S0, YXS, mS")
|
58 |
+
substrate_bound1_ui = gr.Textbox(label="Límites Sustrato 1", value="(0, inf), (0.01, inf), (0, inf)")
|
59 |
+
with gr.Column(visible=False) as substrate_col2:
|
60 |
+
substrate_eq2_ui = gr.Textbox(label="Ecuación de Sustrato 2", lines=2)
|
61 |
+
substrate_param2_ui = gr.Textbox(label="Parámetros Sustrato 2")
|
62 |
+
substrate_bound2_ui = gr.Textbox(label="Límites Sustrato 2")
|
63 |
+
with gr.Column(visible=False) as substrate_col3:
|
64 |
+
substrate_eq3_ui = gr.Textbox(label="Ecuación de Sustrato 3", lines=2)
|
65 |
+
substrate_param3_ui = gr.Textbox(label="Parámetros Sustrato 3")
|
66 |
+
substrate_bound3_ui = gr.Textbox(label="Límites Sustrato 3")
|
67 |
+
|
68 |
+
# --- Sección de Producto ---
|
69 |
+
with gr.Accordion("Ecuaciones y Parámetros de Producto", open=True):
|
70 |
+
with gr.Row():
|
71 |
+
with gr.Column():
|
72 |
+
product_eq1_ui = gr.Textbox(label="Ecuación de Producto 1", value="P0 + YPX * X_val + mP * t", lines=2) # Luedeking-Piret simplificado
|
73 |
+
product_param1_ui = gr.Textbox(label="Parámetros Producto 1", value="P0, YPX, mP")
|
74 |
+
product_bound1_ui = gr.Textbox(label="Límites Producto 1", value="(0, inf), (0, inf), (0, inf)")
|
75 |
+
with gr.Column(visible=False) as product_col2:
|
76 |
+
product_eq2_ui = gr.Textbox(label="Ecuación de Producto 2", lines=2)
|
77 |
+
product_param2_ui = gr.Textbox(label="Parámetros Producto 2")
|
78 |
+
product_bound2_ui = gr.Textbox(label="Límites Producto 2")
|
79 |
+
with gr.Column(visible=False) as product_col3:
|
80 |
+
product_eq3_ui = gr.Textbox(label="Ecuación de Producto 3", lines=2)
|
81 |
+
product_param3_ui = gr.Textbox(label="Parámetros Producto 3")
|
82 |
+
product_bound3_ui = gr.Textbox(label="Límites Producto 3")
|
83 |
+
|
84 |
+
# Lógica para mostrar/ocultar campos de ecuación dinámicamente
|
85 |
+
def update_visibility(count, c2, c3):
|
86 |
+
return gr.Column(visible=count >= 2), gr.Column(visible=count >= 3)
|
87 |
+
|
88 |
+
biomass_eq_count_ui.change(fn=lambda x: update_visibility(x, biomass_col2, biomass_col3), inputs=biomass_eq_count_ui, outputs=[biomass_col2, biomass_col3])
|
89 |
+
substrate_eq_count_ui.change(fn=lambda x: update_visibility(x, substrate_col2, substrate_col3), inputs=substrate_eq_count_ui, outputs=[substrate_col2, substrate_col3])
|
90 |
+
product_eq_count_ui.change(fn=lambda x: update_visibility(x, product_col2, product_col3), inputs=product_eq_count_ui, outputs=[product_col2, product_col3])
|
91 |
+
|
92 |
+
submit_button = gr.Button("Procesar y Analizar", variant="primary", scale=1)
|
93 |
+
|
94 |
+
gr.Markdown("## Resultados del Análisis")
|
95 |
+
with gr.Row():
|
96 |
+
image_output = gr.Image(label="Gráfico Generado", type="pil", width=600, height=900, scale=2) # Ajusta escala si es necesario
|
97 |
+
with gr.Column(scale=3): # Más espacio para el análisis
|
98 |
+
analysis_output = gr.Markdown(label="Análisis del Modelo por IA") # Usar Markdown para mejor formato
|
99 |
+
|
100 |
+
# Lista de todos los inputs para el botón de submit
|
101 |
+
all_inputs = [
|
102 |
+
file_input,
|
103 |
+
biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
|
104 |
+
biomass_param1_ui, biomass_param2_ui, biomass_param3_ui,
|
105 |
+
biomass_bound1_ui, biomass_bound2_ui, biomass_bound3_ui,
|
106 |
+
substrate_eq1_ui, substrate_eq2_ui, substrate_eq3_ui,
|
107 |
+
substrate_param1_ui, substrate_param2_ui, substrate_param3_ui,
|
108 |
+
substrate_bound1_ui, substrate_bound2_ui, substrate_bound3_ui,
|
109 |
+
product_eq1_ui, product_eq2_ui, product_eq3_ui,
|
110 |
+
product_param1_ui, product_param2_ui, product_param3_ui,
|
111 |
+
product_bound1_ui, product_bound2_ui, product_bound3_ui,
|
112 |
+
legend_position_ui,
|
113 |
+
show_legend_ui,
|
114 |
+
show_params_ui,
|
115 |
+
biomass_eq_count_ui,
|
116 |
+
substrate_eq_count_ui,
|
117 |
+
product_eq_count_ui
|
118 |
+
]
|
119 |
+
# La conexión se hace en modal_app.py al inyectar la función de `interface`
|
120 |
+
# El atributo `fn` del botón se establecerá allí.
|
121 |
+
demo.load(fn=lambda: (gr.Column(visible=False), gr.Column(visible=False)), outputs=[biomass_col2, biomass_col3])
|
122 |
+
demo.load(fn=lambda: (gr.Column(visible=False), gr.Column(visible=False)), outputs=[substrate_col2, substrate_col3])
|
123 |
+
demo.load(fn=lambda: (gr.Column(visible=False), gr.Column(visible=False)), outputs=[product_col2, product_col3])
|
124 |
+
|
125 |
+
return demo, all_inputs, [image_output, analysis_output], submit_button # Devolver también inputs, outputs y botón
|
app.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app.py
|
2 |
+
# Importar create_interface desde UI.py
|
3 |
+
from UI import create_interface
|
4 |
+
import interface as app_interface_module # Para el click handler
|
5 |
+
|
6 |
+
def main():
|
7 |
+
# Crear la interfaz y obtener los componentes necesarios
|
8 |
+
demo, all_inputs, outputs_list, submit_button_obj = create_interface()
|
9 |
+
|
10 |
+
# Conectar el botón de submit a la función process_and_plot del módulo interface
|
11 |
+
# Esto es crucial para que la UI llame a la lógica correcta.
|
12 |
+
submit_button_obj.click(
|
13 |
+
fn=app_interface_module.process_and_plot, # La función real
|
14 |
+
inputs=all_inputs, # La lista de componentes de entrada
|
15 |
+
outputs=outputs_list # La lista de componentes de salida
|
16 |
+
)
|
17 |
+
|
18 |
+
demo.launch()
|
19 |
+
|
20 |
+
if __name__ == "__main__":
|
21 |
+
main()
|
config.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# config.py
|
2 |
+
import torch
|
3 |
+
|
4 |
+
# Configuración del dispositivo
|
5 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
6 |
+
|
7 |
+
# Ruta al modelo de Hugging Face Hub que quieres usar para el análisis
|
8 |
+
# Asegúrate de que tienes acceso a este modelo y que es compatible con AutoModelForCausalLM
|
9 |
+
MODEL_PATH = "Qwen/Qwen2-7B-Instruct" # Ejemplo, Qwen2-7B-Instruct es un buen modelo.
|
10 |
+
# MODEL_PATH = "mistralai/Mistral-7B-Instruct-v0.2" # Otro ejemplo
|
11 |
+
# MODEL_PATH = "01-ai/Yi-1.5-9B-Chat" # Si prefieres Yi y es accesible
|
12 |
+
|
13 |
+
# Longitud máxima para NUEVOS tokens generados por el LLM
|
14 |
+
MAX_LENGTH = 1024 # Aumentado para permitir análisis más detallados
|
15 |
+
|
16 |
+
# Temperatura para la generación de texto (ajusta según la creatividad deseada)
|
17 |
+
TEMPERATURE = 0.5
|
decorators.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# decorators.py
|
2 |
+
from functools import wraps
|
3 |
+
# La importación original 'from spaces import GPU' es específica de Hugging Face Spaces.
|
4 |
+
# Para un uso general o con Modal, este decorador necesitaría ser adaptado o
|
5 |
+
# la gestión de GPU se haría directamente a través de la configuración de la función Modal.
|
6 |
+
|
7 |
+
# Por ahora, mantenemos la estructura, pero ten en cuenta que @GPU no funcionará
|
8 |
+
# fuera del entorno de HF Spaces tal como está. Modal tiene su propia forma de asignar GPUs.
|
9 |
+
|
10 |
+
class GPU: # Placeholder para simular la estructura si no se ejecuta en HF Spaces
|
11 |
+
def __init__(self, duration=100):
|
12 |
+
self.duration = duration
|
13 |
+
def __call__(self, func):
|
14 |
+
@wraps(func)
|
15 |
+
def wrapper(*args, **kwargs):
|
16 |
+
# print(f"Simulando ejecución con GPU (duración: {self.duration}s) para: {func.__name__}")
|
17 |
+
return func(*args, **kwargs)
|
18 |
+
return wrapper
|
19 |
+
|
20 |
+
def gpu_decorator(duration=100):
|
21 |
+
"""
|
22 |
+
Decorador personalizado que simula el uso de GPU.
|
23 |
+
En un entorno de Hugging Face Spaces con GPU asignada,
|
24 |
+
el decorador `spaces.GPU` real se encargaría de la gestión.
|
25 |
+
"""
|
26 |
+
def decorator(func):
|
27 |
+
@GPU(duration=duration) # Usando nuestro placeholder o el real de `spaces`
|
28 |
+
@wraps(func)
|
29 |
+
def wrapper(*args, **kwargs):
|
30 |
+
return func(*args, **kwargs)
|
31 |
+
return wrapper
|
32 |
+
return decorator
|
interface.py
ADDED
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# interface.py
|
2 |
+
import numpy as np
|
3 |
+
import pandas as pd
|
4 |
+
import matplotlib
|
5 |
+
matplotlib.use('Agg') # Backend no interactivo para matplotlib en Modal/servidores
|
6 |
+
import matplotlib.pyplot as plt
|
7 |
+
from PIL import Image
|
8 |
+
import io
|
9 |
+
import json
|
10 |
+
import traceback # Para imprimir traceback detallado de errores
|
11 |
+
|
12 |
+
# Importar BioprocessModel de TU models.py (el que usa sympy)
|
13 |
+
from models import BioprocessModel
|
14 |
+
# from decorators import gpu_decorator # El decorador @gpu es de HF Spaces, Modal lo maneja diferente
|
15 |
+
|
16 |
+
# Variables globales que serán "inyectadas" por modal_app.py
|
17 |
+
USE_MODAL_FOR_LLM_ANALYSIS = False
|
18 |
+
generate_analysis_from_modal = None # Placeholder para la función remota de Modal
|
19 |
+
|
20 |
+
def parse_bounds_str(bounds_str_input, num_params):
|
21 |
+
"""Parsea una cadena de límites y devuelve listas para lower y upper bounds."""
|
22 |
+
# Usar una copia para evitar modificar la entrada original si es mutable
|
23 |
+
bounds_str = str(bounds_str_input)
|
24 |
+
|
25 |
+
# Manejar el caso de cadena vacía o solo espacios en blanco
|
26 |
+
if not bounds_str.strip():
|
27 |
+
print(f"Cadena de límites vacía para {num_params} params. Usando límites (-inf, inf).")
|
28 |
+
return [-np.inf] * num_params, [np.inf] * num_params
|
29 |
+
|
30 |
+
try:
|
31 |
+
# Reemplazar 'inf' con 'np.inf' para que eval funcione correctamente con NumPy
|
32 |
+
# Hacerlo insensible a mayúsculas/minúsculas para 'inf'
|
33 |
+
bounds_str = bounds_str.lower().replace('inf', 'np.inf')
|
34 |
+
|
35 |
+
# Evaluar la cadena para convertirla en una lista de tuplas o listas
|
36 |
+
# Ejemplo de entrada esperada: " (0, np.inf), (0,10), (np.nan, np.nan) "
|
37 |
+
# Asegurar que esté encerrado en corchetes para que eval produzca una lista
|
38 |
+
if not bounds_str.startswith('['):
|
39 |
+
bounds_str = f"[{bounds_str}]"
|
40 |
+
|
41 |
+
parsed_bounds_list = eval(bounds_str)
|
42 |
+
|
43 |
+
if not isinstance(parsed_bounds_list, list):
|
44 |
+
raise ValueError("La cadena de límites no evaluó a una lista.")
|
45 |
+
|
46 |
+
if len(parsed_bounds_list) != num_params:
|
47 |
+
raise ValueError(f"Número de tuplas de límites ({len(parsed_bounds_list)}) no coincide con el número de parámetros ({num_params}).")
|
48 |
+
|
49 |
+
lower_bounds = []
|
50 |
+
upper_bounds = []
|
51 |
+
for item in parsed_bounds_list:
|
52 |
+
if not (isinstance(item, (tuple, list)) and len(item) == 2):
|
53 |
+
raise ValueError(f"Cada límite debe ser una tupla/lista de dos elementos (low, high). Se encontró: {item}")
|
54 |
+
# np.nan puede ser usado para indicar sin límite, curve_fit lo trata como -inf/inf
|
55 |
+
low = -np.inf if (item[0] is None or (isinstance(item[0], float) and np.isnan(item[0]))) else float(item[0])
|
56 |
+
high = np.inf if (item[1] is None or (isinstance(item[1], float) and np.isnan(item[1]))) else float(item[1])
|
57 |
+
lower_bounds.append(low)
|
58 |
+
upper_bounds.append(high)
|
59 |
+
|
60 |
+
return lower_bounds, upper_bounds
|
61 |
+
except Exception as e:
|
62 |
+
print(f"Error al parsear los límites '{bounds_str_input}': {e}. Usando límites por defecto (-inf, inf).")
|
63 |
+
return [-np.inf] * num_params, [np.inf] * num_params
|
64 |
+
|
65 |
+
|
66 |
+
def call_llm_analysis_service(prompt: str) -> str:
|
67 |
+
"""Llama al servicio LLM (ya sea localmente o a través de Modal)."""
|
68 |
+
if USE_MODAL_FOR_LLM_ANALYSIS and generate_analysis_from_modal:
|
69 |
+
print("interface.py: Usando la función de análisis LLM de Modal...")
|
70 |
+
try:
|
71 |
+
# La función wrapper en modal_app.py obtiene MODEL_PATH y MAX_LENGTH de config.py
|
72 |
+
return generate_analysis_from_modal(prompt)
|
73 |
+
except Exception as e_modal_call:
|
74 |
+
print(f"Error llamando a la función Modal LLM: {e_modal_call}")
|
75 |
+
return f"Error al contactar el servicio de análisis IA (Modal): {e_modal_call}"
|
76 |
+
else:
|
77 |
+
# --- Implementación de Fallback (o si no se usa Modal) ---
|
78 |
+
print("interface.py: Usando la función de análisis LLM local (fallback)...")
|
79 |
+
try:
|
80 |
+
# Esta parte necesitaría que cargues el modelo localmente
|
81 |
+
# como lo hacías en tu versión original de interface.py
|
82 |
+
from config import MODEL_PATH, MAX_LENGTH, DEVICE # Importar configuración local
|
83 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM # Importaciones locales
|
84 |
+
|
85 |
+
print(f"Fallback: Cargando modelo {MODEL_PATH} localmente en {DEVICE}...")
|
86 |
+
tokenizer_local = AutoTokenizer.from_pretrained(MODEL_PATH)
|
87 |
+
model_local = AutoModelForCausalLM.from_pretrained(MODEL_PATH).to(DEVICE) # No .eval() aquí
|
88 |
+
|
89 |
+
inputs = tokenizer_local(prompt, return_tensors="pt").to(DEVICE)
|
90 |
+
with torch.no_grad(): # Importante para inferencia
|
91 |
+
outputs = model_local.generate(
|
92 |
+
**inputs,
|
93 |
+
max_new_tokens=MAX_LENGTH,
|
94 |
+
eos_token_id=tokenizer_local.eos_token_id,
|
95 |
+
pad_token_id=tokenizer_local.pad_token_id if tokenizer_local.pad_token_id else tokenizer_local.eos_token_id,
|
96 |
+
do_sample=True, temperature=0.6, top_p=0.9
|
97 |
+
)
|
98 |
+
input_len = inputs.input_ids.shape[1]
|
99 |
+
analysis = tokenizer_local.decode(outputs[0][input_len:], skip_special_tokens=True)
|
100 |
+
return analysis.strip()
|
101 |
+
except Exception as e_local_llm:
|
102 |
+
print(f"Error en el fallback LLM local: {e_local_llm}")
|
103 |
+
return f"Análisis (fallback local): Error al cargar/ejecutar modelo LLM local: {e_local_llm}."
|
104 |
+
|
105 |
+
# @gpu_decorator(duration=100) # Este decorador es de HF Spaces. Modal gestiona GPU diferente.
|
106 |
+
def process_and_plot(
|
107 |
+
file_obj, # Gradio pasa un objeto File
|
108 |
+
# Entradas de la UI (desempaquetadas)
|
109 |
+
biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
|
110 |
+
biomass_param1_ui, biomass_param2_ui, biomass_param3_ui,
|
111 |
+
biomass_bound1_ui, biomass_bound2_ui, biomass_bound3_ui,
|
112 |
+
substrate_eq1_ui, substrate_eq2_ui, substrate_eq3_ui,
|
113 |
+
substrate_param1_ui, substrate_param2_ui, substrate_param3_ui,
|
114 |
+
substrate_bound1_ui, substrate_bound2_ui, substrate_bound3_ui,
|
115 |
+
product_eq1_ui, product_eq2_ui, product_eq3_ui,
|
116 |
+
product_param1_ui, product_param2_ui, product_param3_ui,
|
117 |
+
product_bound1_ui, product_bound2_ui, product_bound3_ui,
|
118 |
+
legend_position_ui,
|
119 |
+
show_legend_ui,
|
120 |
+
show_params_ui,
|
121 |
+
biomass_eq_count_ui,
|
122 |
+
substrate_eq_count_ui,
|
123 |
+
product_eq_count_ui
|
124 |
+
):
|
125 |
+
analysis_text = "Iniciando análisis..."
|
126 |
+
if file_obj is None:
|
127 |
+
return None, "Error: Por favor, sube un archivo Excel."
|
128 |
+
|
129 |
+
try:
|
130 |
+
df = pd.read_excel(file_obj.name) # .name para obtener el path del archivo temporal de Gradio
|
131 |
+
except Exception as e:
|
132 |
+
return None, f"Error al leer el archivo Excel: {e}\n{traceback.format_exc()}"
|
133 |
+
|
134 |
+
expected_cols = ['Tiempo', 'Biomasa', 'Sustrato', 'Producto']
|
135 |
+
for col in expected_cols:
|
136 |
+
if col not in df.columns:
|
137 |
+
return None, f"Error: La columna '{col}' no se encuentra en el archivo Excel."
|
138 |
+
|
139 |
+
time_data = df['Tiempo'].values
|
140 |
+
biomass_data_exp = df['Biomasa'].values
|
141 |
+
substrate_data_exp = df['Sustrato'].values
|
142 |
+
product_data_exp = df['Producto'].values
|
143 |
+
|
144 |
+
# Convertir contadores a enteros
|
145 |
+
active_biomass_eqs = int(biomass_eq_count_ui)
|
146 |
+
active_substrate_eqs = int(substrate_eq_count_ui)
|
147 |
+
active_product_eqs = int(product_eq_count_ui)
|
148 |
+
|
149 |
+
# Agrupar entradas de la UI
|
150 |
+
all_eq_inputs = {
|
151 |
+
'biomass': (
|
152 |
+
[biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui][:active_biomass_eqs],
|
153 |
+
[biomass_param1_ui, biomass_param2_ui, biomass_param3_ui][:active_biomass_eqs],
|
154 |
+
[biomass_bound1_ui, biomass_bound2_ui, biomass_bound3_ui][:active_biomass_eqs],
|
155 |
+
biomass_data_exp
|
156 |
+
),
|
157 |
+
'substrate': (
|
158 |
+
[substrate_eq1_ui, substrate_eq2_ui, substrate_eq3_ui][:active_substrate_eqs],
|
159 |
+
[substrate_param1_ui, substrate_param2_ui, substrate_param3_ui][:active_substrate_eqs],
|
160 |
+
[substrate_bound1_ui, substrate_bound2_ui, substrate_bound3_ui][:active_substrate_eqs],
|
161 |
+
substrate_data_exp
|
162 |
+
),
|
163 |
+
'product': (
|
164 |
+
[product_eq1_ui, product_eq2_ui, product_eq3_ui][:active_product_eqs],
|
165 |
+
[product_param1_ui, product_param2_ui, product_param3_ui][:active_product_eqs],
|
166 |
+
[product_bound1_ui, product_bound2_ui, product_bound3_ui][:active_product_eqs],
|
167 |
+
product_data_exp
|
168 |
+
)
|
169 |
+
}
|
170 |
+
|
171 |
+
model_handler = BioprocessModel() # De TU models.py (el que usa sympy)
|
172 |
+
|
173 |
+
fitted_results_for_plot = {'biomass': [], 'substrate': [], 'product': []}
|
174 |
+
results_for_llm_prompt = {'biomass': [], 'substrate': [], 'product': []}
|
175 |
+
biomass_params_for_s_p = None # Para almacenar parámetros de biomasa ajustados
|
176 |
+
|
177 |
+
for model_type, (eq_list, param_str_list, bound_str_list, exp_data) in all_eq_inputs.items():
|
178 |
+
if not exp_data.any(): # Si no hay datos experimentales para este componente, saltar
|
179 |
+
print(f"No hay datos experimentales para {model_type}, saltando ajuste.")
|
180 |
+
continue
|
181 |
+
|
182 |
+
for i in range(len(eq_list)):
|
183 |
+
eq_str = eq_list[i]
|
184 |
+
param_s = param_str_list[i]
|
185 |
+
bound_s = bound_str_list[i]
|
186 |
+
|
187 |
+
if not eq_str or not param_s:
|
188 |
+
print(f"Ecuación o parámetros vacíos para {model_type} #{i+1}, saltando.")
|
189 |
+
continue
|
190 |
+
|
191 |
+
print(f"Procesando {model_type} #{i+1}: Eq='{eq_str}', Params='{param_s}'")
|
192 |
+
|
193 |
+
try:
|
194 |
+
model_handler.set_model(model_type, eq_str, param_s)
|
195 |
+
num_p = len(model_handler.models[model_type]['params'])
|
196 |
+
l_b, u_b = parse_bounds_str(bound_s, num_p)
|
197 |
+
|
198 |
+
# Pasar biomass_params_fitted si es sustrato o producto
|
199 |
+
current_biomass_params = biomass_params_for_s_p if model_type in ['substrate', 'product'] else None
|
200 |
+
|
201 |
+
y_pred, popt = model_handler.fit_model(model_type, time_data, exp_data, bounds=(l_b, u_b), biomass_params_fitted=current_biomass_params)
|
202 |
+
|
203 |
+
# Guardar resultados
|
204 |
+
current_params = model_handler.params[model_type]
|
205 |
+
r2_val = model_handler.r2.get(model_type, float('nan'))
|
206 |
+
rmse_val = model_handler.rmse.get(model_type, float('nan'))
|
207 |
+
|
208 |
+
fitted_results_for_plot[model_type].append({
|
209 |
+
'equation': eq_str,
|
210 |
+
'y_pred': y_pred,
|
211 |
+
'params': current_params,
|
212 |
+
'R2': r2_val
|
213 |
+
})
|
214 |
+
results_for_llm_prompt[model_type].append({
|
215 |
+
'equation': eq_str,
|
216 |
+
'params_fitted': current_params,
|
217 |
+
'R2': r2_val,
|
218 |
+
'RMSE': rmse_val
|
219 |
+
})
|
220 |
+
|
221 |
+
# Si es el primer modelo de biomasa ajustado con éxito, guardar sus parámetros
|
222 |
+
if model_type == 'biomass' and biomass_params_for_s_p is None:
|
223 |
+
biomass_params_for_s_p = current_params
|
224 |
+
print(f"Parámetros de Biomasa (para S/P): {biomass_params_for_s_p}")
|
225 |
+
|
226 |
+
except Exception as e:
|
227 |
+
error_msg = f"Error ajustando {model_type} #{i+1} ('{eq_str}'): {e}\n{traceback.format_exc()}"
|
228 |
+
print(error_msg)
|
229 |
+
# Devolver error a la UI en lugar de solo None
|
230 |
+
return None, error_msg
|
231 |
+
|
232 |
+
# Generar gráfico
|
233 |
+
fig, axs = plt.subplots(3, 1, figsize=(10, 18), sharex=True) # Aumentar altura
|
234 |
+
plot_config = {
|
235 |
+
axs[0]: (biomass_data_exp, 'Biomasa', fitted_results_for_plot['biomass']),
|
236 |
+
axs[1]: (substrate_data_exp, 'Sustrato', fitted_results_for_plot['substrate']),
|
237 |
+
axs[2]: (product_data_exp, 'Producto', fitted_results_for_plot['product'])
|
238 |
+
}
|
239 |
+
|
240 |
+
for ax, data_actual, ylabel, plot_results_list in plot_config.items():
|
241 |
+
ax.plot(time_data, data_actual, 'o', label=f'Datos {ylabel}', markersize=5, alpha=0.7)
|
242 |
+
for idx, res_detail in enumerate(plot_results_list):
|
243 |
+
label = f'Modelo {idx+1} (R²:{res_detail["R2"]:.3f})'
|
244 |
+
# if len(plot_results_list) == 1: label = f'Modelo {ylabel} (R²:{res_detail["R2"]:.3f})'
|
245 |
+
ax.plot(time_data, res_detail['y_pred'], '-', label=label, linewidth=2)
|
246 |
+
ax.set_xlabel('Tiempo')
|
247 |
+
ax.set_ylabel(ylabel)
|
248 |
+
ax.grid(True, linestyle=':', alpha=0.7)
|
249 |
+
if show_legend_ui:
|
250 |
+
ax.legend(loc=legend_position_ui, fontsize='small')
|
251 |
+
|
252 |
+
if show_params_ui and plot_results_list:
|
253 |
+
# Mostrar parámetros para todos los modelos ajustados en este subplot
|
254 |
+
param_display_texts = []
|
255 |
+
for idx, res_detail in enumerate(plot_results_list):
|
256 |
+
params_text = f"Modelo {idx+1}:\n" + "\n".join([f" {k}: {v:.4g}" for k,v in res_detail['params'].items()])
|
257 |
+
param_display_texts.append(params_text)
|
258 |
+
full_param_text = "\n---\n".join(param_display_texts)
|
259 |
+
|
260 |
+
# Ajustar posición del texto para que no se solape con la leyenda si es posible
|
261 |
+
text_x_pos = 0.02
|
262 |
+
text_y_pos = 0.98
|
263 |
+
v_align = 'top'
|
264 |
+
if legend_position_ui and 'upper' in legend_position_ui:
|
265 |
+
text_y_pos = 0.02
|
266 |
+
v_align = 'bottom'
|
267 |
+
|
268 |
+
ax.text(text_x_pos, text_y_pos, full_param_text, transform=ax.transAxes, fontsize=7,
|
269 |
+
verticalalignment=v_align, bbox=dict(boxstyle='round,pad=0.3', fc='lightyellow', alpha=0.8))
|
270 |
+
|
271 |
+
plt.tight_layout(rect=[0, 0, 1, 0.96]) # Ajustar para el suptitle si lo hubiera
|
272 |
+
fig.suptitle("Resultados del Ajuste de Modelos Cinéticos", fontsize=16)
|
273 |
+
|
274 |
+
buf = io.BytesIO()
|
275 |
+
plt.savefig(buf, format='png', dpi=150) # Aumentar dpi para mejor calidad
|
276 |
+
buf.seek(0)
|
277 |
+
image = Image.open(buf)
|
278 |
+
plt.close(fig) # Cerrar la figura para liberar memoria
|
279 |
+
|
280 |
+
# Construir prompt para LLM
|
281 |
+
prompt_intro = "Eres un experto en modelado cinético de bioprocesos. Analiza los siguientes resultados del ajuste de modelos a datos experimentales:\n\n"
|
282 |
+
prompt_details = json.dumps(results_for_llm_prompt, indent=2, ensure_ascii=False)
|
283 |
+
prompt_instructions = """\n\nPor favor, proporciona un análisis detallado y crítico en español, estructurado de la siguiente manera:
|
284 |
+
1. **Resumen General:** Una breve descripción del experimento y qué se intentó modelar.
|
285 |
+
2. **Análisis por Componente (Biomasa, Sustrato, Producto):**
|
286 |
+
a. Para cada ecuación probada:
|
287 |
+
i. Calidad del Ajuste: Evalúa el R² (cercano a 1 es ideal) y el RMSE (más bajo es mejor). Comenta si el ajuste es bueno, regular o pobre.
|
288 |
+
ii. Interpretación de Parámetros: Explica brevemente qué representan los parámetros ajustados y si sus valores parecen razonables en un contexto de bioproceso (ej. tasas positivas, concentraciones no negativas).
|
289 |
+
iii. Ecuación Específica: Menciona la ecuación usada.
|
290 |
+
b. Comparación (si se probó más de una ecuación para un componente): ¿Cuál ecuación proporcionó el mejor ajuste y por qué?
|
291 |
+
3. **Problemas y Limitaciones:**
|
292 |
+
a. ¿Hay problemas evidentes (ej. R² muy bajo, parámetros físicamente no realistas, sobreajuste si se puede inferir, etc.)?
|
293 |
+
b. ¿Qué limitaciones podrían tener los modelos o el proceso de ajuste?
|
294 |
+
4. **Sugerencias y Próximos Pasos:**
|
295 |
+
a. ¿Cómo se podría mejorar el modelado (ej. probar otras ecuaciones, transformar datos, revisar calidad de datos experimentales)?
|
296 |
+
b. ¿Qué experimentos adicionales podrían realizarse para validar o refinar los modelos?
|
297 |
+
5. **Conclusión Final:** Un veredicto general sobre el éxito del modelado y la utilidad de los resultados obtenidos.
|
298 |
+
|
299 |
+
Utiliza un lenguaje claro y accesible, pero manteniendo el rigor técnico. El análisis debe ser útil para alguien que busca entender la cinética de su bioproceso."""
|
300 |
+
|
301 |
+
full_prompt = prompt_intro + prompt_details + prompt_instructions
|
302 |
+
|
303 |
+
# Llamar al servicio LLM (ya sea Modal o local)
|
304 |
+
analysis_text = call_llm_analysis_service(full_prompt)
|
305 |
+
|
306 |
+
return image, analysis_text
|
model_app.py
ADDED
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# modal_app.py
|
2 |
+
import modal
|
3 |
+
import sys
|
4 |
+
from pathlib import Path
|
5 |
+
import os # Para HF_TOKEN
|
6 |
+
|
7 |
+
# --- Configuración ---
|
8 |
+
PYTHON_VERSION = "3.10"
|
9 |
+
APP_NAME = "bioprocess-custom-eq-agent-modal"
|
10 |
+
|
11 |
+
# Directorios (asume que modal_app.py está en la raíz del proyecto junto a los otros .py)
|
12 |
+
LOCAL_APP_DIR = Path(__file__).parent
|
13 |
+
REMOTE_APP_DIR = "/app" # Directorio dentro del contenedor Modal
|
14 |
+
|
15 |
+
stub = modal.Stub(APP_NAME)
|
16 |
+
|
17 |
+
# Definición de la imagen del contenedor Modal
|
18 |
+
app_image = (
|
19 |
+
modal.Image.debian_slim(python_version=PYTHON_VERSION)
|
20 |
+
.pip_install_from_requirements(LOCAL_APP_DIR / "requirements.txt")
|
21 |
+
.copy_mount(
|
22 |
+
modal.Mount.from_local_dir(LOCAL_APP_DIR, remote_path=REMOTE_APP_DIR)
|
23 |
+
)
|
24 |
+
.env({
|
25 |
+
"PYTHONPATH": REMOTE_APP_DIR,
|
26 |
+
"HF_HOME": "/cache/huggingface", # Directorio de caché de Hugging Face
|
27 |
+
"HF_HUB_CACHE": "/cache/huggingface/hub",
|
28 |
+
"TRANSFORMERS_CACHE": "/cache/huggingface/hub", # Alias común
|
29 |
+
"MPLCONFIGDIR": "/tmp/matplotlib_cache" # Para evitar warnings de matplotlib
|
30 |
+
})
|
31 |
+
.run_commands( # Comandos para ejecutar durante la construcción de la imagen
|
32 |
+
"apt-get update && apt-get install -y git git-lfs && rm -rf /var/lib/apt/lists/*", # git-lfs para algunos modelos
|
33 |
+
"mkdir -p /cache/huggingface/hub /tmp/matplotlib_cache" # Crear directorios de caché
|
34 |
+
)
|
35 |
+
)
|
36 |
+
|
37 |
+
# --- Función Modal para Generación de Análisis con LLM ---
|
38 |
+
@stub.function(
|
39 |
+
image=app_image,
|
40 |
+
gpu="any", # Solicitar GPU (ej. "T4", "A10G", o "any")
|
41 |
+
secrets=[
|
42 |
+
modal.Secret.from_name("huggingface-read-token", optional=True) # Para modelos privados/gated
|
43 |
+
],
|
44 |
+
timeout=600, # 10 minutos de timeout
|
45 |
+
# Montar un volumen para cachear modelos de Hugging Face
|
46 |
+
volumes={"/cache/huggingface": modal.Volume.persisted(f"{APP_NAME}-hf-cache-vol")}
|
47 |
+
)
|
48 |
+
def generate_analysis_llm_modal_remote(prompt: str, model_path_config: str, max_new_tokens_config: int) -> str:
|
49 |
+
import torch
|
50 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
51 |
+
|
52 |
+
# El token de HF se inyecta como variable de entorno si el secreto está configurado
|
53 |
+
hf_token = os.environ.get("HUGGING_FACE_TOKEN")
|
54 |
+
|
55 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
56 |
+
print(f"LLM Modal Func: Usando dispositivo: {device}")
|
57 |
+
print(f"LLM Modal Func: Cargando modelo: {model_path_config} con token: {'Sí' if hf_token else 'No'}")
|
58 |
+
|
59 |
+
try:
|
60 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path_config, cache_dir="/cache/huggingface/hub", token=hf_token)
|
61 |
+
model = AutoModelForCausalLM.from_pretrained(
|
62 |
+
model_path_config,
|
63 |
+
torch_dtype="auto", # bfloat16 en A100/H100, float16 en otras
|
64 |
+
device_map="auto", # Distribuye automáticamente en GPUs disponibles
|
65 |
+
cache_dir="/cache/huggingface/hub",
|
66 |
+
token=hf_token,
|
67 |
+
# low_cpu_mem_usage=True # Puede ayudar con modelos muy grandes
|
68 |
+
)
|
69 |
+
# No es necesario .to(device) explícitamente con device_map="auto"
|
70 |
+
# model.eval() no es necesario si solo se hace inferencia y no se entrena
|
71 |
+
|
72 |
+
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=4096-max_new_tokens_config).to(model.device) # Truncar prompt si es muy largo
|
73 |
+
|
74 |
+
with torch.no_grad():
|
75 |
+
outputs = model.generate(
|
76 |
+
**inputs,
|
77 |
+
max_new_tokens=max_new_tokens_config,
|
78 |
+
eos_token_id=tokenizer.eos_token_id,
|
79 |
+
pad_token_id=tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id,
|
80 |
+
do_sample=True,
|
81 |
+
temperature=0.6, # Ajustar para creatividad vs factualidad
|
82 |
+
top_p=0.9,
|
83 |
+
# num_beams=1 # Usar num_beams > 1 para beam search si se desea, pero más lento
|
84 |
+
)
|
85 |
+
|
86 |
+
# Decodificar solo los tokens generados nuevos, no el prompt
|
87 |
+
input_length = inputs.input_ids.shape[1]
|
88 |
+
generated_ids = outputs[0][input_length:]
|
89 |
+
analysis = tokenizer.decode(generated_ids, skip_special_tokens=True)
|
90 |
+
|
91 |
+
print(f"LLM Modal Func: Longitud del análisis generado: {len(analysis)} caracteres.")
|
92 |
+
return analysis.strip()
|
93 |
+
except Exception as e:
|
94 |
+
error_traceback = traceback.format_exc()
|
95 |
+
print(f"Error en generate_analysis_llm_modal_remote: {e}\n{error_traceback}")
|
96 |
+
return f"Error al generar análisis con el modelo LLM: {str(e)}"
|
97 |
+
|
98 |
+
# --- Servidor Gradio ---
|
99 |
+
@stub.asgi_app() # image se hereda del stub si no se especifica aquí
|
100 |
+
def serve_gradio_app_asgi():
|
101 |
+
# Estas importaciones ocurren DENTRO del contenedor Modal
|
102 |
+
import gradio as gr
|
103 |
+
sys.path.insert(0, REMOTE_APP_DIR) # Asegurar que los módulos de la app son importables
|
104 |
+
|
105 |
+
# Importar los módulos de la aplicación AHORA que sys.path está configurado
|
106 |
+
from UI import create_interface
|
107 |
+
import interface as app_interface_module # Renombrar para claridad
|
108 |
+
from config import MODEL_PATH as cfg_MODEL_PATH, MAX_LENGTH as cfg_MAX_LENGTH
|
109 |
+
|
110 |
+
# Wrapper para llamar a la función Modal remota desde tu interface.py
|
111 |
+
def analysis_func_wrapper_for_interface(prompt: str) -> str:
|
112 |
+
print("Gradio Backend: Llamando a generate_analysis_llm_modal_remote.remote...")
|
113 |
+
return generate_analysis_llm_modal_remote.remote(prompt, cfg_MODEL_PATH, cfg_MAX_LENGTH)
|
114 |
+
|
115 |
+
# Inyectar esta función wrapper en el módulo `interface`
|
116 |
+
app_interface_module.generate_analysis_from_modal = analysis_func_wrapper_for_interface
|
117 |
+
app_interface_module.USE_MODAL_FOR_LLM_ANALYSIS = True
|
118 |
+
|
119 |
+
# Crear la app Gradio y conectar el botón
|
120 |
+
gradio_ui, all_ui_inputs, ui_outputs, ui_submit_button = create_interface()
|
121 |
+
|
122 |
+
ui_submit_button.click(
|
123 |
+
fn=app_interface_module.process_and_plot,
|
124 |
+
inputs=all_ui_inputs,
|
125 |
+
outputs=ui_outputs
|
126 |
+
)
|
127 |
+
|
128 |
+
return gr.routes.App.create_app(gradio_ui) # Para montar Gradio en FastAPI/ASGI
|
129 |
+
|
130 |
+
# (Opcional) Un entrypoint local para probar rápidamente la generación LLM
|
131 |
+
@stub.local_entrypoint()
|
132 |
+
def test_llm():
|
133 |
+
print("Probando la generación de LLM con Modal (localmente)...")
|
134 |
+
from config import MODEL_PATH, MAX_LENGTH
|
135 |
+
sample_prompt = "Explica brevemente el concepto de R cuadrado (R²) en el ajuste de modelos."
|
136 |
+
analysis = generate_analysis_llm_modal_remote.remote(sample_prompt, MODEL_PATH, MAX_LENGTH)
|
137 |
+
print("\nRespuesta del LLM:")
|
138 |
+
print(analysis)
|
models.py
ADDED
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# models.py
|
2 |
+
import numpy as np
|
3 |
+
from scipy.optimize import curve_fit
|
4 |
+
from sympy import symbols, sympify, lambdify
|
5 |
+
import warnings
|
6 |
+
from sklearn.metrics import mean_squared_error # Asegúrate que está importado
|
7 |
+
|
8 |
+
class BioprocessModel:
|
9 |
+
def __init__(self):
|
10 |
+
self.params = {} # Almacenará los parámetros ajustados para cada tipo de modelo
|
11 |
+
self.models = {} # Almacenará la configuración de cada tipo de modelo (ecuación, función, etc.)
|
12 |
+
self.r2 = {} # Almacenará el R^2 para cada tipo de modelo
|
13 |
+
self.rmse = {} # Almacenará el RMSE para cada tipo de modelo
|
14 |
+
|
15 |
+
def set_model(self, model_type, equation_str, param_str):
|
16 |
+
"""
|
17 |
+
Configura un modelo (Biomasa, Sustrato, Producto) usando una ecuación simbólica.
|
18 |
+
|
19 |
+
:param model_type: 'biomass', 'substrate', o 'product'
|
20 |
+
:param equation_str: La ecuación como una cadena de texto (ej. "Xm * (1 - exp(-um * t))")
|
21 |
+
Si la ecuación es para sustrato o producto y depende de la biomasa X(t),
|
22 |
+
se debe usar 'X_val' en la ecuación para representar el valor de X(t).
|
23 |
+
:param param_str: Cadena de parámetros separados por comas (ej. "Xm, um")
|
24 |
+
"""
|
25 |
+
equation_str = equation_str.strip()
|
26 |
+
# Si el usuario escribe "Y = ...", tomar solo la parte derecha
|
27 |
+
if '=' in equation_str:
|
28 |
+
equation_str = equation_str.split('=', 1)[1].strip()
|
29 |
+
|
30 |
+
params_list = [param.strip() for param in param_str.split(',')]
|
31 |
+
|
32 |
+
self.models[model_type] = {
|
33 |
+
'equation_str': equation_str,
|
34 |
+
'params': params_list
|
35 |
+
}
|
36 |
+
|
37 |
+
# Símbolos para el tiempo y los parámetros del modelo actual
|
38 |
+
t_sym = symbols('t')
|
39 |
+
current_param_syms = symbols(params_list)
|
40 |
+
|
41 |
+
# Argumentos para lambdify
|
42 |
+
lambdify_args = [t_sym] + list(current_param_syms)
|
43 |
+
|
44 |
+
# Si el modelo es sustrato o producto, puede depender de la biomasa (X_val)
|
45 |
+
# y de los parámetros de biomasa ajustados.
|
46 |
+
# La función `lambdify` solo manejará los parámetros directos de este modelo.
|
47 |
+
# La dependencia de X(t) se resolverá en la función de ajuste.
|
48 |
+
|
49 |
+
# Si la ecuación contiene 'X_val' (representando X(t) para modelos S y P)
|
50 |
+
# lo añadimos como un símbolo que se sustituirá en la función de ajuste.
|
51 |
+
all_symbols_in_eq = sympify(equation_str).free_symbols
|
52 |
+
|
53 |
+
final_lambdify_args = list(lambdify_args) # Copia para modificar
|
54 |
+
|
55 |
+
# Manejo de X_val para modelos S y P que dependen de biomasa
|
56 |
+
# No es necesario añadir X_val a lambdify_args aquí si se maneja en fit_model_wrapper
|
57 |
+
# func = lambdify(final_lambdify_args, sympify(equation_str), 'numpy')
|
58 |
+
|
59 |
+
# Guardar los símbolos para uso posterior si es necesario
|
60 |
+
self.models[model_type]['sympy_expr'] = sympify(equation_str)
|
61 |
+
self.models[model_type]['param_symbols'] = current_param_syms
|
62 |
+
self.models[model_type]['time_symbol'] = t_sym
|
63 |
+
# La función compilada ('function') se creará dinámicamente en fit_model para manejar dependencias
|
64 |
+
|
65 |
+
def fit_model(self, model_type, time, data, bounds, biomass_params_fitted=None):
|
66 |
+
"""
|
67 |
+
Ajusta el modelo configurado a los datos.
|
68 |
+
|
69 |
+
:param model_type: 'biomass', 'substrate', o 'product'
|
70 |
+
:param time: Array de datos de tiempo
|
71 |
+
:param data: Array de datos observados
|
72 |
+
:param bounds: Tupla (lower_bounds, upper_bounds) para los parámetros
|
73 |
+
:param biomass_params_fitted: Dict de parámetros de biomasa ajustados (necesario para sustrato/producto)
|
74 |
+
:return: (y_pred, popt) - Datos predichos y parámetros optimizados
|
75 |
+
"""
|
76 |
+
if model_type not in self.models:
|
77 |
+
raise ValueError(f"Modelo para '{model_type}' no configurado. Llama a set_model primero.")
|
78 |
+
|
79 |
+
model_config = self.models[model_type]
|
80 |
+
equation_expr = model_config['sympy_expr']
|
81 |
+
current_param_names = model_config['params']
|
82 |
+
current_param_syms = model_config['param_symbols']
|
83 |
+
t_sym = model_config['time_symbol']
|
84 |
+
|
85 |
+
# Construir la función a ajustar con curve_fit
|
86 |
+
# Esta función tomará `t` y los parámetros del modelo actual como `*args`
|
87 |
+
def fit_model_wrapper(t_data, *current_p_values):
|
88 |
+
# Crear un diccionario de sustitución para los parámetros del modelo actual
|
89 |
+
subs_dict = {sym: val for sym, val in zip(current_param_syms, current_p_values)}
|
90 |
+
|
91 |
+
# Si es sustrato o producto y depende de la biomasa (X_val)
|
92 |
+
X_val_sym = symbols('X_val') # Símbolo para X(t)
|
93 |
+
if model_type in ['substrate', 'product'] and X_val_sym in equation_expr.free_symbols:
|
94 |
+
if biomass_params_fitted is None or 'biomass' not in self.models:
|
95 |
+
raise ValueError("Parámetros de biomasa ajustados son necesarios para modelos de sustrato/producto que dependen de X(t).")
|
96 |
+
|
97 |
+
# Calcular X(t) usando el modelo de biomasa ajustado y los t_data actuales
|
98 |
+
biomass_model_config = self.models['biomass']
|
99 |
+
biomass_expr = biomass_model_config['sympy_expr']
|
100 |
+
biomass_p_syms = biomass_model_config['param_symbols']
|
101 |
+
|
102 |
+
# Crear dict de sustitución para parámetros de biomasa
|
103 |
+
biomass_subs_dict = {sym: biomass_params_fitted[name] for sym, name in zip(biomass_p_syms, biomass_model_config['params'])}
|
104 |
+
|
105 |
+
# Evaluar X(t) para cada punto en t_data
|
106 |
+
X_t_values = np.array([
|
107 |
+
biomass_expr.subs(biomass_subs_dict).subs({biomass_model_config['time_symbol']: ti}).evalf()
|
108 |
+
for ti in t_data
|
109 |
+
], dtype=float)
|
110 |
+
|
111 |
+
# Añadir X_val (X(t)) al diccionario de sustituciones para la ecuación actual
|
112 |
+
subs_dict[X_val_sym] = X_t_values
|
113 |
+
|
114 |
+
# Evaluar la ecuación del modelo actual (sustrato, producto, o biomasa)
|
115 |
+
# con los valores de sus parámetros y X(t) si es aplicable
|
116 |
+
# Es importante que la ecuación use t_sym como variable independiente
|
117 |
+
# y que lambdify se haga sobre (t_sym, *current_param_syms, X_val_sym_if_present)
|
118 |
+
|
119 |
+
# Re-lambdify aquí para asegurar el scope correcto de X_t_values si se usa X_val_sym
|
120 |
+
args_for_lambdify = [t_sym] + list(current_param_syms)
|
121 |
+
final_expr = equation_expr # Expresión original
|
122 |
+
|
123 |
+
# Si X_val está en la ecuación, NO lo incluimos en args_for_lambdify
|
124 |
+
# sino que lo sustituimos ANTES de lambdify o lo pasamos como constante si es posible.
|
125 |
+
# Lo más robusto es sustituir X_val en la expresión ANTES de lambdify
|
126 |
+
if X_val_sym in equation_expr.free_symbols and X_val_sym in subs_dict:
|
127 |
+
# Si X_val es un array (porque t_data es un array), no podemos sustituirlo directamente
|
128 |
+
# en la expresión simbólica para crear una única función lambdify que tome `t`.
|
129 |
+
# En su lugar, evaluamos la expresión punto por punto después de sustituir parámetros.
|
130 |
+
# Esto es menos eficiente que un lambdify completo, pero más flexible.
|
131 |
+
|
132 |
+
# Evaluación numérica punto por punto
|
133 |
+
# No necesitamos lambdify si X_val es un array y se evalúa punto por punto
|
134 |
+
# La función `fit_model_wrapper` ya está iterando sobre los `t_data` (implícitamente a través de numpy)
|
135 |
+
# Entonces, si X_val es un array, la expresión `final_expr.subs(subs_dict)` debería funcionar si
|
136 |
+
# las operaciones son compatibles con numpy arrays (sympy suele hacerlo).
|
137 |
+
#
|
138 |
+
# La forma en que se llama a esta función desde curve_fit es: `fit_model_wrapper(time, p1, p2, ...)`
|
139 |
+
# donde `time` es el array completo de tiempos.
|
140 |
+
# `X_t_values` es un array de la misma longitud que `time`.
|
141 |
+
|
142 |
+
# Simpler lambdify for the current model parameters, handle X_val in wrapper
|
143 |
+
func_for_current_model = lambdify(args_for_lambdify, equation_expr.subs({X_val_sym: X_val_sym}), 'numpy') # Keep X_val symbolic for now
|
144 |
+
|
145 |
+
# Now call it, and if it needs X_val, it must be part of its signature or global
|
146 |
+
# This part is tricky with sympy and curve_fit when there are interdependencies.
|
147 |
+
# The lambda should capture X_t_values correctly if subs_dict[X_val_sym] is set.
|
148 |
+
|
149 |
+
# Let's evaluate por punto para mayor robustez con X_val siendo un array
|
150 |
+
y_calculated = np.empty_like(t_data, dtype=float)
|
151 |
+
for i, ti in enumerate(t_data):
|
152 |
+
point_subs = subs_dict.copy()
|
153 |
+
point_subs[t_sym] = ti
|
154 |
+
if X_val_sym in point_subs and isinstance(point_subs[X_val_sym], np.ndarray):
|
155 |
+
# Si X_val es un array, tomar el valor correspondiente a ti
|
156 |
+
point_subs[X_val_sym] = point_subs[X_val_sym][i]
|
157 |
+
|
158 |
+
y_calculated[i] = equation_expr.subs(point_subs).evalf()
|
159 |
+
return y_calculated
|
160 |
+
|
161 |
+
else: # Modelo de Biomasa o modelo S/P que no usa X_val (raro)
|
162 |
+
func = lambdify(args_for_lambdify, equation_expr, 'numpy')
|
163 |
+
return func(t_data, *current_p_values)
|
164 |
+
|
165 |
+
|
166 |
+
p0 = np.ones(len(current_param_names)) # Estimaciones iniciales
|
167 |
+
lower_bounds, upper_bounds = bounds
|
168 |
+
|
169 |
+
# Asegurar que los límites tengan la longitud correcta
|
170 |
+
lower_bounds = np.array(lower_bounds if len(lower_bounds) == len(p0) else [-np.inf] * len(p0))
|
171 |
+
upper_bounds = np.array(upper_bounds if len(upper_bounds) == len(p0) else [np.inf] * len(p0))
|
172 |
+
|
173 |
+
# Ignorar advertencias de RuntimeWarning de operaciones inválidas (ej. división por cero durante la optimización)
|
174 |
+
with warnings.catch_warnings():
|
175 |
+
warnings.simplefilter("ignore", RuntimeWarning)
|
176 |
+
warnings.simplefilter("ignore", UserWarning) # Sympy lambdify warnings
|
177 |
+
popt, pcov = curve_fit(fit_model_wrapper, time, data, p0=p0, bounds=(lower_bounds, upper_bounds), maxfev=100000)
|
178 |
+
|
179 |
+
self.params[model_type] = dict(zip(current_param_names, popt))
|
180 |
+
|
181 |
+
# Calcular R^2 y RMSE usando los parámetros ajustados
|
182 |
+
# Es importante volver a llamar a fit_model_wrapper para obtener y_pred con los popt,
|
183 |
+
# ya que la función interna maneja las dependencias de X(t)
|
184 |
+
y_pred = fit_model_wrapper(time, *popt)
|
185 |
+
|
186 |
+
ss_res = np.sum((data - y_pred) ** 2)
|
187 |
+
ss_tot = np.sum((data - np.mean(data)) ** 2)
|
188 |
+
if ss_tot == 0: # Evitar división por cero si los datos son constantes
|
189 |
+
self.r2[model_type] = 1.0 if ss_res < 1e-9 else 0.0
|
190 |
+
else:
|
191 |
+
self.r2[model_type] = 1 - (ss_res / ss_tot)
|
192 |
+
|
193 |
+
self.rmse[model_type] = np.sqrt(mean_squared_error(data, y_pred))
|
194 |
+
|
195 |
+
return y_pred, popt # Devolver y_pred y los parámetros optimizados
|
requirements.txt
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==4.29.0
|
2 |
+
numpy
|
3 |
+
pandas
|
4 |
+
matplotlib
|
5 |
+
scipy
|
6 |
+
sympy
|
7 |
+
openpyxl
|
8 |
+
Pillow
|
9 |
+
torch
|
10 |
+
transformers
|
11 |
+
sentencepiece
|
12 |
+
scikit-learn
|
13 |
+
seaborn
|
14 |
+
accelerate
|
15 |
+
huggingface_hub
|