File size: 13,597 Bytes
d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 d3d8124 9899954 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# 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 |