C2MV commited on
Commit
d3d8124
·
verified ·
1 Parent(s): b3a9926

Upload 8 files

Browse files
Files changed (8) hide show
  1. UI.py +125 -0
  2. app.py +21 -0
  3. config.py +17 -0
  4. decorators.py +32 -0
  5. interface.py +306 -0
  6. model_app.py +138 -0
  7. models.py +195 -0
  8. 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