| | |
| | import numpy as np |
| | import pandas as pd |
| | import matplotlib |
| | matplotlib.use('Agg') |
| | import matplotlib.pyplot as plt |
| | from PIL import Image |
| | import io |
| | import json |
| | import traceback |
| |
|
| | from models import BioprocessModel |
| | |
| |
|
| | USE_MODAL_FOR_LLM_ANALYSIS = False |
| | generate_analysis_from_modal = None |
| |
|
| | def create_error_image(message="Error en procesamiento", width=600, height=400): |
| | |
| | img = Image.new('RGB', (width, height), color = (255, 200, 200)) |
| | print(f"Generando imagen de error: {message}") |
| | return img |
| |
|
| | def parse_bounds_str(bounds_str_input, num_params): |
| | |
| | bounds_str = str(bounds_str_input).strip() |
| | if not bounds_str: |
| | print(f"DEBUG (parse_bounds_str): Cadena de límites vacía para {num_params} params. Usando (-inf, inf).") |
| | return [-np.inf] * num_params, [np.inf] * num_params |
| | try: |
| | bounds_str = bounds_str.lower().replace('inf', 'np.inf').replace('none', 'None') |
| | if not (bounds_str.startswith('[') and bounds_str.endswith(']')): |
| | bounds_str = f"[{bounds_str}]" |
| | parsed_bounds_list = eval(bounds_str, {'np': np, 'inf': np.inf, 'None': None}) |
| | |
| | if not isinstance(parsed_bounds_list, list): |
| | raise ValueError("Cadena de límites no evaluó a una lista.") |
| | if len(parsed_bounds_list) != num_params: |
| | raise ValueError(f"Num límites ({len(parsed_bounds_list)}) != num params ({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"Límite debe ser (low, high). Se encontró: {item}") |
| | 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) |
| | print(f"DEBUG (parse_bounds_str): Límites parseados: L={lower_bounds}, U={upper_bounds}") |
| | return lower_bounds, upper_bounds |
| | except Exception as e: |
| | print(f"ERROR (parse_bounds_str): Parseando '{bounds_str_input}': {e}. Usando por defecto (-inf, inf).") |
| | return [-np.inf] * num_params, [np.inf] * num_params |
| |
|
| |
|
| | def call_llm_analysis_service(prompt: str) -> str: |
| | |
| | if USE_MODAL_FOR_LLM_ANALYSIS and generate_analysis_from_modal: |
| | print("DEBUG (interface.py): Llamando a Modal LLM...") |
| | try: return generate_analysis_from_modal(prompt) |
| | except Exception as e: print(f"ERROR (interface.py): Modal LLM: {e}"); traceback.print_exc(); return f"Error servicio IA: {e}" |
| | else: |
| | print("DEBUG (interface.py): Usando LLM local (fallback)...") |
| | try: |
| | from config import MODEL_PATH, MAX_LENGTH, DEVICE |
| | from transformers import AutoTokenizer, AutoModelForCausalLM |
| | import torch |
| | tokenizer_local = AutoTokenizer.from_pretrained(MODEL_PATH) |
| | model_local = AutoModelForCausalLM.from_pretrained(MODEL_PATH).to(DEVICE) |
| | model_context_window = getattr(model_local.config, 'max_position_embeddings', getattr(model_local.config, 'sliding_window', 4096)) |
| | max_prompt_len = model_context_window - MAX_LENGTH - 50 |
| | if max_prompt_len <= 0 : max_prompt_len = model_context_window // 2 |
| | inputs = tokenizer_local(prompt, return_tensors="pt", truncation=True, max_length=max_prompt_len).to(DEVICE) |
| | with torch.no_grad(): |
| | 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: print(f"ERROR (interface.py): Fallback LLM: {e}"); traceback.print_exc(); return f"Error LLM local: {e}." |
| |
|
| |
|
| | def process_and_plot( |
| | file_obj, |
| | 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 |
| | ): |
| | print("\nDEBUG (interface.py): process_and_plot INICIADO.") |
| | error_img = create_error_image("Error inicial en procesamiento") |
| | error_analysis_text = "No se pudo generar el análisis debido a un error de inicialización." |
| |
|
| | try: |
| | if file_obj is None: |
| | print("ERROR (interface.py): No se subió archivo.") |
| | return error_img, "Error: Por favor, sube un archivo Excel." |
| | print(f"DEBUG (interface.py): Archivo recibido: {file_obj.name}") |
| | |
| | try: |
| | df = pd.read_excel(file_obj.name) |
| | print(f"DEBUG (interface.py): Excel leído. Columnas: {df.columns.tolist()}") |
| | except Exception as e: |
| | return error_img, f"Error al leer el archivo Excel: {e}\n{traceback.format_exc()}" |
| |
|
| | expected_cols = ['Tiempo', 'Biomasa', 'Sustrato', 'Producto'] |
| | missing_cols = [col for col in expected_cols if col not in df.columns] |
| | if missing_cols: |
| | return error_img, f"Error: Faltan columnas en Excel: {', '.join(missing_cols)}." |
| |
|
| | time_data = df['Tiempo'].values |
| | biomass_data_exp = df['Biomasa'].values |
| | substrate_data_exp = df['Sustrato'].values |
| | product_data_exp = df['Producto'].values |
| | print(f"DEBUG (interface.py): Datos extraídos. Longitud de tiempo: {len(time_data)}") |
| |
|
| | try: |
| | active_biomass_eqs = int(float(biomass_eq_count_ui)) |
| | active_substrate_eqs = int(float(substrate_eq_count_ui)) |
| | active_product_eqs = int(float(product_eq_count_ui)) |
| | except (TypeError, ValueError) as e_count: |
| | return error_img, f"Error: Número de ecuaciones inválido: {e_count}" |
| | print(f"DEBUG (interface.py): Counts: Bio={active_biomass_eqs}, Sub={active_substrate_eqs}, Prod={active_product_eqs}") |
| |
|
| | 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() |
| | fitted_results_for_plot = {'biomass': [], 'substrate': [], 'product': []} |
| | results_for_llm_prompt = {'biomass': [], 'substrate': [], 'product': []} |
| | biomass_params_for_s_p_dict = None |
| |
|
| | for model_type, (eq_list, param_str_list, bound_str_list, exp_data) in all_eq_inputs.items(): |
| | if not (isinstance(exp_data, np.ndarray) and exp_data.size > 0 and np.any(np.isfinite(exp_data))): |
| | print(f"INFO (interface.py): Datos experimentales para {model_type} no válidos o vacíos, saltando ajuste.") |
| | results_for_llm_prompt[model_type].append({'equation': 'N/A - Sin datos válidos', 'params_fitted': {}, 'R2': np.nan, 'RMSE': np.nan}) |
| | continue |
| |
|
| | for i in range(len(eq_list)): |
| | eq_str, param_s, bound_s = eq_list[i], param_str_list[i], bound_str_list[i] |
| | if not eq_str or not param_s: |
| | print(f"INFO (interface.py): Ecuación o parámetros vacíos para {model_type} #{i+1}, saltando.") |
| | results_for_llm_prompt[model_type].append({'equation': eq_str if eq_str else 'Ecuación Vacía', 'params_fitted': {}, 'R2': np.nan, 'RMSE': np.nan, 'error': 'Ecuación o parámetros vacíos'}) |
| | continue |
| | |
| | print(f"\nDEBUG (interface.py): 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) |
| | |
| | |
| | current_biomass_params_for_fit = biomass_params_for_s_p_dict if model_type in ['substrate', 'product'] else None |
| | |
| | print(f"DEBUG (interface.py): Llamando a fit_model para {model_type} #{i+1}") |
| | y_pred, popt_values = model_handler.fit_model(model_type, time_data, exp_data, bounds=(l_b, u_b), biomass_params_fitted=current_biomass_params_for_fit) |
| | print(f"DEBUG (interface.py): fit_model regresó para {model_type} #{i+1}. y_pred (primeros 5): {y_pred[:5] if y_pred is not None else 'None'}") |
| |
|
| | if y_pred is None or popt_values is None: |
| | print(f"ERROR (interface.py): Ajuste falló (y_pred o popt es None) para {model_type} #{i+1}.") |
| | results_for_llm_prompt[model_type].append({'equation': eq_str, 'params_fitted': {}, 'R2': np.nan, 'RMSE': np.nan, 'error': 'Fallo en curve_fit'}) |
| | continue |
| |
|
| | current_params = model_handler.params.get(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}) |
| |
|
| | if model_type == 'biomass' and biomass_params_for_s_p_dict is None and current_params: |
| | biomass_params_for_s_p_dict = current_params |
| | print(f"DEBUG (interface.py): Parámetros de Biomasa (para S/P) guardados: {biomass_params_for_s_p_dict}") |
| |
|
| | except Exception as e_fit_loop: |
| | error_msg = f"Error en bucle de ajuste para {model_type} #{i+1} ('{eq_str}'): {e_fit_loop}\n{traceback.format_exc()}" |
| | print(error_msg) |
| | results_for_llm_prompt[model_type].append({'equation': eq_str, 'params_fitted': {}, 'R2': np.nan, 'RMSE': np.nan, 'error': str(e_fit_loop)}) |
| | |
| | |
| |
|
| | |
| | print("DEBUG (interface.py): Generando gráfico...") |
| | fig, axs = plt.subplots(3, 1, figsize=(10, 18), sharex=True) |
| | |
| | plot_config_map = {axs[0]:(biomass_data_exp,'Biomasa',fitted_results_for_plot['biomass']), axs[1]:(substrate_data_exp,'Sustrato',fitted_results_for_plot['sustrato']), axs[2]:(product_data_exp,'Producto',fitted_results_for_plot['product'])} |
| | any_plot_successful = False |
| | for ax, data_actual, ylabel, plot_results in plot_config_map.items(): |
| | if isinstance(data_actual, np.ndarray) and data_actual.size > 0 and np.any(np.isfinite(data_actual)): |
| | ax.plot(time_data, data_actual, 'o', label=f'Datos {ylabel}', markersize=5, alpha=0.7) |
| | else: ax.text(0.5,0.5,f"No hay datos para {ylabel}",transform=ax.transAxes,ha='center',va='center') |
| | for idx, res_detail in enumerate(plot_results): |
| | if res_detail.get('y_pred') is not None and np.any(np.isfinite(res_detail['y_pred'])): |
| | label = f'Modelo {idx+1} (R²:{res_detail.get("R2", float("nan")):.3f})' |
| | ax.plot(time_data, res_detail['y_pred'], '-', label=label, linewidth=2) |
| | any_plot_successful = True |
| | 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: |
| | param_display_texts = [f"Modelo {idx+1}:\n" + "\n".join([f" {k}: {v:.4g}" for k,v in res_d.get('params',{}).items()]) for idx, res_d in enumerate(plot_results) if res_d.get('params')] |
| | if param_display_texts: ax.text(0.02,0.98 if not ('upper' in legend_position_ui) else 0.02,"\n---\n".join(param_display_texts),transform=ax.transAxes,fontsize=7,verticalalignment='top' if not ('upper' in legend_position_ui) else 'bottom',bbox=dict(boxstyle='round,pad=0.3',fc='lightyellow',alpha=0.8)) |
| | |
| | if not any_plot_successful: |
| | print("WARN (interface.py): Ningún modelo produjo un gráfico válido.") |
| | |
| | axs[0].text(0.5, 0.5, "Ningún modelo se pudo ajustar o graficar.", transform=axs[0].transAxes, ha='center', va='center', fontsize=12, color='red') |
| |
|
| |
|
| | plt.tight_layout(rect=[0,0,1,0.96]); fig.suptitle("Resultados del Ajuste de Modelos Cinéticos",fontsize=16) |
| | buf = io.BytesIO(); plt.savefig(buf,format='png',dpi=150); buf.seek(0) |
| | image_pil = Image.open(buf); plt.close(fig) |
| | print("DEBUG (interface.py): Gráfico generado.") |
| |
|
| | |
| | |
| | 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. |
| | iv. Errores: Si hubo un error durante el ajuste para esta ecuación específica, menciónalo. |
| | 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 conciso 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 |
| | print("DEBUG (interface.py): Prompt para LLM generado. Llamando al servicio LLM...") |
| | analysis_text_llm = call_llm_analysis_service(full_prompt) |
| | print("DEBUG (interface.py): Análisis LLM recibido.") |
| |
|
| | return image_pil, analysis_text_llm |
| |
|
| | except Exception as general_e: |
| | error_trace = traceback.format_exc() |
| | error_message_full = f"Error GENERAL INESPERADO en process_and_plot: {general_e}\n{error_trace}" |
| | print(error_message_full) |
| | return create_error_image(f"Error General: {general_e}"), error_message_full |