C2MV commited on
Commit
a622789
·
verified ·
1 Parent(s): aa570a7

Upload 2 files

Browse files
Files changed (2) hide show
  1. UI.py +42 -65
  2. interface.py +87 -135
UI.py CHANGED
@@ -1,21 +1,17 @@
1
  # UI.py
2
  import gradio as gr
3
- import numpy as np # Necesario para np.inf en los valores por defecto de los límites
4
 
5
  def create_interface(process_function_for_button):
6
  """
7
  Crea la interfaz de usuario completa para el modelado de bioprocesos.
8
- Conecta el botón de submit a la 'process_function_for_button' proporcionada.
9
  """
10
-
11
- # Usar un tema estándar de Gradio para mayor compatibilidad.
12
- # theme='gradio/soft' es una buena opción. Puedes cambiarlo si tienes un tema personalizado que funcione.
13
- with gr.Blocks(theme='gradio/soft') as demo:
14
  gr.Markdown("# Modelado de Bioprocesos con Ecuaciones Personalizadas y Análisis por IA")
15
  gr.Markdown(
16
- "Sube un archivo Excel con columnas 'Tiempo', 'Biomasa', 'Sustrato', 'Producto'. "
17
- "Ingresa tus propias ecuaciones (usando 't' para tiempo y 'X_val' para X(t) en ecuaciones de S/P), "
18
- "los nombres de sus parámetros, y sus límites. El sistema ajustará los modelos y un LLM analizará los resultados."
19
  )
20
 
21
  with gr.Row():
@@ -28,113 +24,99 @@ def create_interface(process_function_for_button):
28
  legend_position_ui = gr.Dropdown(
29
  label="Posición de la leyenda",
30
  choices=['best', 'upper right', 'upper left', 'lower right', 'lower left', 'center left', 'center right', 'lower center', 'upper center', 'center'],
31
- value='best', # String value, esto está bien
32
  type="value"
33
  )
34
  with gr.Column(scale=1):
35
  gr.Markdown("### 2. Conteo de Ecuaciones a Probar")
36
- gr.Markdown("Define cuántas ecuaciones diferentes quieres probar para cada componente (1 a 3).")
37
- # Usar floats para gr.Number, Gradio los maneja bien incluso con precision=0
38
  biomass_eq_count_ui = gr.Number(label="Ecuaciones de Biomasa:", value=1.0, minimum=1.0, maximum=3.0, step=1.0, precision=0)
39
  substrate_eq_count_ui = gr.Number(label="Ecuaciones de Sustrato:", value=1.0, minimum=1.0, maximum=3.0, step=1.0, precision=0)
40
  product_eq_count_ui = gr.Number(label="Ecuaciones de Producto:", value=1.0, minimum=1.0, maximum=3.0, step=1.0, precision=0)
41
 
42
- # --- Sección de Biomasa ---
 
43
  with gr.Accordion("3. Definición de Modelos de Biomasa", open=True):
44
- gr.Markdown("Ingresa la ecuación (ej: `Xm*(1 - exp(-um*(t - t_lag)))`), los nombres de los parámetros (ej: `Xm, um, t_lag`), y los límites de los parámetros (ej: `(0, np.inf), (0, 5), (0, 100)`). Usa `t` como la variable de tiempo.")
45
  with gr.Row():
46
- with gr.Column(): # Columna 1 para Biomasa (siempre visible)
47
- biomass_eq1_ui = gr.Textbox(label="Ecuación de Biomasa 1", value="Xm * (1 - exp(-um * (t - t_lag)))", lines=2)
48
  biomass_param1_ui = gr.Textbox(label="Parámetros Biomasa 1", value="Xm, um, t_lag")
49
  biomass_bound1_ui = gr.Textbox(label="Límites Biomasa 1", value="(0, np.inf), (0, np.inf), (0, np.inf)")
50
 
51
- # Columna 2 para Biomasa
52
  biomass_col2_container = gr.Column(visible=False)
53
  with biomass_col2_container:
54
- biomass_eq2_ui = gr.Textbox(label="Ecuación de Biomasa 2", value="X0 * exp(um * t)", lines=2) # Ejemplo: Crecimiento exponencial
55
- biomass_param2_ui = gr.Textbox(label="Parámetros Biomasa 2", value="X0, um")
56
- biomass_bound2_ui = gr.Textbox(label="Límites Biomasa 2", value="(0, np.inf), (0, np.inf)")
57
 
58
- # Columna 3 para Biomasa
59
  biomass_col3_container = gr.Column(visible=False)
60
  with biomass_col3_container:
61
- biomass_eq3_ui = gr.Textbox(label="Ecuación de Biomasa 3", lines=2, value="", placeholder="Ej: (X0 * Xm * exp(um * t)) / (Xm - X0 + X0 * exp(um * t))") # Logística
62
- biomass_param3_ui = gr.Textbox(label="Parámetros Biomasa 3", value="", placeholder="X0, Xm, um")
63
- biomass_bound3_ui = gr.Textbox(label="Límites Biomasa 3", value="", placeholder="(0,np.inf),(0,np.inf),(0,np.inf)")
64
 
65
- # --- Sección de Sustrato ---
66
  with gr.Accordion("4. Definición de Modelos de Sustrato", open=True):
67
- gr.Markdown("Para Sustrato/Producto, si tu ecuación depende de la concentración de biomasa X(t) calculada por un modelo de biomasa, usa `X_val` en tu ecuación. Ejemplo: `S0 - (X_val / YXS)`.")
68
  with gr.Row():
69
  with gr.Column():
70
- substrate_eq1_ui = gr.Textbox(label="Ecuación de Sustrato 1", value="S0 - (X_val / YXS) - mS * t", lines=2)
71
  substrate_param1_ui = gr.Textbox(label="Parámetros Sustrato 1", value="S0, YXS, mS")
72
- substrate_bound1_ui = gr.Textbox(label="Límites Sustrato 1", value="(0, np.inf), (1e-9, np.inf), (0, np.inf)") # YXS > 0
73
 
74
  substrate_col2_container = gr.Column(visible=False)
75
  with substrate_col2_container:
76
- substrate_eq2_ui = gr.Textbox(label="Ecuación de Sustrato 2", lines=2, value="")
77
- substrate_param2_ui = gr.Textbox(label="Parámetros Sustrato 2", value="")
78
- substrate_bound2_ui = gr.Textbox(label="Límites Sustrato 2", value="")
79
 
80
  substrate_col3_container = gr.Column(visible=False)
81
  with substrate_col3_container:
82
- substrate_eq3_ui = gr.Textbox(label="Ecuación de Sustrato 3", lines=2, value="")
83
- substrate_param3_ui = gr.Textbox(label="Parámetros Sustrato 3", value="")
84
- substrate_bound3_ui = gr.Textbox(label="Límites Sustrato 3", value="")
85
 
86
- # --- Sección de Producto ---
87
  with gr.Accordion("5. Definición de Modelos de Producto", open=True):
88
- gr.Markdown("Similar a Sustrato, usa `X_val` para la dependencia de la biomasa X(t).")
89
  with gr.Row():
90
  with gr.Column():
91
- product_eq1_ui = gr.Textbox(label="Ecuación de Producto 1", value="P0 + YPX * X_val + mP * t", lines=2)
92
  product_param1_ui = gr.Textbox(label="Parámetros Producto 1", value="P0, YPX, mP")
93
  product_bound1_ui = gr.Textbox(label="Límites Producto 1", value="(0, np.inf), (0, np.inf), (0, np.inf)")
94
 
95
  product_col2_container = gr.Column(visible=False)
96
  with product_col2_container:
97
- product_eq2_ui = gr.Textbox(label="Ecuación de Producto 2", lines=2, value="")
98
- product_param2_ui = gr.Textbox(label="Parámetros Producto 2", value="")
99
- product_bound2_ui = gr.Textbox(label="Límites Producto 2", value="")
100
 
101
  product_col3_container = gr.Column(visible=False)
102
  with product_col3_container:
103
- product_eq3_ui = gr.Textbox(label="Ecuación de Producto 3", lines=2, value="")
104
- product_param3_ui = gr.Textbox(label="Parámetros Producto 3", value="")
105
- product_bound3_ui = gr.Textbox(label="Límites Producto 3", value="")
106
 
107
- # Lógica para mostrar/ocultar campos de ecuación dinámicamente
108
  def update_eq_visibility(count_value):
109
  try:
110
- # El valor de gr.Number puede ser None si el usuario lo borra
111
- if count_value is None:
112
- count = 0 # O 1, dependiendo de cómo quieras manejarlo. 0 ocultará todo.
113
- else:
114
- count = int(float(count_value)) # Convertir a float primero, luego a int
115
  except ValueError:
116
- count = 0 # Si la conversión falla, asumir 0
117
-
118
- # gr.update es la forma canónica de actualizar propiedades
119
  return gr.update(visible=count >= 2), gr.update(visible=count >= 3)
120
 
121
- # Conectar eventos .change() de los contadores a la función de visibilidad
122
  biomass_eq_count_ui.change(fn=update_eq_visibility, inputs=biomass_eq_count_ui, outputs=[biomass_col2_container, biomass_col3_container])
123
  substrate_eq_count_ui.change(fn=update_eq_visibility, inputs=substrate_eq_count_ui, outputs=[substrate_col2_container, substrate_col3_container])
124
  product_eq_count_ui.change(fn=update_eq_visibility, inputs=product_eq_count_ui, outputs=[product_col2_container, product_col3_container])
125
 
126
- # Botón de envío
127
  submit_button = gr.Button("Procesar y Analizar Modelos", variant="primary", scale=1, elem_id="submit_button_main")
128
 
129
- # Salidas
130
  gr.Markdown("## Resultados del Análisis y Modelado")
131
  with gr.Row():
132
- # Para la imagen, type="pil" es bueno si tu función devuelve un objeto PIL.Image
133
- image_output = gr.Image(label="Gráfico Generado de Ajustes", type="pil", scale=2, show_download_button=True, height=750)
134
  with gr.Column(scale=3):
135
  analysis_output = gr.Markdown(label="Análisis del Modelo por IA")
136
 
137
- # Lista de todos los inputs para el botón de submit
138
  all_inputs_for_button = [
139
  file_input,
140
  biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
@@ -155,18 +137,14 @@ def create_interface(process_function_for_button):
155
  ]
156
  outputs_for_button = [image_output, analysis_output]
157
 
158
- # Conexión del botón DENTRO del contexto de Blocks
159
  submit_button.click(
160
- fn=process_function_for_button, # Usa la función pasada como argumento
161
  inputs=all_inputs_for_button,
162
  outputs=outputs_for_button,
163
- # api_name="process_data" # Opcional, para nombrar el endpoint de la API
164
  )
165
 
166
- # Inicializar visibilidad usando demo.load para que se aplique al cargar la UI
167
  def set_initial_visibility_on_load_wrapper(b_c_val, s_c_val, p_c_val):
168
- # Obtener los valores iniciales de los gr.Number components y aplicar la lógica de visibilidad.
169
- # Los valores de los Number inputs pueden ser float, convertirlos a int.
170
  b_c_int = int(float(b_c_val)) if b_c_val is not None else 0
171
  s_c_int = int(float(s_c_val)) if s_c_val is not None else 0
172
  p_c_int = int(float(p_c_val)) if p_c_val is not None else 0
@@ -175,7 +153,6 @@ def create_interface(process_function_for_button):
175
  s_vis2_upd, s_vis3_upd = update_eq_visibility(s_c_int)
176
  p_vis2_upd, p_vis3_upd = update_eq_visibility(p_c_int)
177
 
178
- # Devolver los resultados de gr.update para cada componente de salida del demo.load
179
  return b_vis2_upd, b_vis3_upd, s_vis2_upd, s_vis3_upd, p_vis2_upd, p_vis3_upd
180
 
181
  demo.load(
 
1
  # UI.py
2
  import gradio as gr
3
+ import numpy as np # Necesario para np.inf
4
 
5
  def create_interface(process_function_for_button):
6
  """
7
  Crea la interfaz de usuario completa para el modelado de bioprocesos.
 
8
  """
9
+ with gr.Blocks(theme='gradio/soft') as demo:
 
 
 
10
  gr.Markdown("# Modelado de Bioprocesos con Ecuaciones Personalizadas y Análisis por IA")
11
  gr.Markdown(
12
+ "Sube un archivo Excel (columnas: 'Tiempo', 'Biomasa', 'Sustrato', 'Producto'). "
13
+ "Ingresa ecuaciones (usa 't' para tiempo; 'X_val' para X(t) en S/P), "
14
+ "parámetros y límites. El sistema ajustará los modelos y un LLM analizará los resultados."
15
  )
16
 
17
  with gr.Row():
 
24
  legend_position_ui = gr.Dropdown(
25
  label="Posición de la leyenda",
26
  choices=['best', 'upper right', 'upper left', 'lower right', 'lower left', 'center left', 'center right', 'lower center', 'upper center', 'center'],
27
+ value='best',
28
  type="value"
29
  )
30
  with gr.Column(scale=1):
31
  gr.Markdown("### 2. Conteo de Ecuaciones a Probar")
32
+ gr.Markdown("Define cuántas ecuaciones diferentes probar para cada componente (1-3).")
 
33
  biomass_eq_count_ui = gr.Number(label="Ecuaciones de Biomasa:", value=1.0, minimum=1.0, maximum=3.0, step=1.0, precision=0)
34
  substrate_eq_count_ui = gr.Number(label="Ecuaciones de Sustrato:", value=1.0, minimum=1.0, maximum=3.0, step=1.0, precision=0)
35
  product_eq_count_ui = gr.Number(label="Ecuaciones de Producto:", value=1.0, minimum=1.0, maximum=3.0, step=1.0, precision=0)
36
 
37
+ # --- Contenedores para campos dinámicos ---
38
+ # Biomasa
39
  with gr.Accordion("3. Definición de Modelos de Biomasa", open=True):
40
+ gr.Markdown("Ecuación (ej: `Xm*(1 - exp(-um*(t - t_lag)))`), Parámetros (ej: `Xm, um, t_lag`), Límites (ej: `(0, np.inf), (0, 5), (0, 100)`).")
41
  with gr.Row():
42
+ with gr.Column():
43
+ biomass_eq1_ui = gr.Textbox(label="Ecuación Biomasa 1", value="Xm * (1 - exp(-um * (t - t_lag)))", lines=2)
44
  biomass_param1_ui = gr.Textbox(label="Parámetros Biomasa 1", value="Xm, um, t_lag")
45
  biomass_bound1_ui = gr.Textbox(label="Límites Biomasa 1", value="(0, np.inf), (0, np.inf), (0, np.inf)")
46
 
 
47
  biomass_col2_container = gr.Column(visible=False)
48
  with biomass_col2_container:
49
+ biomass_eq2_ui = gr.Textbox(label="Ecuación Biomasa 2", value="", lines=2, placeholder="Opcional: X0 * exp(um * t)")
50
+ biomass_param2_ui = gr.Textbox(label="Parámetros Biomasa 2", value="", placeholder="Opcional: X0, um")
51
+ biomass_bound2_ui = gr.Textbox(label="Límites Biomasa 2", value="", placeholder="Opcional: (0, np.inf), (0, np.inf)")
52
 
 
53
  biomass_col3_container = gr.Column(visible=False)
54
  with biomass_col3_container:
55
+ biomass_eq3_ui = gr.Textbox(label="Ecuación Biomasa 3", lines=2, value="", placeholder="Opcional")
56
+ biomass_param3_ui = gr.Textbox(label="Parámetros Biomasa 3", value="", placeholder="Opcional")
57
+ biomass_bound3_ui = gr.Textbox(label="Límites Biomasa 3", value="", placeholder="Opcional")
58
 
59
+ # Sustrato
60
  with gr.Accordion("4. Definición de Modelos de Sustrato", open=True):
61
+ gr.Markdown("Usa `X_val` para X(t) si es necesario. Ej: `S0 - (X_val / YXS)`.")
62
  with gr.Row():
63
  with gr.Column():
64
+ substrate_eq1_ui = gr.Textbox(label="Ecuación Sustrato 1", value="S0 - (X_val / YXS) - mS * t", lines=2)
65
  substrate_param1_ui = gr.Textbox(label="Parámetros Sustrato 1", value="S0, YXS, mS")
66
+ substrate_bound1_ui = gr.Textbox(label="Límites Sustrato 1", value="(0, np.inf), (1e-9, np.inf), (0, np.inf)")
67
 
68
  substrate_col2_container = gr.Column(visible=False)
69
  with substrate_col2_container:
70
+ substrate_eq2_ui = gr.Textbox(label="Ecuación Sustrato 2", lines=2, value="", placeholder="Opcional")
71
+ substrate_param2_ui = gr.Textbox(label="Parámetros Sustrato 2", value="", placeholder="Opcional")
72
+ substrate_bound2_ui = gr.Textbox(label="Límites Sustrato 2", value="", placeholder="Opcional")
73
 
74
  substrate_col3_container = gr.Column(visible=False)
75
  with substrate_col3_container:
76
+ substrate_eq3_ui = gr.Textbox(label="Ecuación Sustrato 3", lines=2, value="", placeholder="Opcional")
77
+ substrate_param3_ui = gr.Textbox(label="Parámetros Sustrato 3", value="", placeholder="Opcional")
78
+ substrate_bound3_ui = gr.Textbox(label="Límites Sustrato 3", value="", placeholder="Opcional")
79
 
80
+ # Producto
81
  with gr.Accordion("5. Definición de Modelos de Producto", open=True):
82
+ gr.Markdown("Usa `X_val` para X(t) si es necesario. Ej: `P0 + YPX * X_val`.")
83
  with gr.Row():
84
  with gr.Column():
85
+ product_eq1_ui = gr.Textbox(label="Ecuación Producto 1", value="P0 + YPX * X_val + mP * t", lines=2)
86
  product_param1_ui = gr.Textbox(label="Parámetros Producto 1", value="P0, YPX, mP")
87
  product_bound1_ui = gr.Textbox(label="Límites Producto 1", value="(0, np.inf), (0, np.inf), (0, np.inf)")
88
 
89
  product_col2_container = gr.Column(visible=False)
90
  with product_col2_container:
91
+ product_eq2_ui = gr.Textbox(label="Ecuación Producto 2", lines=2, value="", placeholder="Opcional")
92
+ product_param2_ui = gr.Textbox(label="Parámetros Producto 2", value="", placeholder="Opcional")
93
+ product_bound2_ui = gr.Textbox(label="Límites Producto 2", value="", placeholder="Opcional")
94
 
95
  product_col3_container = gr.Column(visible=False)
96
  with product_col3_container:
97
+ product_eq3_ui = gr.Textbox(label="Ecuación Producto 3", lines=2, value="", placeholder="Opcional")
98
+ product_param3_ui = gr.Textbox(label="Parámetros Producto 3", value="", placeholder="Opcional")
99
+ product_bound3_ui = gr.Textbox(label="Límites Producto 3", value="", placeholder="Opcional")
100
 
 
101
  def update_eq_visibility(count_value):
102
  try:
103
+ count = int(float(count_value)) if count_value is not None else 0
 
 
 
 
104
  except ValueError:
105
+ count = 0
 
 
106
  return gr.update(visible=count >= 2), gr.update(visible=count >= 3)
107
 
 
108
  biomass_eq_count_ui.change(fn=update_eq_visibility, inputs=biomass_eq_count_ui, outputs=[biomass_col2_container, biomass_col3_container])
109
  substrate_eq_count_ui.change(fn=update_eq_visibility, inputs=substrate_eq_count_ui, outputs=[substrate_col2_container, substrate_col3_container])
110
  product_eq_count_ui.change(fn=update_eq_visibility, inputs=product_eq_count_ui, outputs=[product_col2_container, product_col3_container])
111
 
 
112
  submit_button = gr.Button("Procesar y Analizar Modelos", variant="primary", scale=1, elem_id="submit_button_main")
113
 
 
114
  gr.Markdown("## Resultados del Análisis y Modelado")
115
  with gr.Row():
116
+ image_output = gr.Image(label="Gráfico Generado de Ajustes", type="pil", scale=2, show_download_button=True, height=750, interactive=False)
 
117
  with gr.Column(scale=3):
118
  analysis_output = gr.Markdown(label="Análisis del Modelo por IA")
119
 
 
120
  all_inputs_for_button = [
121
  file_input,
122
  biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
 
137
  ]
138
  outputs_for_button = [image_output, analysis_output]
139
 
 
140
  submit_button.click(
141
+ fn=process_function_for_button,
142
  inputs=all_inputs_for_button,
143
  outputs=outputs_for_button,
144
+ # api_name="process_data" # Descomentar si necesitas un nombre de API específico
145
  )
146
 
 
147
  def set_initial_visibility_on_load_wrapper(b_c_val, s_c_val, p_c_val):
 
 
148
  b_c_int = int(float(b_c_val)) if b_c_val is not None else 0
149
  s_c_int = int(float(s_c_val)) if s_c_val is not None else 0
150
  p_c_int = int(float(p_c_val)) if p_c_val is not None else 0
 
153
  s_vis2_upd, s_vis3_upd = update_eq_visibility(s_c_int)
154
  p_vis2_upd, p_vis3_upd = update_eq_visibility(p_c_int)
155
 
 
156
  return b_vis2_upd, b_vis3_upd, s_vis2_upd, s_vis3_upd, p_vis2_upd, p_vis3_upd
157
 
158
  demo.load(
interface.py CHANGED
@@ -2,72 +2,78 @@
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
-
35
  if len(parsed_bounds_list) != num_params:
36
- 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}).")
37
 
38
- lower_bounds = []
39
- upper_bounds = []
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
-
51
  return lower_bounds, upper_bounds
52
  except Exception as e:
53
- print(f"Error al parsear los límites '{bounds_str_input}': {e}. Usando límites por defecto (-inf, inf).")
54
  return [-np.inf] * num_params, [np.inf] * num_params
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)
@@ -80,8 +86,7 @@ def call_llm_analysis_service(prompt: str) -> str:
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,
85
  eos_token_id=tokenizer_local.eos_token_id,
86
  pad_token_id=tokenizer_local.pad_token_id if tokenizer_local.pad_token_id else tokenizer_local.eos_token_id,
87
  do_sample=True, temperature=0.6, top_p=0.9
@@ -113,31 +118,37 @@ def process_and_plot(
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': (
@@ -167,135 +178,76 @@ def process_and_plot(
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:
276
- 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.
277
- 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).
278
- iii. Ecuación Específica: Menciona la ecuación usada.
279
- 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é?
280
- 3. **Problemas y Limitaciones:**
281
- a. ¿Hay problemas evidentes (ej. R² muy bajo, parámetros físicamente no realistas, sobreajuste si se puede inferir, etc.)?
282
- b. ¿Qué limitaciones podrían tener los modelos o el proceso de ajuste?
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
 
2
  import numpy as np
3
  import pandas as pd
4
  import matplotlib
5
+ matplotlib.use('Agg') # Backend no interactivo
6
  import matplotlib.pyplot as plt
7
  from PIL import Image
8
  import io
9
  import json
10
+ import traceback # Para traceback detallado
11
 
12
+ # Importar BioprocessModel de TU models.py (el que usa sympy)
13
+ from models import BioprocessModel
14
+ # from decorators import gpu_decorator # No es necesario con Modal
15
 
16
+ # Variables globales que serán "inyectadas"
17
  USE_MODAL_FOR_LLM_ANALYSIS = False
18
  generate_analysis_from_modal = None
19
 
20
+ def create_error_image(message="Error", width=600, height=400):
21
+ """Crea una imagen PIL simple para mostrar mensajes de error."""
22
+ img = Image.new('RGB', (width, height), color = (255, 200, 200)) # Fondo rojo claro
23
+ # No podemos dibujar texto fácilmente sin Pillow-SIMD o dependencias de dibujo complejas.
24
+ # Una imagen simple es suficiente para indicar un error.
25
+ # from PIL import ImageDraw
26
+ # d = ImageDraw.Draw(img)
27
+ # d.text((10,10), message, fill=(0,0,0)) # Esto requeriría una fuente
28
+ print(f"Generando imagen de error: {message}")
29
+ return img
30
+
31
  def parse_bounds_str(bounds_str_input, num_params):
32
  bounds_str = str(bounds_str_input).strip()
33
  if not bounds_str:
34
  print(f"Cadena de límites vacía para {num_params} params. Usando (-inf, inf).")
35
  return [-np.inf] * num_params, [np.inf] * num_params
 
36
  try:
37
  bounds_str = bounds_str.lower().replace('inf', 'np.inf').replace('none', 'None')
38
+ if not (bounds_str.startswith('[') and bounds_str.endswith(']')):
39
  bounds_str = f"[{bounds_str}]"
40
+ parsed_bounds_list = eval(bounds_str, {'np': np, 'inf': np.inf, 'None': None})
 
41
 
42
  if not isinstance(parsed_bounds_list, list):
43
+ raise ValueError("Cadena de límites no evaluó a una lista.")
 
44
  if len(parsed_bounds_list) != num_params:
45
+ raise ValueError(f"Num límites ({len(parsed_bounds_list)}) != num params ({num_params}).")
46
 
47
+ lower_bounds, upper_bounds = [], []
 
48
  for item in parsed_bounds_list:
49
  if not (isinstance(item, (tuple, list)) and len(item) == 2):
50
+ raise ValueError(f"Límite debe ser (low, high). Se encontró: {item}")
 
 
51
  low = -np.inf if (item[0] is None or (isinstance(item[0], float) and np.isnan(item[0]))) else float(item[0])
52
  high = np.inf if (item[1] is None or (isinstance(item[1], float) and np.isnan(item[1]))) else float(item[1])
53
+ lower_bounds.append(low); upper_bounds.append(high)
 
 
 
54
  return lower_bounds, upper_bounds
55
  except Exception as e:
56
+ print(f"Error al parsear límites '{bounds_str_input}': {e}. Usando por defecto (-inf, inf).")
57
  return [-np.inf] * num_params, [np.inf] * num_params
58
 
 
59
  def call_llm_analysis_service(prompt: str) -> str:
60
+ """Llama al servicio LLM (ya sea localmente o a través de Modal)."""
61
+ # ... (sin cambios respecto a la versión anterior completa)
62
  if USE_MODAL_FOR_LLM_ANALYSIS and generate_analysis_from_modal:
63
  print("interface.py: Usando la función de análisis LLM de Modal...")
64
  try:
65
  return generate_analysis_from_modal(prompt)
66
  except Exception as e_modal_call:
67
  print(f"Error llamando a la función Modal LLM: {e_modal_call}")
68
+ traceback.print_exc()
69
  return f"Error al contactar el servicio de análisis IA (Modal): {e_modal_call}"
70
  else:
71
  print("interface.py: Usando la función de análisis LLM local (fallback)...")
72
+ # Implementación de fallback local (como en la respuesta anterior)
73
  try:
74
+ from config import MODEL_PATH, MAX_LENGTH, DEVICE
75
+ from transformers import AutoTokenizer, AutoModelForCausalLM
76
+ import torch # Asegurar importación de torch para fallback
77
 
78
  print(f"Fallback: Cargando modelo {MODEL_PATH} localmente en {DEVICE}...")
79
  tokenizer_local = AutoTokenizer.from_pretrained(MODEL_PATH)
 
86
  inputs = tokenizer_local(prompt, return_tensors="pt", truncation=True, max_length=max_prompt_len).to(DEVICE)
87
  with torch.no_grad():
88
  outputs = model_local.generate(
89
+ **inputs, max_new_tokens=MAX_LENGTH,
 
90
  eos_token_id=tokenizer_local.eos_token_id,
91
  pad_token_id=tokenizer_local.pad_token_id if tokenizer_local.pad_token_id else tokenizer_local.eos_token_id,
92
  do_sample=True, temperature=0.6, top_p=0.9
 
118
  substrate_eq_count_ui,
119
  product_eq_count_ui
120
  ):
121
+ # Imagen y texto de error por defecto
122
+ error_img = create_error_image("Error en procesamiento")
123
+ error_analysis_text = "No se pudo generar el análisis debido a un error."
124
+
125
+ try:
126
  if file_obj is None:
127
+ return error_img, "Error: Por favor, sube un archivo Excel."
128
 
129
  try:
130
  df = pd.read_excel(file_obj.name)
131
  except Exception as e:
132
+ return error_img, 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 error_img, 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
+ # Asegurar que los contadores sean enteros válidos
145
+ try:
146
+ active_biomass_eqs = int(float(biomass_eq_count_ui))
147
+ active_substrate_eqs = int(float(substrate_eq_count_ui))
148
+ active_product_eqs = int(float(product_eq_count_ui))
149
+ except (TypeError, ValueError):
150
+ return error_img, "Error: Número de ecuaciones inválido."
151
+
152
 
153
  all_eq_inputs = {
154
  'biomass': (
 
178
  biomass_params_for_s_p = None
179
 
180
  for model_type, (eq_list, param_str_list, bound_str_list, exp_data) in all_eq_inputs.items():
181
+ if not (isinstance(exp_data, np.ndarray) and exp_data.size > 0 and np.any(np.isfinite(exp_data))):
182
+ print(f"Datos experimentales para {model_type} no válidos o vacíos, saltando ajuste.")
183
  continue
184
 
185
  for i in range(len(eq_list)):
186
+ eq_str, param_s, bound_s = eq_list[i], param_str_list[i], bound_str_list[i]
187
+ if not eq_str or not param_s: continue
 
 
 
 
 
188
 
 
 
189
  try:
190
  model_handler.set_model(model_type, eq_str, param_s)
191
  num_p = len(model_handler.models[model_type]['params'])
192
  l_b, u_b = parse_bounds_str(bound_s, num_p)
193
+ current_biomass_p = biomass_params_for_s_p if model_type in ['substrate', 'product'] else None
194
 
195
+ y_pred, popt = model_handler.fit_model(model_type, time_data, exp_data, bounds=(l_b, u_b), biomass_params_fitted=current_biomass_p)
 
 
196
 
197
+ current_params = model_handler.params.get(model_type, {}) # Obtener params del handler
198
  r2_val = model_handler.r2.get(model_type, float('nan'))
199
  rmse_val = model_handler.rmse.get(model_type, float('nan'))
200
 
201
+ fitted_results_for_plot[model_type].append({'equation': eq_str, 'y_pred': y_pred, 'params': current_params, 'R2': r2_val})
202
+ results_for_llm_prompt[model_type].append({'equation': eq_str, 'params_fitted': current_params, 'R2': r2_val, 'RMSE': rmse_val})
 
 
 
 
 
 
 
 
 
 
203
 
204
+ if model_type == 'biomass' and biomass_params_for_s_p is None and current_params:
205
  biomass_params_for_s_p = current_params
206
+ except Exception as e_fit:
207
+ error_msg = f"Error ajustando {model_type} #{i+1} ('{eq_str}'): {e_fit}\n{traceback.format_exc()}"
208
+ print(error_msg); return error_img, error_msg
 
 
 
209
 
210
  # Generar gráfico
211
  fig, axs = plt.subplots(3, 1, figsize=(10, 18), sharex=True)
212
+ plot_config_map = {
213
+ axs[0]: (biomass_data_exp, 'Biomasa', fitted_results_for_plot['biomass']),
214
  axs[1]: (substrate_data_exp, 'Sustrato', fitted_results_for_plot['sustrato']),
215
  axs[2]: (product_data_exp, 'Producto', fitted_results_for_plot['producto'])
216
  }
217
 
218
+ for ax, data_actual, ylabel, plot_results in plot_config_map.items():
219
+ if isinstance(data_actual, np.ndarray) and data_actual.size > 0 and np.any(np.isfinite(data_actual)):
220
  ax.plot(time_data, data_actual, 'o', label=f'Datos {ylabel}', markersize=5, alpha=0.7)
221
  else:
222
+ ax.text(0.5, 0.5, f"No hay datos para {ylabel}", transform=ax.transAxes, ha='center', va='center')
223
 
224
+ for idx, res_detail in enumerate(plot_results):
225
+ label = f'Modelo {idx+1} (R²:{res_detail.get("R2", float("nan")):.3f})'
226
  ax.plot(time_data, res_detail['y_pred'], '-', label=label, linewidth=2)
227
+ ax.set_xlabel('Tiempo'); ax.set_ylabel(ylabel); ax.grid(True, linestyle=':', alpha=0.7)
228
+ if show_legend_ui: ax.legend(loc=legend_position_ui, fontsize='small')
 
 
 
229
 
230
+ if show_params_ui and plot_results:
231
+ param_display_texts = [f"Modelo {idx+1}:\n" + "\n".join([f" {k}: {v:.4g}" for k,v in res_detail.get('params',{}).items()]) for idx, res_detail in enumerate(plot_results)]
232
+ ax.text(0.02, 0.98 if not ('upper' in legend_position_ui) else 0.02, "\n---\n".join(param_display_texts),
233
+ transform=ax.transAxes, fontsize=7, verticalalignment='top' if not ('upper' in legend_position_ui) else 'bottom',
234
+ bbox=dict(boxstyle='round,pad=0.3', fc='lightyellow', alpha=0.8))
235
+
236
+ plt.tight_layout(rect=[0, 0, 1, 0.96]); fig.suptitle("Resultados del Ajuste de Modelos Cinéticos", fontsize=16)
237
+ buf = io.BytesIO(); plt.savefig(buf, format='png', dpi=150); buf.seek(0)
238
+ image_pil = Image.open(buf); plt.close(fig)
239
+
240
+ # Construir prompt y llamar a LLM
241
+ prompt_intro = "Eres un experto en modelado cinético de bioprocesos...\n\n" # (como antes)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  prompt_details = json.dumps(results_for_llm_prompt, indent=2, ensure_ascii=False)
243
+ prompt_instructions = "\n\nPor favor, proporciona un análisis detallado...\n" # (como antes)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  full_prompt = prompt_intro + prompt_details + prompt_instructions
245
+ analysis_text_llm = call_llm_analysis_service(full_prompt)
 
246
 
247
+ return image_pil, analysis_text_llm
248
 
249
  except Exception as general_e:
 
250
  error_trace = traceback.format_exc()
251
  error_message_full = f"Error inesperado en process_and_plot: {general_e}\n{error_trace}"
252
  print(error_message_full)
253
+ return create_error_image(f"Error: {general_e}"), error_message_full