BioRAG / models.py
C2MV's picture
Upload 2 files
9899954 verified
# models.py
import numpy as np
from scipy.optimize import curve_fit
from sympy import symbols, sympify, lambdify
import warnings
from sklearn.metrics import mean_squared_error
class BioprocessModel:
def __init__(self):
self.params = {}
self.models = {}
self.r2 = {}
self.rmse = {}
print("DEBUG (models.py): BioprocessModel instanciado.")
def set_model(self, model_type, equation_str, param_str):
print(f"\nDEBUG (models.py): set_model llamado para tipo='{model_type}'")
print(f" Equation str (raw): '{equation_str}'")
print(f" Param str (raw): '{param_str}'")
equation_str_cleaned = str(equation_str).strip()
if '=' in equation_str_cleaned:
equation_str_cleaned = equation_str_cleaned.split('=', 1)[1].strip()
if not equation_str_cleaned:
print(f"ERROR (models.py): Ecuación vacía para {model_type}.")
raise ValueError(f"La cadena de la ecuación para '{model_type}' no puede estar vacía.")
if not param_str:
print(f"ERROR (models.py): Cadena de parámetros vacía para {model_type}.")
raise ValueError(f"La cadena de parámetros para '{model_type}' no puede estar vacía.")
params_list = [param.strip() for param in param_str.split(',')]
if not all(params_list): # Chequear si algún nombre de parámetro es vacío
print(f"ERROR (models.py): Algún nombre de parámetro está vacío en '{param_str}' para {model_type}.")
raise ValueError(f"Los nombres de los parámetros no pueden ser vacíos para '{model_type}'.")
print(f" Equation (cleaned): '{equation_str_cleaned}'")
print(f" Params list: {params_list}")
self.models[model_type] = {
'equation_str': equation_str_cleaned,
'params': params_list
}
try:
# Símbolos para el tiempo y los parámetros del modelo actual
t_sym = symbols('t')
# Asegurar que los parámetros sean símbolos válidos
current_param_syms = []
for p_name in params_list:
if not p_name.isidentifier(): # Chequeo básico de validez del nombre del símbolo
raise ValueError(f"Nombre de parámetro '{p_name}' no es un identificador Python válido para sympy.")
current_param_syms.append(symbols(p_name))
# Símbolo para X(t) si es necesario (solo para modelos S y P)
X_val_sym = symbols('X_val')
# Crear la expresión simbólica
sympy_expr = sympify(equation_str_cleaned)
print(f" Sympy expression: {sympy_expr}")
print(f" Free symbols in expr: {sympy_expr.free_symbols}")
# Guardar la expresión y los símbolos para uso en fit_model
self.models[model_type]['sympy_expr'] = sympy_expr
self.models[model_type]['param_symbols'] = tuple(current_param_syms) # Usar tupla
self.models[model_type]['time_symbol'] = t_sym
self.models[model_type]['X_val_symbol'] = X_val_sym # Guardar por si se usa
print(f" Modelo '{model_type}' configurado exitosamente.")
except Exception as e:
print(f"ERROR (models.py): Fallo al procesar con sympy para '{model_type}': {e}")
raise ValueError(f"Error en la ecuación o parámetros para '{model_type}': {e}")
def fit_model(self, model_type, time, data, bounds, biomass_params_fitted=None):
print(f"\nDEBUG (models.py): fit_model llamado para tipo='{model_type}'")
if model_type not in self.models:
print(f"ERROR (models.py): Modelo para '{model_type}' no configurado.")
raise ValueError(f"Modelo para '{model_type}' no configurado. Llama a set_model primero.")
model_config = self.models[model_type]
equation_expr = model_config['sympy_expr']
current_param_names = model_config['params'] # Lista de strings
current_param_syms = model_config['param_symbols'] # Tupla de símbolos sympy
t_sym = model_config['time_symbol']
X_val_sym = model_config['X_val_symbol']
print(f" Ajustando con ecuación: {equation_expr}")
print(f" Parámetros a ajustar: {current_param_names}")
print(f" Datos de tiempo (primeros 5): {time[:5]}")
print(f" Datos experimentales (primeros 5): {data[:5]}")
print(f" Límites: {bounds}")
if biomass_params_fitted:
print(f" Parámetros de biomasa ajustados (para S/P): {biomass_params_fitted}")
# Función que será pasada a curve_fit
def fit_model_wrapper(t_data_wrapper, *current_p_values_wrapper):
# `t_data_wrapper` es un array numpy de tiempos.
# `current_p_values_wrapper` es una tupla de los valores actuales de los parámetros.
# Diccionario de sustitución para los parámetros del modelo actual
subs_dict_wrapper = {sym: val for sym, val in zip(current_param_syms, current_p_values_wrapper)}
# Preparar la expresión para lambdify: solo con `t` y los parámetros del modelo actual
lambdify_args_wrapper = [t_sym] + list(current_param_syms)
expr_to_lambdify = equation_expr
# Manejar dependencia de X_val para sustrato y producto
if model_type in ['substrate', 'product'] and X_val_sym in equation_expr.free_symbols:
if biomass_params_fitted is None or 'biomass' not in self.models or 'sympy_expr' not in self.models['biomass']:
print("ERROR (models.py fit_model_wrapper): Falta config/params de biomasa para modelo S/P dependiente.")
# Devolver algo que cause error en curve_fit o un array de NaNs de tamaño correcto
return np.full_like(t_data_wrapper, np.nan)
biomass_model_config = self.models['biomass']
biomass_expr = biomass_model_config['sympy_expr']
biomass_p_syms = biomass_model_config['param_symbols']
biomass_t_sym = biomass_model_config['time_symbol']
biomass_subs_for_calc = {sym: biomass_params_fitted[name] for sym, name in zip(biomass_p_syms, biomass_model_config['params'])}
# Calcular X(t) para cada tiempo en t_data_wrapper
# Esto DEBE resultar en un array numpy
try:
# Lambdify la expresión de biomasa una vez si es posible
if 'biomass_func_lambdified' not in biomass_model_config:
biomass_model_config['biomass_func_lambdified'] = lambdify(
[biomass_t_sym] + list(biomass_p_syms),
biomass_expr,
'numpy'
)
# Obtener los valores de los parámetros de biomasa en el orden correcto
biomass_p_values_for_calc = [biomass_params_fitted[p_name] for p_name in biomass_model_config['params']]
X_t_values_wrapper = biomass_model_config['biomass_func_lambdified'](t_data_wrapper, *biomass_p_values_for_calc)
except Exception as e_biomass_calc:
print(f"ERROR (models.py fit_model_wrapper): Calculando X(t) para S/P: {e_biomass_calc}")
return np.full_like(t_data_wrapper, np.nan)
# Ahora, X_val_sym necesita ser reemplazado por X_t_values_wrapper en expr_to_lambdify
# Esto es complicado porque X_t_values_wrapper es un array, no un escalar simbólico.
# La forma más segura es sustituirlo en la expresión antes de lambdify, si sympy lo permite,
# o pasarlo como un argumento extra a una función lambdify que lo espere.
#
# Alternativa: si lambdify no maneja bien un array como X_val, evaluar punto por punto.
# Por ahora, intentaremos pasar X_val como un argumento adicional a lambdify.
if X_val_sym not in current_param_syms: # Asegurar que no es ya un parámetro del modelo S/P
lambdify_args_wrapper.append(X_val_sym)
# Crear la función compilada para el modelo actual (S o P)
# Esta función ahora tomará t, params_actuales..., y X_val_array
func_compiled = lambdify(lambdify_args_wrapper, expr_to_lambdify, 'numpy')
# Llamar a la función compilada
try:
# Pasar X_t_values_wrapper como el último argumento si X_val_sym fue añadido
call_args = [t_data_wrapper] + list(current_p_values_wrapper)
if X_val_sym in lambdify_args_wrapper[-1:]: # Si X_val_sym es el último argumento esperado
call_args.append(X_t_values_wrapper)
return func_compiled(*call_args)
except Exception as e_sp_eval:
print(f"ERROR (models.py fit_model_wrapper): Evaluando S/P con X_val: {e_sp_eval}")
return np.full_like(t_data_wrapper, np.nan)
else: # Es el modelo de biomasa o un modelo S/P que no usa X_val
func_compiled = lambdify(lambdify_args_wrapper, expr_to_lambdify, 'numpy')
try:
return func_compiled(t_data_wrapper, *current_p_values_wrapper)
except Exception as e_bio_eval:
print(f"ERROR (models.py fit_model_wrapper): Evaluando biomasa: {e_bio_eval}")
return np.full_like(t_data_wrapper, np.nan)
p0 = np.ones(len(current_param_names))
lower_bounds, upper_bounds = bounds
lower_bounds = np.array(lower_bounds if len(lower_bounds) == len(p0) else [-np.inf] * len(p0))
upper_bounds = np.array(upper_bounds if len(upper_bounds) == len(p0) else [np.inf] * len(p0))
print(f" Estimaciones iniciales p0: {p0}")
print(f" Límites para curve_fit: L={lower_bounds}, U={upper_bounds}")
popt, pcov = None, None # Inicializar
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
warnings.simplefilter("ignore", UserWarning)
try:
popt, pcov = curve_fit(fit_model_wrapper, time, data, p0=p0, bounds=(lower_bounds, upper_bounds), maxfev=50000, method='trf') # 'trf' es bueno con límites
print(f" curve_fit completado. Parámetros optimizados (popt): {popt}")
except RuntimeError as e_curvefit: # A menudo "Optimal parameters not found"
print(f"ERROR (models.py): curve_fit falló para {model_type} con RuntimeError: {e_curvefit}")
self.params[model_type] = {p: np.nan for p in current_param_names}
self.r2[model_type] = np.nan
self.rmse[model_type] = np.nan
return np.full_like(data, np.nan), None # Devolver NaNs y None para popt
except ValueError as e_val_curvefit: # A menudo por límites o datos incompatibles
print(f"ERROR (models.py): curve_fit falló para {model_type} con ValueError: {e_val_curvefit}")
self.params[model_type] = {p: np.nan for p in current_param_names}
self.r2[model_type] = np.nan
self.rmse[model_type] = np.nan
return np.full_like(data, np.nan), None
except Exception as e_gen_curvefit: # Cualquier otro error
print(f"ERROR (models.py): curve_fit falló inesperadamente para {model_type}: {e_gen_curvefit}")
self.params[model_type] = {p: np.nan for p in current_param_names}
self.r2[model_type] = np.nan
self.rmse[model_type] = np.nan
return np.full_like(data, np.nan), None
if popt is None: # Si curve_fit falló y ya manejamos el error
return np.full_like(data, np.nan), None
self.params[model_type] = dict(zip(current_param_names, popt))
# Re-calcular y_pred con los parámetros optimizados
try:
y_pred = fit_model_wrapper(time, *popt)
if np.any(np.isnan(y_pred)): # Si la evaluación con popt da NaN
print(f"ADVERTENCIA (models.py): y_pred contiene NaNs después del ajuste para {model_type}.")
self.r2[model_type] = np.nan
self.rmse[model_type] = np.nan
# No sobrescribir self.params[model_type] aquí si popt fue encontrado
else:
ss_res = np.sum((data - y_pred) ** 2)
ss_tot = np.sum((data - np.mean(data)) ** 2)
if ss_tot == 0:
self.r2[model_type] = 1.0 if ss_res < 1e-9 else 0.0
else:
self.r2[model_type] = 1 - (ss_res / ss_tot)
self.rmse[model_type] = np.sqrt(mean_squared_error(data, y_pred))
except Exception as e_ypred:
print(f"ERROR (models.py): Calculando y_pred final para {model_type}: {e_ypred}")
y_pred = np.full_like(data, np.nan) # Devolver NaNs si la predicción final falla
self.r2[model_type] = np.nan
self.rmse[model_type] = np.nan
print(f" Ajuste para {model_type} completado. R2: {self.r2.get(model_type)}, RMSE: {self.rmse.get(model_type)}")
return y_pred, popt