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