File size: 11,264 Bytes
d3d8124 |
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 |
# 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 # Asegúrate que está importado
class BioprocessModel:
def __init__(self):
self.params = {} # Almacenará los parámetros ajustados para cada tipo de modelo
self.models = {} # Almacenará la configuración de cada tipo de modelo (ecuación, función, etc.)
self.r2 = {} # Almacenará el R^2 para cada tipo de modelo
self.rmse = {} # Almacenará el RMSE para cada tipo de modelo
def set_model(self, model_type, equation_str, param_str):
"""
Configura un modelo (Biomasa, Sustrato, Producto) usando una ecuación simbólica.
:param model_type: 'biomass', 'substrate', o 'product'
:param equation_str: La ecuación como una cadena de texto (ej. "Xm * (1 - exp(-um * t))")
Si la ecuación es para sustrato o producto y depende de la biomasa X(t),
se debe usar 'X_val' en la ecuación para representar el valor de X(t).
:param param_str: Cadena de parámetros separados por comas (ej. "Xm, um")
"""
equation_str = equation_str.strip()
# Si el usuario escribe "Y = ...", tomar solo la parte derecha
if '=' in equation_str:
equation_str = equation_str.split('=', 1)[1].strip()
params_list = [param.strip() for param in param_str.split(',')]
self.models[model_type] = {
'equation_str': equation_str,
'params': params_list
}
# Símbolos para el tiempo y los parámetros del modelo actual
t_sym = symbols('t')
current_param_syms = symbols(params_list)
# Argumentos para lambdify
lambdify_args = [t_sym] + list(current_param_syms)
# Si el modelo es sustrato o producto, puede depender de la biomasa (X_val)
# y de los parámetros de biomasa ajustados.
# La función `lambdify` solo manejará los parámetros directos de este modelo.
# La dependencia de X(t) se resolverá en la función de ajuste.
# Si la ecuación contiene 'X_val' (representando X(t) para modelos S y P)
# lo añadimos como un símbolo que se sustituirá en la función de ajuste.
all_symbols_in_eq = sympify(equation_str).free_symbols
final_lambdify_args = list(lambdify_args) # Copia para modificar
# Manejo de X_val para modelos S y P que dependen de biomasa
# No es necesario añadir X_val a lambdify_args aquí si se maneja en fit_model_wrapper
# func = lambdify(final_lambdify_args, sympify(equation_str), 'numpy')
# Guardar los símbolos para uso posterior si es necesario
self.models[model_type]['sympy_expr'] = sympify(equation_str)
self.models[model_type]['param_symbols'] = current_param_syms
self.models[model_type]['time_symbol'] = t_sym
# La función compilada ('function') se creará dinámicamente en fit_model para manejar dependencias
def fit_model(self, model_type, time, data, bounds, biomass_params_fitted=None):
"""
Ajusta el modelo configurado a los datos.
:param model_type: 'biomass', 'substrate', o 'product'
:param time: Array de datos de tiempo
:param data: Array de datos observados
:param bounds: Tupla (lower_bounds, upper_bounds) para los parámetros
:param biomass_params_fitted: Dict de parámetros de biomasa ajustados (necesario para sustrato/producto)
:return: (y_pred, popt) - Datos predichos y parámetros optimizados
"""
if model_type not in self.models:
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']
current_param_syms = model_config['param_symbols']
t_sym = model_config['time_symbol']
# Construir la función a ajustar con curve_fit
# Esta función tomará `t` y los parámetros del modelo actual como `*args`
def fit_model_wrapper(t_data, *current_p_values):
# Crear un diccionario de sustitución para los parámetros del modelo actual
subs_dict = {sym: val for sym, val in zip(current_param_syms, current_p_values)}
# Si es sustrato o producto y depende de la biomasa (X_val)
X_val_sym = symbols('X_val') # Símbolo para X(t)
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:
raise ValueError("Parámetros de biomasa ajustados son necesarios para modelos de sustrato/producto que dependen de X(t).")
# Calcular X(t) usando el modelo de biomasa ajustado y los t_data actuales
biomass_model_config = self.models['biomass']
biomass_expr = biomass_model_config['sympy_expr']
biomass_p_syms = biomass_model_config['param_symbols']
# Crear dict de sustitución para parámetros de biomasa
biomass_subs_dict = {sym: biomass_params_fitted[name] for sym, name in zip(biomass_p_syms, biomass_model_config['params'])}
# Evaluar X(t) para cada punto en t_data
X_t_values = np.array([
biomass_expr.subs(biomass_subs_dict).subs({biomass_model_config['time_symbol']: ti}).evalf()
for ti in t_data
], dtype=float)
# Añadir X_val (X(t)) al diccionario de sustituciones para la ecuación actual
subs_dict[X_val_sym] = X_t_values
# Evaluar la ecuación del modelo actual (sustrato, producto, o biomasa)
# con los valores de sus parámetros y X(t) si es aplicable
# Es importante que la ecuación use t_sym como variable independiente
# y que lambdify se haga sobre (t_sym, *current_param_syms, X_val_sym_if_present)
# Re-lambdify aquí para asegurar el scope correcto de X_t_values si se usa X_val_sym
args_for_lambdify = [t_sym] + list(current_param_syms)
final_expr = equation_expr # Expresión original
# Si X_val está en la ecuación, NO lo incluimos en args_for_lambdify
# sino que lo sustituimos ANTES de lambdify o lo pasamos como constante si es posible.
# Lo más robusto es sustituir X_val en la expresión ANTES de lambdify
if X_val_sym in equation_expr.free_symbols and X_val_sym in subs_dict:
# Si X_val es un array (porque t_data es un array), no podemos sustituirlo directamente
# en la expresión simbólica para crear una única función lambdify que tome `t`.
# En su lugar, evaluamos la expresión punto por punto después de sustituir parámetros.
# Esto es menos eficiente que un lambdify completo, pero más flexible.
# Evaluación numérica punto por punto
# No necesitamos lambdify si X_val es un array y se evalúa punto por punto
# La función `fit_model_wrapper` ya está iterando sobre los `t_data` (implícitamente a través de numpy)
# Entonces, si X_val es un array, la expresión `final_expr.subs(subs_dict)` debería funcionar si
# las operaciones son compatibles con numpy arrays (sympy suele hacerlo).
#
# La forma en que se llama a esta función desde curve_fit es: `fit_model_wrapper(time, p1, p2, ...)`
# donde `time` es el array completo de tiempos.
# `X_t_values` es un array de la misma longitud que `time`.
# Simpler lambdify for the current model parameters, handle X_val in wrapper
func_for_current_model = lambdify(args_for_lambdify, equation_expr.subs({X_val_sym: X_val_sym}), 'numpy') # Keep X_val symbolic for now
# Now call it, and if it needs X_val, it must be part of its signature or global
# This part is tricky with sympy and curve_fit when there are interdependencies.
# The lambda should capture X_t_values correctly if subs_dict[X_val_sym] is set.
# Let's evaluate por punto para mayor robustez con X_val siendo un array
y_calculated = np.empty_like(t_data, dtype=float)
for i, ti in enumerate(t_data):
point_subs = subs_dict.copy()
point_subs[t_sym] = ti
if X_val_sym in point_subs and isinstance(point_subs[X_val_sym], np.ndarray):
# Si X_val es un array, tomar el valor correspondiente a ti
point_subs[X_val_sym] = point_subs[X_val_sym][i]
y_calculated[i] = equation_expr.subs(point_subs).evalf()
return y_calculated
else: # Modelo de Biomasa o modelo S/P que no usa X_val (raro)
func = lambdify(args_for_lambdify, equation_expr, 'numpy')
return func(t_data, *current_p_values)
p0 = np.ones(len(current_param_names)) # Estimaciones iniciales
lower_bounds, upper_bounds = bounds
# Asegurar que los límites tengan la longitud correcta
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))
# Ignorar advertencias de RuntimeWarning de operaciones inválidas (ej. división por cero durante la optimización)
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
warnings.simplefilter("ignore", UserWarning) # Sympy lambdify warnings
popt, pcov = curve_fit(fit_model_wrapper, time, data, p0=p0, bounds=(lower_bounds, upper_bounds), maxfev=100000)
self.params[model_type] = dict(zip(current_param_names, popt))
# Calcular R^2 y RMSE usando los parámetros ajustados
# Es importante volver a llamar a fit_model_wrapper para obtener y_pred con los popt,
# ya que la función interna maneja las dependencias de X(t)
y_pred = fit_model_wrapper(time, *popt)
ss_res = np.sum((data - y_pred) ** 2)
ss_tot = np.sum((data - np.mean(data)) ** 2)
if ss_tot == 0: # Evitar división por cero si los datos son constantes
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))
return y_pred, popt # Devolver y_pred y los parámetros optimizados |