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