C2MV commited on
Commit
00b61aa
·
verified ·
1 Parent(s): 1452a4c

Upload 2 files

Browse files
Files changed (2) hide show
  1. UI.py +20 -15
  2. interface.py +192 -197
UI.py CHANGED
@@ -3,7 +3,7 @@ import gradio as gr
3
  import numpy as np # Importar numpy para np.inf
4
 
5
  def create_interface(process_function_for_button):
6
- with gr.Blocks(theme='gradio/soft') as demo: # Usar un theme común de Gradio
7
  gr.Markdown("# Modelado de Bioprocesos con Ecuaciones Personalizadas y Análisis por IA")
8
 
9
  with gr.Row():
@@ -16,11 +16,10 @@ def create_interface(process_function_for_button):
16
  legend_position_ui = gr.Dropdown(
17
  label="Posición de la leyenda",
18
  choices=['best', 'upper right', 'upper left', 'lower right', 'lower left', 'center left', 'center right', 'lower center', 'upper center', 'center'],
19
- value='best' # String value, esto está bien
20
  )
21
  with gr.Column(scale=1):
22
  gr.Markdown("### Conteo de Ecuaciones a Probar")
23
- # Asegurar que los valores son numéricos y dentro del rango
24
  biomass_eq_count_ui = gr.Number(label="Biomasa (1-3)", value=1, minimum=1, maximum=3, step=1, precision=0)
25
  substrate_eq_count_ui = gr.Number(label="Sustrato (1-3)", value=1, minimum=1, maximum=3, step=1, precision=0)
26
  product_eq_count_ui = gr.Number(label="Producto (1-3)", value=1, minimum=1, maximum=3, step=1, precision=0)
@@ -30,10 +29,9 @@ def create_interface(process_function_for_button):
30
  with gr.Row():
31
  with gr.Column(): # Columna 1 siempre visible
32
  biomass_eq1_ui = gr.Textbox(label="Ecuación de Biomasa 1", value="Xm * (1 - exp(-um * (t - t_lag)))", lines=2, placeholder="Ej: Xm * (1 - exp(-um * (t - t_lag)))")
33
- biomass_param1_ui = gr.Textbox(label="Parámetros Biomasa 1", value="Xm, um, t_lag", info="Nombres, coma sep. 't' para tiempo. 'X_val' para X(t) en S/P.")
34
- biomass_bound1_ui = gr.Textbox(label="Límites Biomasa 1", value="(0, np.inf), (0, np.inf), (0, np.inf)", info="Formato: (low,high). Use np.inf o None.")
35
 
36
- # Definir Columnas 2 y 3 fuera del `with` si se manipula su visibilidad programáticamente
37
  biomass_col2_container = gr.Column(visible=False)
38
  with biomass_col2_container:
39
  biomass_eq2_ui = gr.Textbox(label="Ecuación de Biomasa 2", value="X0 * exp(um * t)", lines=2)
@@ -42,7 +40,7 @@ def create_interface(process_function_for_button):
42
 
43
  biomass_col3_container = gr.Column(visible=False)
44
  with biomass_col3_container:
45
- biomass_eq3_ui = gr.Textbox(label="Ecuación de Biomasa 3", lines=2, value="") # Explicit empty string
46
  biomass_param3_ui = gr.Textbox(label="Parámetros Biomasa 3", value="")
47
  biomass_bound3_ui = gr.Textbox(label="Límites Biomasa 3", value="")
48
 
@@ -89,9 +87,10 @@ def create_interface(process_function_for_button):
89
 
90
  # Lógica para mostrar/ocultar campos de ecuación dinámicamente
91
  def update_eq_visibility(count_value):
92
- # count_value es float si viene de gr.Number, convertir a int
93
- count = int(count_value)
94
- return gr.update(visible=count >= 2), gr.update(visible=count >= 3)
 
95
 
96
  biomass_eq_count_ui.change(fn=update_eq_visibility, inputs=biomass_eq_count_ui, outputs=[biomass_col2_container, biomass_col3_container])
97
  substrate_eq_count_ui.change(fn=update_eq_visibility, inputs=substrate_eq_count_ui, outputs=[substrate_col2_container, substrate_col3_container])
@@ -101,10 +100,11 @@ def create_interface(process_function_for_button):
101
 
102
  gr.Markdown("## Resultados del Análisis")
103
  with gr.Row():
104
- image_output = gr.Image(label="Gráfico Generado", type="pil", scale=2, show_download_button=True, height=600) # Ajustar altura
105
  with gr.Column(scale=3):
106
- analysis_output = gr.Markdown(label="Análisis del Modelo por IA") # Usar Markdown para mejor formato
107
 
 
108
  all_inputs_for_button = [
109
  file_input,
110
  biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
@@ -132,12 +132,17 @@ def create_interface(process_function_for_button):
132
  )
133
 
134
  # Inicializar visibilidad usando demo.load para que se aplique al cargar la UI
135
- def set_initial_visibility_on_load_wrapper(b_c, s_c, p_c):
136
- # Convertir a int, ya que el valor inicial de gr.Number puede ser float
137
- b_c_int, s_c_int, p_c_int = int(b_c), int(s_c), int(p_c)
 
 
 
 
138
  b_vis2_upd, b_vis3_upd = update_eq_visibility(b_c_int)
139
  s_vis2_upd, s_vis3_upd = update_eq_visibility(s_c_int)
140
  p_vis2_upd, p_vis3_upd = update_eq_visibility(p_c_int)
 
141
  return b_vis2_upd, b_vis3_upd, s_vis2_upd, s_vis3_upd, p_vis2_upd, p_vis3_upd
142
 
143
  demo.load(
 
3
  import numpy as np # Importar numpy para np.inf
4
 
5
  def create_interface(process_function_for_button):
6
+ with gr.Blocks(theme='gradio/soft') as demo:
7
  gr.Markdown("# Modelado de Bioprocesos con Ecuaciones Personalizadas y Análisis por IA")
8
 
9
  with gr.Row():
 
16
  legend_position_ui = gr.Dropdown(
17
  label="Posición de la leyenda",
18
  choices=['best', 'upper right', 'upper left', 'lower right', 'lower left', 'center left', 'center right', 'lower center', 'upper center', 'center'],
19
+ value='best'
20
  )
21
  with gr.Column(scale=1):
22
  gr.Markdown("### Conteo de Ecuaciones a Probar")
 
23
  biomass_eq_count_ui = gr.Number(label="Biomasa (1-3)", value=1, minimum=1, maximum=3, step=1, precision=0)
24
  substrate_eq_count_ui = gr.Number(label="Sustrato (1-3)", value=1, minimum=1, maximum=3, step=1, precision=0)
25
  product_eq_count_ui = gr.Number(label="Producto (1-3)", value=1, minimum=1, maximum=3, step=1, precision=0)
 
29
  with gr.Row():
30
  with gr.Column(): # Columna 1 siempre visible
31
  biomass_eq1_ui = gr.Textbox(label="Ecuación de Biomasa 1", value="Xm * (1 - exp(-um * (t - t_lag)))", lines=2, placeholder="Ej: Xm * (1 - exp(-um * (t - t_lag)))")
32
+ biomass_param1_ui = gr.Textbox(label="Parámetros Biomasa 1", value="Xm, um, t_lag", info="Nombres, coma sep. Use 't' para tiempo. 'X_val' para X(t) en S/P.")
33
+ biomass_bound1_ui = gr.Textbox(label="Límites Biomasa 1", value="(0, np.inf), (0, np.inf), (0, np.inf)", info="Formato: (low,high). Use np.inf.")
34
 
 
35
  biomass_col2_container = gr.Column(visible=False)
36
  with biomass_col2_container:
37
  biomass_eq2_ui = gr.Textbox(label="Ecuación de Biomasa 2", value="X0 * exp(um * t)", lines=2)
 
40
 
41
  biomass_col3_container = gr.Column(visible=False)
42
  with biomass_col3_container:
43
+ biomass_eq3_ui = gr.Textbox(label="Ecuación de Biomasa 3", lines=2, value="")
44
  biomass_param3_ui = gr.Textbox(label="Parámetros Biomasa 3", value="")
45
  biomass_bound3_ui = gr.Textbox(label="Límites Biomasa 3", value="")
46
 
 
87
 
88
  # Lógica para mostrar/ocultar campos de ecuación dinámicamente
89
  def update_eq_visibility(count_value):
90
+ # Asegurar que el valor es entero antes de la comparación
91
+ count = int(count_value)
92
+ # Retorna diccionarios de actualización para `gr.update`
93
+ return { "visible": count >= 2 }, { "visible": count >= 3 }
94
 
95
  biomass_eq_count_ui.change(fn=update_eq_visibility, inputs=biomass_eq_count_ui, outputs=[biomass_col2_container, biomass_col3_container])
96
  substrate_eq_count_ui.change(fn=update_eq_visibility, inputs=substrate_eq_count_ui, outputs=[substrate_col2_container, substrate_col3_container])
 
100
 
101
  gr.Markdown("## Resultados del Análisis")
102
  with gr.Row():
103
+ image_output = gr.Image(label="Gráfico Generado", type="pil", scale=2, show_download_button=True, height=600)
104
  with gr.Column(scale=3):
105
+ analysis_output = gr.Markdown(label="Análisis del Modelo por IA")
106
 
107
+ # Lista de todos los inputs para el botón de submit
108
  all_inputs_for_button = [
109
  file_input,
110
  biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
 
132
  )
133
 
134
  # Inicializar visibilidad usando demo.load para que se aplique al cargar la UI
135
+ # Esto asegura que el estado inicial de visibilidad es correcto
136
+ def set_initial_visibility_on_load_wrapper(b_c_val, s_c_val, p_c_val):
137
+ # Obtener los valores iniciales de los gr.Number components
138
+ # y aplicar la lógica de visibilidad.
139
+ # Los valores de los Number inputs pueden ser float, convertirlos a int
140
+ b_c_int, s_c_int, p_c_int = int(b_c_val), int(s_c_val), int(p_c_val)
141
+
142
  b_vis2_upd, b_vis3_upd = update_eq_visibility(b_c_int)
143
  s_vis2_upd, s_vis3_upd = update_eq_visibility(s_c_int)
144
  p_vis2_upd, p_vis3_upd = update_eq_visibility(p_c_int)
145
+
146
  return b_vis2_upd, b_vis3_upd, s_vis2_upd, s_vis3_upd, p_vis2_upd, p_vis3_upd
147
 
148
  demo.load(
interface.py CHANGED
@@ -2,44 +2,33 @@
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
 
@@ -51,9 +40,11 @@ def parse_bounds_str(bounds_str_input, num_params):
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
 
@@ -64,30 +55,30 @@ def parse_bounds_str(bounds_str_input, 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,
@@ -100,12 +91,12 @@ def call_llm_analysis_service(prompt: str) -> str:
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,
@@ -122,165 +113,163 @@ def process_and_plot(
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:
@@ -294,13 +283,19 @@ def process_and_plot(
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
 
 
 
 
 
 
2
  import numpy as np
3
  import pandas as pd
4
  import matplotlib
5
+ matplotlib.use('Agg')
6
  import matplotlib.pyplot as plt
7
  from PIL import Image
8
  import io
9
  import json
10
+ import traceback
11
 
12
+ from models import BioprocessModel # Asegúrate que esto apunta a tu models.py
13
+ # from decorators import gpu_decorator # Mantener comentado si usas Modal
 
14
 
15
+ # Variables globales inyectadas por modal_app.py o app.py
16
  USE_MODAL_FOR_LLM_ANALYSIS = False
17
+ generate_analysis_from_modal = None
18
 
19
  def parse_bounds_str(bounds_str_input, num_params):
20
+ bounds_str = str(bounds_str_input).strip()
21
+ if not bounds_str:
22
+ print(f"Cadena de límites vacía para {num_params} params. Usando (-inf, inf).")
 
 
 
 
23
  return [-np.inf] * num_params, [np.inf] * num_params
24
 
25
  try:
26
+ bounds_str = bounds_str.lower().replace('inf', 'np.inf').replace('none', 'None')
27
+ if not (bounds_str.startswith('[') and bounds_str.endswith(']')): # Asegurar que sea una lista
 
 
 
 
 
 
28
  bounds_str = f"[{bounds_str}]"
29
 
30
+ parsed_bounds_list = eval(bounds_str, {'np': np, 'inf': np.inf, 'None': None}) # Evaluar con np
31
+
32
  if not isinstance(parsed_bounds_list, list):
33
  raise ValueError("La cadena de límites no evaluó a una lista.")
34
 
 
40
  for item in parsed_bounds_list:
41
  if not (isinstance(item, (tuple, list)) and len(item) == 2):
42
  raise ValueError(f"Cada límite debe ser una tupla/lista de dos elementos (low, high). Se encontró: {item}")
43
+
44
+ # Convertir a float y manejar None/np.nan
45
  low = -np.inf if (item[0] is None or (isinstance(item[0], float) and np.isnan(item[0]))) else float(item[0])
46
  high = np.inf if (item[1] is None or (isinstance(item[1], float) and np.isnan(item[1]))) else float(item[1])
47
+
48
  lower_bounds.append(low)
49
  upper_bounds.append(high)
50
 
 
55
 
56
 
57
  def call_llm_analysis_service(prompt: str) -> str:
 
58
  if USE_MODAL_FOR_LLM_ANALYSIS and generate_analysis_from_modal:
59
  print("interface.py: Usando la función de análisis LLM de Modal...")
60
  try:
 
61
  return generate_analysis_from_modal(prompt)
62
  except Exception as e_modal_call:
63
  print(f"Error llamando a la función Modal LLM: {e_modal_call}")
64
+ traceback.print_exc() # Imprimir el traceback de la llamada a Modal
65
  return f"Error al contactar el servicio de análisis IA (Modal): {e_modal_call}"
66
  else:
 
67
  print("interface.py: Usando la función de análisis LLM local (fallback)...")
68
  try:
 
 
69
  from config import MODEL_PATH, MAX_LENGTH, DEVICE # Importar configuración local
70
  from transformers import AutoTokenizer, AutoModelForCausalLM # Importaciones locales
71
 
72
  print(f"Fallback: Cargando modelo {MODEL_PATH} localmente en {DEVICE}...")
73
  tokenizer_local = AutoTokenizer.from_pretrained(MODEL_PATH)
74
+ model_local = AutoModelForCausalLM.from_pretrained(MODEL_PATH).to(DEVICE)
75
 
76
+ model_context_window = getattr(model_local.config, 'max_position_embeddings', getattr(model_local.config, 'sliding_window', 4096))
77
+ max_prompt_len = model_context_window - MAX_LENGTH - 50
78
+ if max_prompt_len <= 0 : max_prompt_len = model_context_window // 2
79
+
80
+ inputs = tokenizer_local(prompt, return_tensors="pt", truncation=True, max_length=max_prompt_len).to(DEVICE)
81
+ with torch.no_grad():
82
  outputs = model_local.generate(
83
  **inputs,
84
  max_new_tokens=MAX_LENGTH,
 
91
  return analysis.strip()
92
  except Exception as e_local_llm:
93
  print(f"Error en el fallback LLM local: {e_local_llm}")
94
+ traceback.print_exc()
95
  return f"Análisis (fallback local): Error al cargar/ejecutar modelo LLM local: {e_local_llm}."
96
 
97
+
98
  def process_and_plot(
99
+ file_obj,
 
100
  biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
101
  biomass_param1_ui, biomass_param2_ui, biomass_param3_ui,
102
  biomass_bound1_ui, biomass_bound2_ui, biomass_bound3_ui,
 
113
  substrate_eq_count_ui,
114
  product_eq_count_ui
115
  ):
116
+ try: # Bloque try-except general para capturar cualquier error y retornar consistentemente
117
+ analysis_text = "Iniciando análisis..."
118
+ default_image = Image.new('RGB', (600, 400), color = 'white') # Imagen placeholder
119
+
120
+ if file_obj is None:
121
+ return default_image, "Error: Por favor, sube un archivo Excel."
122
+
123
+ try:
124
+ df = pd.read_excel(file_obj.name)
125
+ except Exception as e:
126
+ return default_image, f"Error al leer el archivo Excel: {e}\n{traceback.format_exc()}"
127
+
128
+ expected_cols = ['Tiempo', 'Biomasa', 'Sustrato', 'Producto']
129
+ for col in expected_cols:
130
+ if col not in df.columns:
131
+ return default_image, f"Error: La columna '{col}' no se encuentra en el archivo Excel."
132
+
133
+ time_data = df['Tiempo'].values
134
+ biomass_data_exp = df['Biomasa'].values
135
+ substrate_data_exp = df['Sustrato'].values
136
+ product_data_exp = df['Producto'].values
137
+
138
+ active_biomass_eqs = int(biomass_eq_count_ui)
139
+ active_substrate_eqs = int(substrate_eq_count_ui)
140
+ active_product_eqs = int(product_eq_count_ui)
141
+
142
+ all_eq_inputs = {
143
+ 'biomass': (
144
+ [biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui][:active_biomass_eqs],
145
+ [biomass_param1_ui, biomass_param2_ui, biomass_param3_ui][:active_biomass_eqs],
146
+ [biomass_bound1_ui, biomass_bound2_ui, biomass_bound3_ui][:active_biomass_eqs],
147
+ biomass_data_exp
148
+ ),
149
+ 'substrate': (
150
+ [substrate_eq1_ui, substrate_eq2_ui, substrate_eq3_ui][:active_substrate_eqs],
151
+ [substrate_param1_ui, substrate_param2_ui, substrate_param3_ui][:active_substrate_eqs],
152
+ [substrate_bound1_ui, substrate_bound2_ui, substrate_bound3_ui][:active_substrate_eqs],
153
+ substrate_data_exp
154
+ ),
155
+ 'product': (
156
+ [product_eq1_ui, product_eq2_ui, product_eq3_ui][:active_product_eqs],
157
+ [product_param1_ui, product_param2_ui, product_param3_ui][:active_product_eqs],
158
+ [product_bound1_ui, product_bound2_ui, product_bound3_ui][:active_product_eqs],
159
+ product_data_exp
160
+ )
161
+ }
162
+
163
+ model_handler = BioprocessModel()
164
+
165
+ fitted_results_for_plot = {'biomass': [], 'substrate': [], 'product': []}
166
+ results_for_llm_prompt = {'biomass': [], 'substrate': [], 'product': []}
167
+ biomass_params_for_s_p = None
168
+
169
+ for model_type, (eq_list, param_str_list, bound_str_list, exp_data) in all_eq_inputs.items():
170
+ if not np.any(exp_data) and len(exp_data) > 0: # Check if all data points are zero or NaN
171
+ print(f"Datos experimentales para {model_type} son todos cero o NaN, saltando ajuste.")
 
 
 
 
 
 
 
 
172
  continue
 
 
173
 
174
+ for i in range(len(eq_list)):
175
+ eq_str = eq_list[i]
176
+ param_s = param_str_list[i]
177
+ bound_s = bound_str_list[i]
178
+
179
+ if not eq_str or not param_s:
180
+ print(f"Ecuación o parámetros vacíos para {model_type} #{i+1}, saltando.")
181
+ continue
 
182
 
183
+ print(f"Procesando {model_type} #{i+1}: Eq='{eq_str}', Params='{param_s}'")
184
+
185
+ try:
186
+ model_handler.set_model(model_type, eq_str, param_s)
187
+ num_p = len(model_handler.models[model_type]['params'])
188
+ l_b, u_b = parse_bounds_str(bound_s, num_p)
189
+
190
+ current_biomass_params = biomass_params_for_s_p if model_type in ['substrate', 'product'] else None
191
+
192
+ y_pred, popt = model_handler.fit_model(model_type, time_data, exp_data, bounds=(l_b, u_b), biomass_params_fitted=current_biomass_params)
193
+
194
+ current_params = model_handler.params[model_type]
195
+ r2_val = model_handler.r2.get(model_type, float('nan'))
196
+ rmse_val = model_handler.rmse.get(model_type, float('nan'))
197
+
198
+ fitted_results_for_plot[model_type].append({
199
+ 'equation': eq_str,
200
+ 'y_pred': y_pred,
201
+ 'params': current_params,
202
+ 'R2': r2_val
203
+ })
204
+ results_for_llm_prompt[model_type].append({
205
+ 'equation': eq_str,
206
+ 'params_fitted': current_params,
207
+ 'R2': r2_val,
208
+ 'RMSE': rmse_val
209
+ })
210
+
211
+ if model_type == 'biomass' and biomass_params_for_s_p is None:
212
+ biomass_params_for_s_p = current_params
213
+ print(f"Parámetros de Biomasa (para S/P): {biomass_params_for_s_p}")
214
+
215
+ except Exception as e:
216
+ error_msg = f"Error ajustando {model_type} #{i+1} ('{eq_str}'): {e}\n{traceback.format_exc()}"
217
+ print(error_msg)
218
+ return default_image, error_msg
219
+
220
+ # Generar gráfico
221
+ fig, axs = plt.subplots(3, 1, figsize=(10, 18), sharex=True)
222
+ plot_config = {
223
+ axs[0]: (biomass_data_exp, 'Biomasa', fitted_results_for_plot['biomasa']),
224
+ axs[1]: (substrate_data_exp, 'Sustrato', fitted_results_for_plot['sustrato']),
225
+ axs[2]: (product_data_exp, 'Producto', fitted_results_for_plot['producto'])
226
+ }
227
+
228
+ for ax, data_actual, ylabel, plot_results_list in plot_config.items():
229
+ if np.any(data_actual): # Solo plotear si hay datos
230
+ ax.plot(time_data, data_actual, 'o', label=f'Datos {ylabel}', markersize=5, alpha=0.7)
231
+ else:
232
+ ax.text(0.5, 0.5, f"No hay datos para {ylabel}", transform=ax.transAxes, ha='center', va='center', fontsize=12, color='gray')
233
+
 
234
  for idx, res_detail in enumerate(plot_results_list):
235
+ label = f'Modelo {idx+1} (R²:{res_detail["R2"]:.3f})'
236
+ ax.plot(time_data, res_detail['y_pred'], '-', label=label, linewidth=2)
237
+ ax.set_xlabel('Tiempo')
238
+ ax.set_ylabel(ylabel)
239
+ ax.grid(True, linestyle=':', alpha=0.7)
240
+ if show_legend_ui:
241
+ ax.legend(loc=legend_position_ui, fontsize='small')
242
 
243
+ if show_params_ui and plot_results_list:
244
+ param_display_texts = []
245
+ for idx, res_detail in enumerate(plot_results_list):
246
+ params_text = f"Modelo {idx+1}:\n" + "\n".join([f" {k}: {v:.4g}" for k,v in res_detail['params'].items()])
247
+ param_display_texts.append(params_text)
248
+ full_param_text = "\n---\n".join(param_display_texts)
249
+
250
+ text_x_pos = 0.02
251
+ text_y_pos = 0.98
252
+ v_align = 'top'
253
+ if legend_position_ui and 'upper' in legend_position_ui:
254
+ text_y_pos = 0.02
255
+ v_align = 'bottom'
256
+
257
+ ax.text(text_x_pos, text_y_pos, full_param_text, transform=ax.transAxes, fontsize=7,
258
+ verticalalignment=v_align, bbox=dict(boxstyle='round,pad=0.3', fc='lightyellow', alpha=0.8))
259
+
260
+ plt.tight_layout(rect=[0, 0, 1, 0.96])
261
+ fig.suptitle("Resultados del Ajuste de Modelos Cinéticos", fontsize=16)
262
+
263
+ buf = io.BytesIO()
264
+ plt.savefig(buf, format='png', dpi=150)
265
+ buf.seek(0)
266
+ image = Image.open(buf)
267
+ plt.close(fig)
268
+
269
+ # Construir prompt para LLM y llamar al servicio
270
+ prompt_intro = "Eres un experto en modelado cinético de bioprocesos. Analiza los siguientes resultados del ajuste de modelos a datos experimentales:\n\n"
271
+ prompt_details = json.dumps(results_for_llm_prompt, indent=2, ensure_ascii=False)
272
+ prompt_instructions = """\n\nPor favor, proporciona un análisis detallado y crítico en español, estructurado de la siguiente manera:
273
  1. **Resumen General:** Una breve descripción del experimento y qué se intentó modelar.
274
  2. **Análisis por Componente (Biomasa, Sustrato, Producto):**
275
  a. Para cada ecuación probada:
 
283
  4. **Sugerencias y Próximos Pasos:**
284
  a. ¿Cómo se podría mejorar el modelado (ej. probar otras ecuaciones, transformar datos, revisar calidad de datos experimentales)?
285
  b. ¿Qué experimentos adicionales podrían realizarse para validar o refinar los modelos?
286
+ 5. **Conclusión Final:** Un veredicto general conciso sobre el éxito del modelado y la utilidad de los resultados obtenidos.
287
 
288
  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."""
289
+
290
+ full_prompt = prompt_intro + prompt_details + prompt_instructions
291
+
292
+ analysis_text = call_llm_analysis_service(full_prompt)
293
+
294
+ return image, analysis_text
295
 
296
+ except Exception as general_e:
297
+ # Captura cualquier excepción no manejada y la muestra en la UI
298
+ error_trace = traceback.format_exc()
299
+ error_message_full = f"Error inesperado en process_and_plot: {general_e}\n{error_trace}"
300
+ print(error_message_full)
301
+ return Image.new('RGB', (600, 400), color = 'red'), error_message_full # Retorna imagen roja de error