|
|
|
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 = {} |
|
|
|
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() |
|
|
|
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 |
|
} |
|
|
|
|
|
t_sym = symbols('t') |
|
current_param_syms = symbols(params_list) |
|
|
|
|
|
lambdify_args = [t_sym] + list(current_param_syms) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
all_symbols_in_eq = sympify(equation_str).free_symbols |
|
|
|
final_lambdify_args = list(lambdify_args) |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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'] |
|
|
|
|
|
|
|
def fit_model_wrapper(t_data, *current_p_values): |
|
|
|
subs_dict = {sym: val for sym, val in zip(current_param_syms, current_p_values)} |
|
|
|
|
|
X_val_sym = symbols('X_val') |
|
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).") |
|
|
|
|
|
biomass_model_config = self.models['biomass'] |
|
biomass_expr = biomass_model_config['sympy_expr'] |
|
biomass_p_syms = biomass_model_config['param_symbols'] |
|
|
|
|
|
biomass_subs_dict = {sym: biomass_params_fitted[name] for sym, name in zip(biomass_p_syms, biomass_model_config['params'])} |
|
|
|
|
|
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) |
|
|
|
|
|
subs_dict[X_val_sym] = X_t_values |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
args_for_lambdify = [t_sym] + list(current_param_syms) |
|
final_expr = equation_expr |
|
|
|
|
|
|
|
|
|
if X_val_sym in equation_expr.free_symbols and X_val_sym in subs_dict: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func_for_current_model = lambdify(args_for_lambdify, equation_expr.subs({X_val_sym: X_val_sym}), 'numpy') |
|
|
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
point_subs[X_val_sym] = point_subs[X_val_sym][i] |
|
|
|
y_calculated[i] = equation_expr.subs(point_subs).evalf() |
|
return y_calculated |
|
|
|
else: |
|
func = lambdify(args_for_lambdify, equation_expr, 'numpy') |
|
return func(t_data, *current_p_values) |
|
|
|
|
|
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)) |
|
|
|
|
|
with warnings.catch_warnings(): |
|
warnings.simplefilter("ignore", RuntimeWarning) |
|
warnings.simplefilter("ignore", UserWarning) |
|
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)) |
|
|
|
|
|
|
|
|
|
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: |
|
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 |