BioRAG / interface.py
C2MV's picture
Upload 8 files
d3d8124 verified
raw
history blame
16.1 kB
# interface.py
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg') # Backend no interactivo para matplotlib en Modal/servidores
import matplotlib.pyplot as plt
from PIL import Image
import io
import json
import traceback # Para imprimir traceback detallado de errores
# Importar BioprocessModel de TU models.py (el que usa sympy)
from models import BioprocessModel
# from decorators import gpu_decorator # El decorador @gpu es de HF Spaces, Modal lo maneja diferente
# Variables globales que serán "inyectadas" por modal_app.py
USE_MODAL_FOR_LLM_ANALYSIS = False
generate_analysis_from_modal = None # Placeholder para la función remota de Modal
def parse_bounds_str(bounds_str_input, num_params):
"""Parsea una cadena de límites y devuelve listas para lower y upper bounds."""
# Usar una copia para evitar modificar la entrada original si es mutable
bounds_str = str(bounds_str_input)
# Manejar el caso de cadena vacía o solo espacios en blanco
if not bounds_str.strip():
print(f"Cadena de límites vacía para {num_params} params. Usando límites (-inf, inf).")
return [-np.inf] * num_params, [np.inf] * num_params
try:
# Reemplazar 'inf' con 'np.inf' para que eval funcione correctamente con NumPy
# Hacerlo insensible a mayúsculas/minúsculas para 'inf'
bounds_str = bounds_str.lower().replace('inf', 'np.inf')
# Evaluar la cadena para convertirla en una lista de tuplas o listas
# Ejemplo de entrada esperada: " (0, np.inf), (0,10), (np.nan, np.nan) "
# Asegurar que esté encerrado en corchetes para que eval produzca una lista
if not bounds_str.startswith('['):
bounds_str = f"[{bounds_str}]"
parsed_bounds_list = eval(bounds_str)
if not isinstance(parsed_bounds_list, list):
raise ValueError("La cadena de límites no evaluó a una lista.")
if len(parsed_bounds_list) != num_params:
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}).")
lower_bounds = []
upper_bounds = []
for item in parsed_bounds_list:
if not (isinstance(item, (tuple, list)) and len(item) == 2):
raise ValueError(f"Cada límite debe ser una tupla/lista de dos elementos (low, high). Se encontró: {item}")
# np.nan puede ser usado para indicar sin límite, curve_fit lo trata como -inf/inf
low = -np.inf if (item[0] is None or (isinstance(item[0], float) and np.isnan(item[0]))) else float(item[0])
high = np.inf if (item[1] is None or (isinstance(item[1], float) and np.isnan(item[1]))) else float(item[1])
lower_bounds.append(low)
upper_bounds.append(high)
return lower_bounds, upper_bounds
except Exception as e:
print(f"Error al parsear los límites '{bounds_str_input}': {e}. Usando límites por defecto (-inf, inf).")
return [-np.inf] * num_params, [np.inf] * num_params
def call_llm_analysis_service(prompt: str) -> str:
"""Llama al servicio LLM (ya sea localmente o a través de Modal)."""
if USE_MODAL_FOR_LLM_ANALYSIS and generate_analysis_from_modal:
print("interface.py: Usando la función de análisis LLM de Modal...")
try:
# La función wrapper en modal_app.py obtiene MODEL_PATH y MAX_LENGTH de config.py
return generate_analysis_from_modal(prompt)
except Exception as e_modal_call:
print(f"Error llamando a la función Modal LLM: {e_modal_call}")
return f"Error al contactar el servicio de análisis IA (Modal): {e_modal_call}"
else:
# --- Implementación de Fallback (o si no se usa Modal) ---
print("interface.py: Usando la función de análisis LLM local (fallback)...")
try:
# Esta parte necesitaría que cargues el modelo localmente
# como lo hacías en tu versión original de interface.py
from config import MODEL_PATH, MAX_LENGTH, DEVICE # Importar configuración local
from transformers import AutoTokenizer, AutoModelForCausalLM # Importaciones locales
print(f"Fallback: Cargando modelo {MODEL_PATH} localmente en {DEVICE}...")
tokenizer_local = AutoTokenizer.from_pretrained(MODEL_PATH)
model_local = AutoModelForCausalLM.from_pretrained(MODEL_PATH).to(DEVICE) # No .eval() aquí
inputs = tokenizer_local(prompt, return_tensors="pt").to(DEVICE)
with torch.no_grad(): # Importante para inferencia
outputs = model_local.generate(
**inputs,
max_new_tokens=MAX_LENGTH,
eos_token_id=tokenizer_local.eos_token_id,
pad_token_id=tokenizer_local.pad_token_id if tokenizer_local.pad_token_id else tokenizer_local.eos_token_id,
do_sample=True, temperature=0.6, top_p=0.9
)
input_len = inputs.input_ids.shape[1]
analysis = tokenizer_local.decode(outputs[0][input_len:], skip_special_tokens=True)
return analysis.strip()
except Exception as e_local_llm:
print(f"Error en el fallback LLM local: {e_local_llm}")
return f"Análisis (fallback local): Error al cargar/ejecutar modelo LLM local: {e_local_llm}."
# @gpu_decorator(duration=100) # Este decorador es de HF Spaces. Modal gestiona GPU diferente.
def process_and_plot(
file_obj, # Gradio pasa un objeto File
# Entradas de la UI (desempaquetadas)
biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui,
biomass_param1_ui, biomass_param2_ui, biomass_param3_ui,
biomass_bound1_ui, biomass_bound2_ui, biomass_bound3_ui,
substrate_eq1_ui, substrate_eq2_ui, substrate_eq3_ui,
substrate_param1_ui, substrate_param2_ui, substrate_param3_ui,
substrate_bound1_ui, substrate_bound2_ui, substrate_bound3_ui,
product_eq1_ui, product_eq2_ui, product_eq3_ui,
product_param1_ui, product_param2_ui, product_param3_ui,
product_bound1_ui, product_bound2_ui, product_bound3_ui,
legend_position_ui,
show_legend_ui,
show_params_ui,
biomass_eq_count_ui,
substrate_eq_count_ui,
product_eq_count_ui
):
analysis_text = "Iniciando análisis..."
if file_obj is None:
return None, "Error: Por favor, sube un archivo Excel."
try:
df = pd.read_excel(file_obj.name) # .name para obtener el path del archivo temporal de Gradio
except Exception as e:
return None, f"Error al leer el archivo Excel: {e}\n{traceback.format_exc()}"
expected_cols = ['Tiempo', 'Biomasa', 'Sustrato', 'Producto']
for col in expected_cols:
if col not in df.columns:
return None, f"Error: La columna '{col}' no se encuentra en el archivo Excel."
time_data = df['Tiempo'].values
biomass_data_exp = df['Biomasa'].values
substrate_data_exp = df['Sustrato'].values
product_data_exp = df['Producto'].values
# Convertir contadores a enteros
active_biomass_eqs = int(biomass_eq_count_ui)
active_substrate_eqs = int(substrate_eq_count_ui)
active_product_eqs = int(product_eq_count_ui)
# Agrupar entradas de la UI
all_eq_inputs = {
'biomass': (
[biomass_eq1_ui, biomass_eq2_ui, biomass_eq3_ui][:active_biomass_eqs],
[biomass_param1_ui, biomass_param2_ui, biomass_param3_ui][:active_biomass_eqs],
[biomass_bound1_ui, biomass_bound2_ui, biomass_bound3_ui][:active_biomass_eqs],
biomass_data_exp
),
'substrate': (
[substrate_eq1_ui, substrate_eq2_ui, substrate_eq3_ui][:active_substrate_eqs],
[substrate_param1_ui, substrate_param2_ui, substrate_param3_ui][:active_substrate_eqs],
[substrate_bound1_ui, substrate_bound2_ui, substrate_bound3_ui][:active_substrate_eqs],
substrate_data_exp
),
'product': (
[product_eq1_ui, product_eq2_ui, product_eq3_ui][:active_product_eqs],
[product_param1_ui, product_param2_ui, product_param3_ui][:active_product_eqs],
[product_bound1_ui, product_bound2_ui, product_bound3_ui][:active_product_eqs],
product_data_exp
)
}
model_handler = BioprocessModel() # De TU models.py (el que usa sympy)
fitted_results_for_plot = {'biomass': [], 'substrate': [], 'product': []}
results_for_llm_prompt = {'biomass': [], 'substrate': [], 'product': []}
biomass_params_for_s_p = None # Para almacenar parámetros de biomasa ajustados
for model_type, (eq_list, param_str_list, bound_str_list, exp_data) in all_eq_inputs.items():
if not exp_data.any(): # Si no hay datos experimentales para este componente, saltar
print(f"No hay datos experimentales para {model_type}, saltando ajuste.")
continue
for i in range(len(eq_list)):
eq_str = eq_list[i]
param_s = param_str_list[i]
bound_s = bound_str_list[i]
if not eq_str or not param_s:
print(f"Ecuación o parámetros vacíos para {model_type} #{i+1}, saltando.")
continue
print(f"Procesando {model_type} #{i+1}: Eq='{eq_str}', Params='{param_s}'")
try:
model_handler.set_model(model_type, eq_str, param_s)
num_p = len(model_handler.models[model_type]['params'])
l_b, u_b = parse_bounds_str(bound_s, num_p)
# Pasar biomass_params_fitted si es sustrato o producto
current_biomass_params = biomass_params_for_s_p if model_type in ['substrate', 'product'] else None
y_pred, popt = model_handler.fit_model(model_type, time_data, exp_data, bounds=(l_b, u_b), biomass_params_fitted=current_biomass_params)
# Guardar resultados
current_params = model_handler.params[model_type]
r2_val = model_handler.r2.get(model_type, float('nan'))
rmse_val = model_handler.rmse.get(model_type, float('nan'))
fitted_results_for_plot[model_type].append({
'equation': eq_str,
'y_pred': y_pred,
'params': current_params,
'R2': r2_val
})
results_for_llm_prompt[model_type].append({
'equation': eq_str,
'params_fitted': current_params,
'R2': r2_val,
'RMSE': rmse_val
})
# Si es el primer modelo de biomasa ajustado con éxito, guardar sus parámetros
if model_type == 'biomass' and biomass_params_for_s_p is None:
biomass_params_for_s_p = current_params
print(f"Parámetros de Biomasa (para S/P): {biomass_params_for_s_p}")
except Exception as e:
error_msg = f"Error ajustando {model_type} #{i+1} ('{eq_str}'): {e}\n{traceback.format_exc()}"
print(error_msg)
# Devolver error a la UI en lugar de solo None
return None, error_msg
# Generar gráfico
fig, axs = plt.subplots(3, 1, figsize=(10, 18), sharex=True) # Aumentar altura
plot_config = {
axs[0]: (biomass_data_exp, 'Biomasa', fitted_results_for_plot['biomass']),
axs[1]: (substrate_data_exp, 'Sustrato', fitted_results_for_plot['substrate']),
axs[2]: (product_data_exp, 'Producto', fitted_results_for_plot['product'])
}
for ax, data_actual, ylabel, plot_results_list in plot_config.items():
ax.plot(time_data, data_actual, 'o', label=f'Datos {ylabel}', markersize=5, alpha=0.7)
for idx, res_detail in enumerate(plot_results_list):
label = f'Modelo {idx+1} (R²:{res_detail["R2"]:.3f})'
# if len(plot_results_list) == 1: label = f'Modelo {ylabel} (R²:{res_detail["R2"]:.3f})'
ax.plot(time_data, res_detail['y_pred'], '-', label=label, linewidth=2)
ax.set_xlabel('Tiempo')
ax.set_ylabel(ylabel)
ax.grid(True, linestyle=':', alpha=0.7)
if show_legend_ui:
ax.legend(loc=legend_position_ui, fontsize='small')
if show_params_ui and plot_results_list:
# Mostrar parámetros para todos los modelos ajustados en este subplot
param_display_texts = []
for idx, res_detail in enumerate(plot_results_list):
params_text = f"Modelo {idx+1}:\n" + "\n".join([f" {k}: {v:.4g}" for k,v in res_detail['params'].items()])
param_display_texts.append(params_text)
full_param_text = "\n---\n".join(param_display_texts)
# Ajustar posición del texto para que no se solape con la leyenda si es posible
text_x_pos = 0.02
text_y_pos = 0.98
v_align = 'top'
if legend_position_ui and 'upper' in legend_position_ui:
text_y_pos = 0.02
v_align = 'bottom'
ax.text(text_x_pos, text_y_pos, full_param_text, transform=ax.transAxes, fontsize=7,
verticalalignment=v_align, bbox=dict(boxstyle='round,pad=0.3', fc='lightyellow', alpha=0.8))
plt.tight_layout(rect=[0, 0, 1, 0.96]) # Ajustar para el suptitle si lo hubiera
fig.suptitle("Resultados del Ajuste de Modelos Cinéticos", fontsize=16)
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=150) # Aumentar dpi para mejor calidad
buf.seek(0)
image = Image.open(buf)
plt.close(fig) # Cerrar la figura para liberar memoria
# Construir prompt para LLM
prompt_intro = "Eres un experto en modelado cinético de bioprocesos. Analiza los siguientes resultados del ajuste de modelos a datos experimentales:\n\n"
prompt_details = json.dumps(results_for_llm_prompt, indent=2, ensure_ascii=False)
prompt_instructions = """\n\nPor favor, proporciona un análisis detallado y crítico en español, estructurado de la siguiente manera:
1. **Resumen General:** Una breve descripción del experimento y qué se intentó modelar.
2. **Análisis por Componente (Biomasa, Sustrato, Producto):**
a. Para cada ecuación probada:
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.
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).
iii. Ecuación Específica: Menciona la ecuación usada.
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é?
3. **Problemas y Limitaciones:**
a. ¿Hay problemas evidentes (ej. R² muy bajo, parámetros físicamente no realistas, sobreajuste si se puede inferir, etc.)?
b. ¿Qué limitaciones podrían tener los modelos o el proceso de ajuste?
4. **Sugerencias y Próximos Pasos:**
a. ¿Cómo se podría mejorar el modelado (ej. probar otras ecuaciones, transformar datos, revisar calidad de datos experimentales)?
b. ¿Qué experimentos adicionales podrían realizarse para validar o refinar los modelos?
5. **Conclusión Final:** Un veredicto general sobre el éxito del modelado y la utilidad de los resultados obtenidos.
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."""
full_prompt = prompt_intro + prompt_details + prompt_instructions
# Llamar al servicio LLM (ya sea Modal o local)
analysis_text = call_llm_analysis_service(full_prompt)
return image, analysis_text