Update app.py
Browse files
app.py
CHANGED
@@ -41,18 +41,14 @@ class BioprocessModel:
|
|
41 |
|
42 |
@staticmethod
|
43 |
def logistic(time, xo, xm, um):
|
44 |
-
|
45 |
-
|
46 |
-
return np.full_like(time, np.nan) # or handle appropriately
|
47 |
-
# Add a small epsilon to prevent division by zero in the denominator
|
48 |
denominator = (1 - (xo / xm) * (1 - np.exp(um * time)))
|
49 |
-
denominator = np.where(denominator == 0, 1e-9, denominator)
|
50 |
return (xo * np.exp(um * time)) / denominator
|
51 |
|
52 |
-
|
53 |
@staticmethod
|
54 |
def gompertz(time, xm, um, lag):
|
55 |
-
# Ensure xm is not zero
|
56 |
if xm == 0:
|
57 |
return np.full_like(time, np.nan)
|
58 |
return xm * np.exp(-np.exp((um * np.e / xm) * (lag - time) + 1))
|
@@ -64,14 +60,14 @@ class BioprocessModel:
|
|
64 |
@staticmethod
|
65 |
def logistic_diff(X, t, params):
|
66 |
xo, xm, um = params
|
67 |
-
if xm == 0:
|
68 |
return 0
|
69 |
return um * X * (1 - X / xm)
|
70 |
|
71 |
@staticmethod
|
72 |
def gompertz_diff(X, t, params):
|
73 |
xm, um, lag = params
|
74 |
-
if xm == 0:
|
75 |
return 0
|
76 |
return X * (um * np.e / xm) * np.exp((um * np.e / xm) * (lag - t) + 1)
|
77 |
|
@@ -84,44 +80,32 @@ class BioprocessModel:
|
|
84 |
if self.biomass_model is None or not biomass_params:
|
85 |
return np.full_like(time, np.nan)
|
86 |
X_t = self.biomass_model(time, *biomass_params)
|
87 |
-
if np.any(np.isnan(X_t)):
|
88 |
return np.full_like(time, np.nan)
|
89 |
-
# dXdt = np.gradient(X_t, time, edge_order=2) # Use edge_order=2 for better boundary derivatives
|
90 |
-
# integral_X = np.cumsum(X_t) * np.gradient(time)
|
91 |
-
# A more robust way to calculate integral, especially for non-uniform time
|
92 |
integral_X = np.zeros_like(X_t)
|
93 |
if len(time) > 1:
|
94 |
-
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
95 |
integral_X = np.cumsum(X_t * dt)
|
96 |
|
97 |
-
|
98 |
-
# Initial biomass value is the first element of biomass_params for logistic (xo)
|
99 |
-
# For Gompertz and Moser, biomass_params[0] is Xm. We need X(t=0)
|
100 |
if self.model_type == 'logistic':
|
101 |
X0 = biomass_params[0]
|
102 |
elif self.model_type == 'gompertz':
|
103 |
-
# X(0) for Gompertz
|
104 |
X0 = self.gompertz(0, *biomass_params)
|
105 |
elif self.model_type == 'moser':
|
106 |
-
# X(0) for Moser
|
107 |
X0 = self.moser(0, *biomass_params)
|
108 |
else:
|
109 |
-
X0 = X_t[0]
|
110 |
-
|
111 |
return so - p * (X_t - X0) - q * integral_X
|
112 |
|
113 |
-
|
114 |
def product(self, time, po, alpha, beta, biomass_params):
|
115 |
if self.biomass_model is None or not biomass_params:
|
116 |
return np.full_like(time, np.nan)
|
117 |
X_t = self.biomass_model(time, *biomass_params)
|
118 |
-
if np.any(np.isnan(X_t)):
|
119 |
return np.full_like(time, np.nan)
|
120 |
-
# dXdt = np.gradient(X_t, time, edge_order=2)
|
121 |
-
# integral_X = np.cumsum(X_t) * np.gradient(time)
|
122 |
integral_X = np.zeros_like(X_t)
|
123 |
if len(time) > 1:
|
124 |
-
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
125 |
integral_X = np.cumsum(X_t * dt)
|
126 |
|
127 |
if self.model_type == 'logistic':
|
@@ -132,7 +116,6 @@ class BioprocessModel:
|
|
132 |
X0 = self.moser(0, *biomass_params)
|
133 |
else:
|
134 |
X0 = X_t[0]
|
135 |
-
|
136 |
return po + alpha * (X_t - X0) + beta * integral_X
|
137 |
|
138 |
def process_data(self, df):
|
@@ -151,12 +134,11 @@ class BioprocessModel:
|
|
151 |
self.datax.append(data_biomass)
|
152 |
self.dataxp.append(np.mean(data_biomass, axis=0))
|
153 |
self.datax_std.append(np.std(data_biomass, axis=0, ddof=1))
|
154 |
-
else:
|
155 |
self.datax.append(np.array([]))
|
156 |
self.dataxp.append(np.array([]))
|
157 |
self.datax_std.append(np.array([]))
|
158 |
|
159 |
-
|
160 |
if len(substrate_cols) > 0:
|
161 |
data_substrate = [df[col].values for col in substrate_cols]
|
162 |
data_substrate = np.array(data_substrate)
|
@@ -178,8 +160,6 @@ class BioprocessModel:
|
|
178 |
self.datap.append(np.array([]))
|
179 |
self.datapp.append(np.array([]))
|
180 |
self.datap_std.append(np.array([]))
|
181 |
-
|
182 |
-
|
183 |
self.time = time
|
184 |
|
185 |
def fit_model(self):
|
@@ -195,35 +175,28 @@ class BioprocessModel:
|
|
195 |
|
196 |
def fit_biomass(self, time, biomass):
|
197 |
try:
|
198 |
-
|
199 |
-
if len(np.unique(biomass)) < 2 : # or np.std(biomass) == 0:
|
200 |
print(f"Biomasa constante para {self.model_type}, no se puede ajustar el modelo.")
|
201 |
return None
|
202 |
|
203 |
if self.model_type == 'logistic':
|
204 |
-
# Ensure initial xo is less than xm. Max biomass could be initial guess for xm.
|
205 |
-
# xo guess: first non-zero biomass value or a small positive number
|
206 |
xo_guess = biomass[biomass > 1e-6][0] if np.any(biomass > 1e-6) else 1e-3
|
207 |
xm_guess = max(biomass) * 1.1 if max(biomass) > xo_guess else xo_guess * 2
|
208 |
-
if xm_guess <= xo_guess: xm_guess = xo_guess + 1e-3
|
209 |
p0 = [xo_guess, xm_guess, 0.1]
|
210 |
bounds = ([1e-9, 1e-9, 1e-9], [np.inf, np.inf, np.inf])
|
211 |
popt, _ = curve_fit(self.logistic, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9)
|
212 |
-
# Check for xm > xo after fit
|
213 |
if popt[1] <= popt[0]:
|
214 |
print(f"Advertencia: En modelo logístico, Xm ({popt[1]:.2f}) no es mayor que Xo ({popt[0]:.2f}). Ajuste puede no ser válido.")
|
215 |
-
# Optionally, try to re-fit with constraints or return None
|
216 |
self.params['biomass'] = {'xo': popt[0], 'xm': popt[1], 'um': popt[2]}
|
217 |
y_pred = self.logistic(time, *popt)
|
218 |
|
219 |
elif self.model_type == 'gompertz':
|
220 |
xm_guess = max(biomass) if max(biomass) > 0 else 1.0
|
221 |
um_guess = 0.1
|
222 |
-
# Estimate lag phase: time until significant growth starts
|
223 |
-
# This is a rough estimate, could be improved
|
224 |
lag_guess = time[np.argmax(np.gradient(biomass))] if len(biomass) > 1 and np.any(np.gradient(biomass) > 1e-6) else time[0]
|
225 |
p0 = [xm_guess, um_guess, lag_guess]
|
226 |
-
bounds = ([1e-9, 1e-9, 0], [np.inf, np.inf, max(time) if len(time)>0 else 100])
|
227 |
popt, _ = curve_fit(self.gompertz, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9)
|
228 |
self.params['biomass'] = {'xm': popt[0], 'um': popt[1], 'lag': popt[2]}
|
229 |
y_pred = self.gompertz(time, *popt)
|
@@ -231,9 +204,8 @@ class BioprocessModel:
|
|
231 |
elif self.model_type == 'moser':
|
232 |
Xm_guess = max(biomass) if max(biomass) > 0 else 1.0
|
233 |
um_guess = 0.1
|
234 |
-
Ks_guess = time[0]
|
235 |
p0 = [Xm_guess, um_guess, Ks_guess]
|
236 |
-
# Ks could be negative if growth starts before t=0 effectively
|
237 |
bounds = ([1e-9, 1e-9, -np.inf], [np.inf, np.inf, max(time) if len(time)>0 else 100])
|
238 |
popt, _ = curve_fit(self.moser, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9)
|
239 |
self.params['biomass'] = {'Xm': popt[0], 'um': popt[1], 'Ks': popt[2]}
|
@@ -247,18 +219,17 @@ class BioprocessModel:
|
|
247 |
self.rmse['biomass'] = np.nan
|
248 |
return None
|
249 |
|
250 |
-
# Ensure R2 calculation is robust against constant biomass data (already checked, but good practice)
|
251 |
ss_res = np.sum((biomass - y_pred) ** 2)
|
252 |
ss_tot = np.sum((biomass - np.mean(biomass)) ** 2)
|
253 |
-
if ss_tot == 0:
|
254 |
-
self.r2['biomass'] = 1.0 if ss_res == 0 else 0.0
|
255 |
else:
|
256 |
self.r2['biomass'] = 1 - (ss_res / ss_tot)
|
257 |
self.rmse['biomass'] = np.sqrt(mean_squared_error(biomass, y_pred))
|
258 |
return y_pred
|
259 |
except RuntimeError as e:
|
260 |
print(f"Error de Runtime en fit_biomass_{self.model_type} (probablemente no se pudo ajustar): {e}")
|
261 |
-
self.params['biomass'] = {}
|
262 |
self.r2['biomass'] = np.nan
|
263 |
self.rmse['biomass'] = np.nan
|
264 |
return None
|
@@ -270,11 +241,10 @@ class BioprocessModel:
|
|
270 |
return None
|
271 |
|
272 |
def fit_substrate(self, time, substrate, biomass_params_dict):
|
273 |
-
if not biomass_params_dict:
|
274 |
print(f"Error en fit_substrate_{self.model_type}: Parámetros de biomasa no disponibles.")
|
275 |
return None
|
276 |
try:
|
277 |
-
# Extract parameters based on model type
|
278 |
if self.model_type == 'logistic':
|
279 |
biomass_params_values = [biomass_params_dict['xo'], biomass_params_dict['xm'], biomass_params_dict['um']]
|
280 |
elif self.model_type == 'gompertz':
|
@@ -285,12 +255,11 @@ class BioprocessModel:
|
|
285 |
return None
|
286 |
|
287 |
so_guess = substrate[0] if len(substrate) > 0 else 1.0
|
288 |
-
p_guess = 0.1
|
289 |
-
q_guess = 0.01
|
290 |
p0 = [so_guess, p_guess, q_guess]
|
291 |
-
bounds = ([0, 0, 0], [np.inf, np.inf, np.inf])
|
292 |
|
293 |
-
# Use a lambda that directly takes the parameter values list
|
294 |
popt, _ = curve_fit(
|
295 |
lambda t, so, p, q: self.substrate(t, so, p, q, biomass_params_values),
|
296 |
time, substrate, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9
|
@@ -340,10 +309,10 @@ class BioprocessModel:
|
|
340 |
return None
|
341 |
|
342 |
po_guess = product[0] if len(product) > 0 else 0.0
|
343 |
-
alpha_guess = 0.1
|
344 |
-
beta_guess = 0.01
|
345 |
p0 = [po_guess, alpha_guess, beta_guess]
|
346 |
-
bounds = ([0, 0, 0], [np.inf, np.inf, np.inf])
|
347 |
|
348 |
popt, _ = curve_fit(
|
349 |
lambda t, po, alpha, beta: self.product(t, po, alpha, beta, biomass_params_values),
|
@@ -381,98 +350,67 @@ class BioprocessModel:
|
|
381 |
|
382 |
def generate_fine_time_grid(self, time):
|
383 |
if time is None or len(time) == 0:
|
384 |
-
return np.array([0])
|
385 |
time_fine = np.linspace(time.min(), time.max(), 500)
|
386 |
return time_fine
|
387 |
|
388 |
def system(self, y, t, biomass_params_list, substrate_params_list, product_params_list, model_type):
|
389 |
-
X, S, P = y
|
390 |
|
391 |
-
# Biomass growth (dX/dt)
|
392 |
if model_type == 'logistic':
|
393 |
-
# biomass_params_list for logistic: [xo, xm, um]
|
394 |
-
# logistic_diff expects X (current biomass), t, params=[xo, xm, um]
|
395 |
-
# However, logistic_diff is defined as um * X * (1 - X / xm) using current X
|
396 |
-
# For ODE integration, xo is part of initial conditions, not the rate params.
|
397 |
-
# So, params for logistic_diff should be [xm, um] effectively, if xo is handled by y[0]
|
398 |
-
# Let's assume biomass_params_list = [xo, xm, um] from fitted model
|
399 |
-
# The differential equation for logistic growth does not directly use xo.
|
400 |
-
# It's um * X * (1 - X / Xm). So params = [Xm, um]
|
401 |
-
# For consistency, we pass all fitted params and let the diff eq select.
|
402 |
dXdt = self.logistic_diff(X, t, biomass_params_list)
|
403 |
elif model_type == 'gompertz':
|
404 |
-
# biomass_params_list for gompertz: [xm, um, lag]
|
405 |
dXdt = self.gompertz_diff(X, t, biomass_params_list)
|
406 |
elif model_type == 'moser':
|
407 |
-
# biomass_params_list for moser: [Xm, um, Ks]
|
408 |
dXdt = self.moser_diff(X, t, biomass_params_list)
|
409 |
else:
|
410 |
-
dXdt = 0.0
|
411 |
|
412 |
-
# Substrate consumption (dS/dt)
|
413 |
-
# substrate_params_list: [so, p, q]
|
414 |
-
# dS/dt = -p * dX/dt - q * X
|
415 |
-
# so is initial substrate, not used in differential form directly
|
416 |
p_val = substrate_params_list[1] if len(substrate_params_list) > 1 else 0
|
417 |
q_val = substrate_params_list[2] if len(substrate_params_list) > 2 else 0
|
418 |
dSdt = -p_val * dXdt - q_val * X
|
419 |
|
420 |
-
# Product formation (dP/dt)
|
421 |
-
# product_params_list: [po, alpha, beta]
|
422 |
-
# dP/dt = alpha * dX/dt + beta * X
|
423 |
-
# po is initial product, not used in differential form directly
|
424 |
alpha_val = product_params_list[1] if len(product_params_list) > 1 else 0
|
425 |
beta_val = product_params_list[2] if len(product_params_list) > 2 else 0
|
426 |
dPdt = alpha_val * dXdt + beta_val * X
|
427 |
|
428 |
return [dXdt, dSdt, dPdt]
|
429 |
|
430 |
-
|
431 |
def get_initial_conditions(self, time, biomass, substrate, product):
|
432 |
-
# Use experimental data for initial conditions if params are not available or to be robust
|
433 |
X0_exp = biomass[0] if len(biomass) > 0 else 0
|
434 |
S0_exp = substrate[0] if len(substrate) > 0 else 0
|
435 |
P0_exp = product[0] if len(product) > 0 else 0
|
436 |
|
437 |
-
# Initial biomass (X0)
|
438 |
if 'biomass' in self.params and self.params['biomass']:
|
439 |
if self.model_type == 'logistic':
|
440 |
-
# xo is the initial biomass in logistic model definition
|
441 |
X0 = self.params['biomass'].get('xo', X0_exp)
|
442 |
elif self.model_type == 'gompertz':
|
443 |
-
# X(t=0) for Gompertz
|
444 |
xm = self.params['biomass'].get('xm', 1)
|
445 |
um = self.params['biomass'].get('um', 0.1)
|
446 |
lag = self.params['biomass'].get('lag', 0)
|
447 |
-
X0 = self.gompertz(0, xm, um, lag)
|
448 |
-
if np.isnan(X0): X0 = X0_exp
|
449 |
elif self.model_type == 'moser':
|
450 |
-
# X(t=0) for Moser
|
451 |
Xm_param = self.params['biomass'].get('Xm', 1)
|
452 |
um_param = self.params['biomass'].get('um', 0.1)
|
453 |
Ks_param = self.params['biomass'].get('Ks', 0)
|
454 |
-
X0 = self.moser(0, Xm_param, um_param, Ks_param)
|
455 |
-
if np.isnan(X0): X0 = X0_exp
|
456 |
else:
|
457 |
-
X0 = X0_exp
|
458 |
else:
|
459 |
X0 = X0_exp
|
460 |
|
461 |
-
# Initial substrate (S0)
|
462 |
if 'substrate' in self.params and self.params['substrate']:
|
463 |
-
# so is the initial substrate in the Luedeking-Piret substrate model
|
464 |
S0 = self.params['substrate'].get('so', S0_exp)
|
465 |
else:
|
466 |
S0 = S0_exp
|
467 |
|
468 |
-
# Initial product (P0)
|
469 |
if 'product' in self.params and self.params['product']:
|
470 |
-
# po is the initial product in the Luedeking-Piret product model
|
471 |
P0 = self.params['product'].get('po', P0_exp)
|
472 |
else:
|
473 |
P0 = P0_exp
|
474 |
|
475 |
-
# Ensure initial conditions are not NaN
|
476 |
X0 = X0 if not np.isnan(X0) else 0.0
|
477 |
S0 = S0 if not np.isnan(S0) else 0.0
|
478 |
P0 = P0 if not np.isnan(P0) else 0.0
|
@@ -483,40 +421,25 @@ class BioprocessModel:
|
|
483 |
if 'biomass' not in self.params or not self.params['biomass']:
|
484 |
print("No hay parámetros de biomasa, no se pueden resolver las EDO.")
|
485 |
return None, None, None, time
|
486 |
-
if time is None or len(time) == 0 :
|
487 |
print("Tiempo no válido para resolver EDOs.")
|
488 |
return None, None, None, np.array([])
|
489 |
|
490 |
-
|
491 |
-
# Prepare biomass_params_list for ODE system
|
492 |
-
# These are the parameters *of the differential equation itself*, not necessarily all fitted constants
|
493 |
-
# For logistic_diff: expects [xm, um] effectively if xo is IC.
|
494 |
-
# But our diff functions are written to take the full fitted set.
|
495 |
if self.model_type == 'logistic':
|
496 |
-
# self.params['biomass'] = {'xo': popt[0], 'xm': popt[1], 'um': popt[2]}
|
497 |
biomass_params_list = [self.params['biomass']['xo'], self.params['biomass']['xm'], self.params['biomass']['um']]
|
498 |
elif self.model_type == 'gompertz':
|
499 |
-
# self.params['biomass'] = {'xm': popt[0], 'um': popt[1], 'lag': popt[2]}
|
500 |
biomass_params_list = [self.params['biomass']['xm'], self.params['biomass']['um'], self.params['biomass']['lag']]
|
501 |
elif self.model_type == 'moser':
|
502 |
-
# self.params['biomass'] = {'Xm': popt[0], 'um': popt[1], 'Ks': popt[2]}
|
503 |
biomass_params_list = [self.params['biomass']['Xm'], self.params['biomass']['um'], self.params['biomass']['Ks']]
|
504 |
else:
|
505 |
print(f"Tipo de modelo de biomasa desconocido: {self.model_type}")
|
506 |
return None, None, None, time
|
507 |
|
508 |
-
# Prepare substrate_params_list for ODE system
|
509 |
-
# self.params['substrate'] = {'so': popt[0], 'p': popt[1], 'q': popt[2]}
|
510 |
-
# The ODE system uses p and q. so is an initial condition.
|
511 |
substrate_params_list = [
|
512 |
self.params.get('substrate', {}).get('so', 0),
|
513 |
self.params.get('substrate', {}).get('p', 0),
|
514 |
self.params.get('substrate', {}).get('q', 0)
|
515 |
]
|
516 |
-
|
517 |
-
# Prepare product_params_list for ODE system
|
518 |
-
# self.params['product'] = {'po': popt[0], 'alpha': popt[1], 'beta': popt[2]}
|
519 |
-
# The ODE system uses alpha and beta. po is an initial condition.
|
520 |
product_params_list = [
|
521 |
self.params.get('product', {}).get('po', 0),
|
522 |
self.params.get('product', {}).get('alpha', 0),
|
@@ -532,10 +455,9 @@ class BioprocessModel:
|
|
532 |
try:
|
533 |
sol = odeint(self.system, initial_conditions, time_fine,
|
534 |
args=(biomass_params_list, substrate_params_list, product_params_list, self.model_type),
|
535 |
-
rtol=1e-6, atol=1e-6)
|
536 |
except Exception as e:
|
537 |
print(f"Error al resolver EDOs con odeint: {e}")
|
538 |
-
# Try with lsoda if default fails (often more robust)
|
539 |
try:
|
540 |
print("Intentando con método 'lsoda'...")
|
541 |
sol = odeint(self.system, initial_conditions, time_fine,
|
@@ -545,11 +467,9 @@ class BioprocessModel:
|
|
545 |
print(f"Error al resolver EDOs con odeint (método lsoda): {e_lsoda}")
|
546 |
return None, None, None, time_fine
|
547 |
|
548 |
-
|
549 |
X = sol[:, 0]
|
550 |
S = sol[:, 1]
|
551 |
P = sol[:, 2]
|
552 |
-
|
553 |
return X, S, P, time_fine
|
554 |
|
555 |
def plot_results(self, time, biomass, substrate, product,
|
@@ -559,17 +479,16 @@ class BioprocessModel:
|
|
559 |
show_legend=True, show_params=True,
|
560 |
style='whitegrid',
|
561 |
line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
|
562 |
-
use_differential=False, axis_labels=None
|
|
|
563 |
|
564 |
-
if y_pred_biomass is None and not use_differential:
|
565 |
print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type} y no se usan EDO. Omitiendo figura.")
|
566 |
return None
|
567 |
if use_differential and ('biomass' not in self.params or not self.params['biomass']):
|
568 |
print(f"Se solicitó usar EDO pero no hay parámetros de biomasa para {experiment_name}. Omitiendo EDO.")
|
569 |
-
use_differential = False
|
570 |
-
|
571 |
|
572 |
-
# Set axis labels with defaults
|
573 |
if axis_labels is None:
|
574 |
axis_labels = {
|
575 |
'x_label': 'Tiempo',
|
@@ -579,21 +498,17 @@ class BioprocessModel:
|
|
579 |
}
|
580 |
|
581 |
sns.set_style(style)
|
582 |
-
time_to_plot = time
|
583 |
|
584 |
if use_differential and 'biomass' in self.params and self.params['biomass']:
|
585 |
X_ode, S_ode, P_ode, time_fine_ode = self.solve_differential_equations(time, biomass, substrate, product)
|
586 |
if X_ode is not None:
|
587 |
y_pred_biomass, y_pred_substrate, y_pred_product = X_ode, S_ode, P_ode
|
588 |
-
time_to_plot = time_fine_ode
|
589 |
else:
|
590 |
print(f"Fallo al resolver EDOs para {experiment_name}, usando resultados de curve_fit si existen.")
|
591 |
-
|
592 |
-
time_to_plot = time # Revert to original time if ODE failed
|
593 |
else:
|
594 |
-
# If not using differential or if biomass params are missing, use the curve_fit time
|
595 |
-
# For curve_fit, the predictions are already on the original 'time' grid.
|
596 |
-
# If we want smoother curve_fit lines, we need to evaluate them on a finer grid too.
|
597 |
if not use_differential and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
598 |
time_fine_curvefit = self.generate_fine_time_grid(time)
|
599 |
if time_fine_curvefit is not None and len(time_fine_curvefit)>0:
|
@@ -606,23 +521,20 @@ class BioprocessModel:
|
|
606 |
else:
|
607 |
y_pred_substrate_fine = np.full_like(time_fine_curvefit, np.nan)
|
608 |
|
609 |
-
|
610 |
if 'product' in self.params and self.params['product']:
|
611 |
product_params_values = list(self.params['product'].values())
|
612 |
y_pred_product_fine = self.product(time_fine_curvefit, *product_params_values, biomass_params_values)
|
613 |
else:
|
614 |
y_pred_product_fine = np.full_like(time_fine_curvefit, np.nan)
|
615 |
|
616 |
-
# Check if any fine predictions are all NaN
|
617 |
if not np.all(np.isnan(y_pred_biomass_fine)):
|
618 |
y_pred_biomass = y_pred_biomass_fine
|
619 |
-
time_to_plot = time_fine_curvefit
|
620 |
if not np.all(np.isnan(y_pred_substrate_fine)):
|
621 |
y_pred_substrate = y_pred_substrate_fine
|
622 |
if not np.all(np.isnan(y_pred_product_fine)):
|
623 |
y_pred_product = y_pred_product_fine
|
624 |
|
625 |
-
|
626 |
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 15))
|
627 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()})', fontsize=16)
|
628 |
|
@@ -636,11 +548,16 @@ class BioprocessModel:
|
|
636 |
]
|
637 |
|
638 |
for idx, (ax, data_exp, y_pred_model, data_std_exp, ylabel, model_name_legend, params_dict, r2_val, rmse_val) in enumerate(plots_config):
|
639 |
-
# Plot experimental data if available and not all NaN
|
640 |
if data_exp is not None and len(data_exp) > 0 and not np.all(np.isnan(data_exp)):
|
641 |
-
if data_std_exp is not None and len(data_std_exp) == len(data_exp) and not np.all(np.isnan(data_std_exp)):
|
642 |
-
ax.errorbar(
|
643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
644 |
else:
|
645 |
ax.plot(time, data_exp, marker=marker_style, linestyle='', color=point_color,
|
646 |
label='Datos experimentales')
|
@@ -649,11 +566,9 @@ class BioprocessModel:
|
|
649 |
horizontalalignment='center', verticalalignment='center',
|
650 |
transform=ax.transAxes, fontsize=10, color='gray')
|
651 |
|
652 |
-
|
653 |
-
# Plot model prediction if available and not all NaN
|
654 |
if y_pred_model is not None and len(y_pred_model) > 0 and not np.all(np.isnan(y_pred_model)):
|
655 |
ax.plot(time_to_plot, y_pred_model, linestyle=line_style, color=line_color, label=model_name_legend)
|
656 |
-
elif idx == 0 and y_pred_biomass is None:
|
657 |
ax.text(0.5, 0.6, 'Modelo de biomasa no ajustado.',
|
658 |
horizontalalignment='center', verticalalignment='center',
|
659 |
transform=ax.transAxes, fontsize=10, color='red')
|
@@ -667,7 +582,6 @@ class BioprocessModel:
|
|
667 |
horizontalalignment='center', verticalalignment='center',
|
668 |
transform=ax.transAxes, fontsize=10, color='orange')
|
669 |
|
670 |
-
|
671 |
ax.set_xlabel(axis_labels['x_label'])
|
672 |
ax.set_ylabel(ylabel)
|
673 |
if show_legend:
|
@@ -675,18 +589,16 @@ class BioprocessModel:
|
|
675 |
ax.set_title(f'{ylabel}')
|
676 |
|
677 |
if show_params and params_dict and all(isinstance(v, (int, float)) and np.isfinite(v) for v in params_dict.values()):
|
678 |
-
param_text = '\n'.join([f"{k} = {v:.3g}" for k, v in params_dict.items()])
|
679 |
-
# Ensure R2 and RMSE are finite for display
|
680 |
r2_display = f"{r2_val:.3f}" if np.isfinite(r2_val) else "N/A"
|
681 |
rmse_display = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else "N/A"
|
682 |
text = f"{param_text}\nR² = {r2_display}\nRMSE = {rmse_display}"
|
683 |
|
684 |
if params_position == 'outside right':
|
685 |
bbox_props = dict(boxstyle='round,pad=0.3', facecolor='wheat', alpha=0.5)
|
686 |
-
|
687 |
-
fig.subplots_adjust(right=0.75) # Make space for the annotation
|
688 |
ax.annotate(text, xy=(1.05, 0.5), xycoords='axes fraction',
|
689 |
-
xytext=(10,0), textcoords='offset points',
|
690 |
verticalalignment='center', horizontalalignment='left',
|
691 |
bbox=bbox_props)
|
692 |
else:
|
@@ -700,15 +612,12 @@ class BioprocessModel:
|
|
700 |
horizontalalignment='center', verticalalignment='center',
|
701 |
transform=ax.transAxes, fontsize=9, color='grey')
|
702 |
|
703 |
-
|
704 |
-
plt.tight_layout(rect=[0, 0.03, 1, 0.95]) # Adjust rect to accommodate suptitle
|
705 |
-
|
706 |
buf = io.BytesIO()
|
707 |
fig.savefig(buf, format='png', bbox_inches='tight')
|
708 |
buf.seek(0)
|
709 |
image = Image.open(buf).convert("RGB")
|
710 |
plt.close(fig)
|
711 |
-
|
712 |
return image
|
713 |
|
714 |
def plot_combined_results(self, time, biomass, substrate, product,
|
@@ -718,9 +627,9 @@ class BioprocessModel:
|
|
718 |
show_legend=True, show_params=True,
|
719 |
style='whitegrid',
|
720 |
line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
|
721 |
-
use_differential=False, axis_labels=None
|
|
|
722 |
|
723 |
-
# Similar checks as in plot_results
|
724 |
if y_pred_biomass is None and not use_differential:
|
725 |
print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type} (combinado). Omitiendo figura.")
|
726 |
return None
|
@@ -728,7 +637,6 @@ class BioprocessModel:
|
|
728 |
print(f"Se solicitó usar EDO (combinado) pero no hay parámetros de biomasa para {experiment_name}. Omitiendo EDO.")
|
729 |
use_differential = False
|
730 |
|
731 |
-
|
732 |
if axis_labels is None:
|
733 |
axis_labels = {
|
734 |
'x_label': 'Tiempo',
|
@@ -738,7 +646,7 @@ class BioprocessModel:
|
|
738 |
}
|
739 |
|
740 |
sns.set_style(style)
|
741 |
-
time_to_plot = time
|
742 |
|
743 |
if use_differential and 'biomass' in self.params and self.params['biomass']:
|
744 |
X_ode, S_ode, P_ode, time_fine_ode = self.solve_differential_equations(time, biomass, substrate, product)
|
@@ -747,8 +655,8 @@ class BioprocessModel:
|
|
747 |
time_to_plot = time_fine_ode
|
748 |
else:
|
749 |
print(f"Fallo al resolver EDOs para {experiment_name} (combinado), usando resultados de curve_fit si existen.")
|
750 |
-
time_to_plot = time
|
751 |
-
else:
|
752 |
if not use_differential and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
753 |
time_fine_curvefit = self.generate_fine_time_grid(time)
|
754 |
if time_fine_curvefit is not None and len(time_fine_curvefit)>0:
|
@@ -775,21 +683,25 @@ class BioprocessModel:
|
|
775 |
if not np.all(np.isnan(y_pred_product_fine)):
|
776 |
y_pred_product = y_pred_product_fine
|
777 |
|
778 |
-
|
779 |
-
fig, ax1 = plt.subplots(figsize=(12, 7)) # Increased width for params possibly outside
|
780 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()})', fontsize=16)
|
781 |
|
782 |
colors = {'Biomasa': 'blue', 'Sustrato': 'green', 'Producto': 'red'}
|
783 |
data_colors = {'Biomasa': 'darkblue', 'Sustrato': 'darkgreen', 'Producto': 'darkred'}
|
784 |
model_colors = {'Biomasa': 'cornflowerblue', 'Sustrato': 'limegreen', 'Producto': 'salmon'}
|
785 |
|
786 |
-
|
787 |
ax1.set_xlabel(axis_labels['x_label'])
|
788 |
ax1.set_ylabel(axis_labels['biomass_label'], color=colors['Biomasa'])
|
789 |
if biomass is not None and len(biomass) > 0 and not np.all(np.isnan(biomass)):
|
790 |
-
if biomass_std is not None and len(biomass_std) == len(biomass) and not np.all(np.isnan(biomass_std)):
|
791 |
-
ax1.errorbar(
|
792 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
793 |
else:
|
794 |
ax1.plot(time, biomass, marker=marker_style, linestyle='', color=data_colors['Biomasa'],
|
795 |
label=f'{axis_labels["biomass_label"]} (Datos)', markersize=5)
|
@@ -801,9 +713,15 @@ class BioprocessModel:
|
|
801 |
ax2 = ax1.twinx()
|
802 |
ax2.set_ylabel(axis_labels['substrate_label'], color=colors['Sustrato'])
|
803 |
if substrate is not None and len(substrate) > 0 and not np.all(np.isnan(substrate)):
|
804 |
-
if substrate_std is not None and len(substrate_std) == len(substrate) and not np.all(np.isnan(substrate_std)):
|
805 |
-
ax2.errorbar(
|
806 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
807 |
else:
|
808 |
ax2.plot(time, substrate, marker=marker_style, linestyle='', color=data_colors['Sustrato'],
|
809 |
label=f'{axis_labels["substrate_label"]} (Datos)', markersize=5)
|
@@ -813,16 +731,21 @@ class BioprocessModel:
|
|
813 |
ax2.tick_params(axis='y', labelcolor=colors['Sustrato'])
|
814 |
|
815 |
ax3 = ax1.twinx()
|
816 |
-
ax3.spines["right"].set_position(("axes", 1.15))
|
817 |
ax3.set_frame_on(True)
|
818 |
ax3.patch.set_visible(False)
|
819 |
|
820 |
-
|
821 |
ax3.set_ylabel(axis_labels['product_label'], color=colors['Producto'])
|
822 |
if product is not None and len(product) > 0 and not np.all(np.isnan(product)):
|
823 |
-
if product_std is not None and len(product_std) == len(product) and not np.all(np.isnan(product_std)):
|
824 |
-
ax3.errorbar(
|
825 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
826 |
else:
|
827 |
ax3.plot(time, product, marker=marker_style, linestyle='', color=data_colors['Producto'],
|
828 |
label=f'{axis_labels["product_label"]} (Datos)', markersize=5)
|
@@ -831,21 +754,18 @@ class BioprocessModel:
|
|
831 |
label=f'{axis_labels["product_label"]} (Modelo)')
|
832 |
ax3.tick_params(axis='y', labelcolor=colors['Producto'])
|
833 |
|
834 |
-
# Collect legends from all axes
|
835 |
lines_labels_collect = []
|
836 |
for ax_current in [ax1, ax2, ax3]:
|
837 |
h, l = ax_current.get_legend_handles_labels()
|
838 |
-
if h:
|
839 |
lines_labels_collect.append((h,l))
|
840 |
|
841 |
if lines_labels_collect:
|
842 |
-
lines, labels = [sum(lol, []) for lol in zip(*[(h,l) for h,l in lines_labels_collect])]
|
843 |
-
# Filter out duplicate labels for legend, keeping order
|
844 |
unique_labels_dict = dict(zip(labels, lines))
|
845 |
if show_legend:
|
846 |
ax1.legend(unique_labels_dict.values(), unique_labels_dict.keys(), loc=legend_position)
|
847 |
|
848 |
-
|
849 |
if show_params:
|
850 |
texts_to_display = []
|
851 |
param_categories = [
|
@@ -860,22 +780,18 @@ class BioprocessModel:
|
|
860 |
r2_display = f"{r2_val:.3f}" if np.isfinite(r2_val) else "N/A"
|
861 |
rmse_display = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else "N/A"
|
862 |
texts_to_display.append(f"{label}:\n{param_text}\n R² = {r2_display}\n RMSE = {rmse_display}")
|
863 |
-
elif params_dict:
|
864 |
texts_to_display.append(f"{label}:\n Parámetros no válidos o N/A")
|
865 |
-
# else: No params for this category, skip.
|
866 |
-
|
867 |
|
868 |
total_text = "\n\n".join(texts_to_display)
|
869 |
|
870 |
-
if total_text:
|
871 |
if params_position == 'outside right':
|
872 |
-
fig.subplots_adjust(right=0.70)
|
873 |
bbox_props = dict(boxstyle='round,pad=0.3', facecolor='wheat', alpha=0.7)
|
874 |
-
# Annotate relative to the figure, not a specific axis, for true "outside"
|
875 |
fig.text(0.72, 0.5, total_text, transform=fig.transFigure,
|
876 |
verticalalignment='center', horizontalalignment='left',
|
877 |
bbox=bbox_props, fontsize=8)
|
878 |
-
|
879 |
else:
|
880 |
text_x, ha = (0.95, 'right') if 'right' in params_position else (0.05, 'left')
|
881 |
text_y, va = (0.95, 'top') if 'upper' in params_position else (0.05, 'bottom')
|
@@ -884,39 +800,34 @@ class BioprocessModel:
|
|
884 |
bbox={'boxstyle':'round,pad=0.3', 'facecolor':'wheat', 'alpha':0.7}, fontsize=8)
|
885 |
|
886 |
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
887 |
-
# For combined plot, ensure right spine of ax3 is visible if params are outside
|
888 |
if params_position == 'outside right':
|
889 |
fig.subplots_adjust(right=0.70)
|
890 |
|
891 |
-
|
892 |
buf = io.BytesIO()
|
893 |
fig.savefig(buf, format='png', bbox_inches='tight')
|
894 |
buf.seek(0)
|
895 |
image = Image.open(buf).convert("RGB")
|
896 |
plt.close(fig)
|
897 |
-
|
898 |
return image
|
899 |
|
900 |
def process_all_data(file, legend_position, params_position, model_types_selected, experiment_names_str,
|
901 |
-
lower_bounds_str, upper_bounds_str,
|
902 |
mode, style, line_color, point_color, line_style, marker_style,
|
903 |
show_legend, show_params, use_differential, maxfev_val,
|
904 |
-
axis_labels_dict
|
|
|
905 |
|
906 |
if file is None:
|
907 |
return [], pd.DataFrame(), "Por favor, sube un archivo Excel."
|
908 |
|
909 |
try:
|
910 |
-
# Try reading with multi-index header first
|
911 |
try:
|
912 |
xls = pd.ExcelFile(file.name)
|
913 |
-
except AttributeError:
|
914 |
xls = pd.ExcelFile(file)
|
915 |
-
|
916 |
sheet_names = xls.sheet_names
|
917 |
if not sheet_names:
|
918 |
return [], pd.DataFrame(), "El archivo Excel está vacío o no contiene hojas."
|
919 |
-
|
920 |
except Exception as e:
|
921 |
return [], pd.DataFrame(), f"Error al leer el archivo Excel: {e}"
|
922 |
|
@@ -926,7 +837,6 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
926 |
experiment_names_list = experiment_names_str.strip().split('\n') if experiment_names_str.strip() else []
|
927 |
all_plot_messages = []
|
928 |
|
929 |
-
|
930 |
for sheet_name_idx, sheet_name in enumerate(sheet_names):
|
931 |
current_experiment_name_base = (experiment_names_list[sheet_name_idx]
|
932 |
if sheet_name_idx < len(experiment_names_list) and experiment_names_list[sheet_name_idx]
|
@@ -936,39 +846,27 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
936 |
if df.empty:
|
937 |
all_plot_messages.append(f"Hoja '{sheet_name}' está vacía.")
|
938 |
continue
|
939 |
-
# Basic validation of expected column structure (Tiempo, Biomasa, etc.)
|
940 |
if not any(col_level2 == 'Tiempo' for _, col_level2 in df.columns):
|
941 |
all_plot_messages.append(f"Hoja '{sheet_name}' no contiene la subcolumna 'Tiempo'. Saltando hoja.")
|
942 |
continue
|
943 |
-
|
944 |
except Exception as e:
|
945 |
all_plot_messages.append(f"Error al leer la hoja '{sheet_name}': {e}. Saltando hoja.")
|
946 |
continue
|
947 |
|
948 |
-
# Create a dummy model instance to process data for this sheet
|
949 |
model_dummy_for_sheet = BioprocessModel()
|
950 |
try:
|
951 |
model_dummy_for_sheet.process_data(df)
|
952 |
-
except ValueError as e:
|
953 |
all_plot_messages.append(f"Error procesando datos de la hoja '{sheet_name}': {e}. Saltando hoja.")
|
954 |
continue
|
955 |
|
956 |
-
time_exp_full = model_dummy_for_sheet.time # Time from the first experiment in the sheet usually
|
957 |
-
|
958 |
-
# INDEPENDENT MODE: Iterate through top-level columns (experiments)
|
959 |
if mode == 'independent':
|
960 |
-
# df.columns.levels[0] gives unique top-level column names
|
961 |
-
# However, direct iteration over df.columns.levels[0] might not align if some experiments are missing certain sub-columns.
|
962 |
-
# A safer way is to group by the first level of the column index.
|
963 |
grouped_cols = df.columns.get_level_values(0).unique()
|
964 |
-
|
965 |
for exp_idx, exp_col_name in enumerate(grouped_cols):
|
966 |
current_experiment_name = f"{current_experiment_name_base} - Exp {exp_idx + 1} ({exp_col_name})"
|
967 |
-
exp_df = df[exp_col_name]
|
968 |
-
|
969 |
try:
|
970 |
time_exp = exp_df['Tiempo'].dropna().values
|
971 |
-
# Ensure data is 1D array of numbers, handle potential errors
|
972 |
biomass_exp = exp_df['Biomasa'].dropna().astype(float).values if 'Biomasa' in exp_df else np.array([])
|
973 |
substrate_exp = exp_df['Sustrato'].dropna().astype(float).values if 'Sustrato' in exp_df else np.array([])
|
974 |
product_exp = exp_df['Producto'].dropna().astype(float).values if 'Producto' in exp_df else np.array([])
|
@@ -976,9 +874,8 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
976 |
if len(time_exp) == 0:
|
977 |
all_plot_messages.append(f"No hay datos de tiempo para {current_experiment_name}. Saltando.")
|
978 |
continue
|
979 |
-
if len(biomass_exp) == 0 :
|
980 |
all_plot_messages.append(f"No hay datos de biomasa para {current_experiment_name}. Saltando modelos para este experimento.")
|
981 |
-
# Still add to comparison_data as NaN
|
982 |
for model_type_iter in model_types_selected:
|
983 |
comparison_data.append({
|
984 |
'Experimento': current_experiment_name, 'Modelo': model_type_iter.capitalize(),
|
@@ -986,8 +883,6 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
986 |
**{f'RMSE {comp}': np.nan for comp in ['Biomasa', 'Sustrato', 'Producto']}
|
987 |
})
|
988 |
continue
|
989 |
-
|
990 |
-
|
991 |
except KeyError as e:
|
992 |
all_plot_messages.append(f"Faltan columnas (Tiempo, Biomasa, Sustrato, Producto) en '{current_experiment_name}': {e}. Saltando.")
|
993 |
continue
|
@@ -995,19 +890,13 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
995 |
all_plot_messages.append(f"Error extrayendo datos para '{current_experiment_name}': {e_data}. Saltando.")
|
996 |
continue
|
997 |
|
998 |
-
|
999 |
-
# For independent mode, standard deviation is not applicable unless replicates are within this exp_df
|
1000 |
-
# Assuming exp_df contains single replicate data here. If it has sub-columns for replicates,
|
1001 |
-
# then mean/std should be calculated here. For now, pass None for std.
|
1002 |
biomass_std_exp, substrate_std_exp, product_std_exp = None, None, None
|
1003 |
|
1004 |
for model_type_iter in model_types_selected:
|
1005 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
1006 |
-
model_instance.fit_model()
|
1007 |
-
|
1008 |
y_pred_biomass = model_instance.fit_biomass(time_exp, biomass_exp)
|
1009 |
y_pred_substrate, y_pred_product = None, None
|
1010 |
-
|
1011 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
1012 |
if len(substrate_exp) > 0 :
|
1013 |
y_pred_substrate = model_instance.fit_substrate(time_exp, substrate_exp, model_instance.params['biomass'])
|
@@ -1016,16 +905,11 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
1016 |
else:
|
1017 |
all_plot_messages.append(f"Ajuste de biomasa falló para {current_experiment_name} con modelo {model_type_iter}.")
|
1018 |
|
1019 |
-
|
1020 |
comparison_data.append({
|
1021 |
-
'Experimento': current_experiment_name,
|
1022 |
-
'
|
1023 |
-
'R²
|
1024 |
-
'RMSE
|
1025 |
-
'R² Sustrato': model_instance.r2.get('substrate', np.nan),
|
1026 |
-
'RMSE Sustrato': model_instance.rmse.get('substrate', np.nan),
|
1027 |
-
'R² Producto': model_instance.r2.get('product', np.nan),
|
1028 |
-
'RMSE Producto': model_instance.rmse.get('product', np.nan)
|
1029 |
})
|
1030 |
|
1031 |
fig = model_instance.plot_results(
|
@@ -1035,19 +919,17 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
1035 |
current_experiment_name, legend_position, params_position,
|
1036 |
show_legend, show_params, style,
|
1037 |
line_color, point_color, line_style, marker_style,
|
1038 |
-
use_differential, axis_labels_dict
|
|
|
|
|
|
|
1039 |
)
|
1040 |
if fig: figures.append(fig)
|
1041 |
experiment_counter +=1
|
1042 |
|
1043 |
-
|
1044 |
-
# AVERAGE or COMBINADO MODE: Use processed data (mean, std) from model_dummy_for_sheet
|
1045 |
elif mode in ['average', 'combinado']:
|
1046 |
current_experiment_name = f"{current_experiment_name_base} - Promedio"
|
1047 |
-
|
1048 |
-
# Data from model_dummy_for_sheet (which processed the whole sheet)
|
1049 |
-
# These are lists, take the last appended (corresponds to current sheet)
|
1050 |
-
time_avg = model_dummy_for_sheet.time # Should be consistent across sheet
|
1051 |
biomass_avg = model_dummy_for_sheet.dataxp[-1] if model_dummy_for_sheet.dataxp else np.array([])
|
1052 |
substrate_avg = model_dummy_for_sheet.datasp[-1] if model_dummy_for_sheet.datasp else np.array([])
|
1053 |
product_avg = model_dummy_for_sheet.datapp[-1] if model_dummy_for_sheet.datapp else np.array([])
|
@@ -1069,14 +951,11 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
1069 |
})
|
1070 |
continue
|
1071 |
|
1072 |
-
|
1073 |
for model_type_iter in model_types_selected:
|
1074 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
1075 |
model_instance.fit_model()
|
1076 |
-
|
1077 |
y_pred_biomass = model_instance.fit_biomass(time_avg, biomass_avg)
|
1078 |
y_pred_substrate, y_pred_product = None, None
|
1079 |
-
|
1080 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
1081 |
if len(substrate_avg) > 0:
|
1082 |
y_pred_substrate = model_instance.fit_substrate(time_avg, substrate_avg, model_instance.params['biomass'])
|
@@ -1085,16 +964,11 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
1085 |
else:
|
1086 |
all_plot_messages.append(f"Ajuste de biomasa promedio falló para {current_experiment_name} con modelo {model_type_iter}.")
|
1087 |
|
1088 |
-
|
1089 |
comparison_data.append({
|
1090 |
-
'Experimento': current_experiment_name,
|
1091 |
-
'
|
1092 |
-
'R²
|
1093 |
-
'RMSE
|
1094 |
-
'R² Sustrato': model_instance.r2.get('substrate', np.nan),
|
1095 |
-
'RMSE Sustrato': model_instance.rmse.get('substrate', np.nan),
|
1096 |
-
'R² Producto': model_instance.r2.get('product', np.nan),
|
1097 |
-
'RMSE Producto': model_instance.rmse.get('product', np.nan)
|
1098 |
})
|
1099 |
|
1100 |
plot_func = model_instance.plot_combined_results if mode == 'combinado' else model_instance.plot_results
|
@@ -1105,25 +979,25 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
1105 |
current_experiment_name, legend_position, params_position,
|
1106 |
show_legend, show_params, style,
|
1107 |
line_color, point_color, line_style, marker_style,
|
1108 |
-
use_differential, axis_labels_dict
|
|
|
|
|
|
|
1109 |
)
|
1110 |
if fig: figures.append(fig)
|
1111 |
experiment_counter +=1
|
1112 |
|
1113 |
-
|
1114 |
comparison_df = pd.DataFrame(comparison_data)
|
1115 |
if not comparison_df.empty:
|
1116 |
-
# Ensure numeric columns for sorting, coerce errors to NaN
|
1117 |
for col in ['R² Biomasa', 'RMSE Biomasa', 'R² Sustrato', 'RMSE Sustrato', 'R² Producto', 'RMSE Producto']:
|
1118 |
if col in comparison_df.columns:
|
1119 |
comparison_df[col] = pd.to_numeric(comparison_df[col], errors='coerce')
|
1120 |
-
|
1121 |
comparison_df_sorted = comparison_df.sort_values(
|
1122 |
by=['Experimento', 'Modelo', 'R² Biomasa', 'R² Sustrato', 'R² Producto', 'RMSE Biomasa', 'RMSE Sustrato', 'RMSE Producto'],
|
1123 |
-
ascending=[True, True, False, False, False, True, True, True]
|
1124 |
).reset_index(drop=True)
|
1125 |
else:
|
1126 |
-
comparison_df_sorted = pd.DataFrame(columns=[
|
1127 |
'Experimento', 'Modelo', 'R² Biomasa', 'RMSE Biomasa',
|
1128 |
'R² Sustrato', 'RMSE Sustrato', 'R² Producto', 'RMSE Producto'
|
1129 |
])
|
@@ -1135,11 +1009,8 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
1135 |
final_message += "\nNo se generaron gráficos, pero hay datos en la tabla."
|
1136 |
elif not figures and comparison_df_sorted.empty:
|
1137 |
final_message += "\nNo se generaron gráficos ni datos para la tabla."
|
1138 |
-
|
1139 |
-
|
1140 |
return figures, comparison_df_sorted, final_message
|
1141 |
|
1142 |
-
|
1143 |
def create_interface():
|
1144 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
1145 |
gr.Markdown("# Modelos Cinéticos de Bioprocesos")
|
@@ -1252,8 +1123,8 @@ def create_interface():
|
|
1252 |
with gr.Row():
|
1253 |
style_dropdown = gr.Dropdown(choices=['white', 'dark', 'whitegrid', 'darkgrid', 'ticks'],
|
1254 |
label="Estilo de Gráfico (Seaborn)", value='whitegrid')
|
1255 |
-
line_color_picker = gr.ColorPicker(label="Color de Línea (Modelo)", value='#0072B2')
|
1256 |
-
point_color_picker = gr.ColorPicker(label="Color de Puntos (Datos)", value='#D55E00')
|
1257 |
|
1258 |
with gr.Row():
|
1259 |
line_style_dropdown = gr.Dropdown(choices=['-', '--', '-.', ':'], label="Estilo de Línea", value='-')
|
@@ -1265,37 +1136,37 @@ def create_interface():
|
|
1265 |
with gr.Row():
|
1266 |
substrate_axis_label_input = gr.Textbox(label="Título Eje Y (Sustrato)", value="Sustrato (g/L)", placeholder="Sustrato (unidades)")
|
1267 |
product_axis_label_input = gr.Textbox(label="Título Eje Y (Producto)", value="Producto (g/L)", placeholder="Producto (unidades)")
|
|
|
|
|
|
|
|
|
|
|
|
|
1268 |
|
1269 |
-
|
1270 |
-
# Lower/Upper bounds are not currently used by the curve_fit in BioprocessModel,
|
1271 |
-
# but kept here for potential future implementation.
|
1272 |
with gr.Accordion("Configuración Avanzada de Ajuste (No implementado aún)", open=False):
|
1273 |
with gr.Row():
|
1274 |
lower_bounds_str = gr.Textbox(label="Lower Bounds (no usado actualmente)", lines=3)
|
1275 |
upper_bounds_str = gr.Textbox(label="Upper Bounds (no usado actualmente)", lines=3)
|
1276 |
|
1277 |
simulate_btn = gr.Button("Simular y Graficar", variant="primary")
|
1278 |
-
|
1279 |
status_message = gr.Textbox(label="Estado del Procesamiento", interactive=False)
|
1280 |
-
|
1281 |
output_gallery = gr.Gallery(label="Resultados Gráficos", columns=[2,1], height='auto', object_fit="contain")
|
1282 |
-
# Change the gr.Dataframe initialization
|
1283 |
output_table = gr.Dataframe(
|
1284 |
label="Tabla Comparativa de Modelos (Ordenada por R² Biomasa Descendente)",
|
1285 |
headers=["Experimento", "Modelo", "R² Biomasa", "RMSE Biomasa",
|
1286 |
"R² Sustrato", "RMSE Sustrato", "R² Producto", "RMSE Producto"],
|
1287 |
-
interactive=False, wrap=True
|
1288 |
)
|
1289 |
-
|
1290 |
-
state_df = gr.State(pd.DataFrame()) # To store the dataframe for export
|
1291 |
|
1292 |
def run_simulation_interface(file, legend_pos, params_pos, models_sel, analysis_mode, exp_names,
|
1293 |
low_bounds, up_bounds, plot_style,
|
1294 |
line_col, point_col, line_sty, marker_sty,
|
1295 |
show_leg, show_par, use_diff, maxfev,
|
1296 |
-
x_label, biomass_label, substrate_label, product_label
|
|
|
1297 |
if file is None:
|
1298 |
-
return [], pd.DataFrame(), "Error: Por favor, sube un archivo Excel."
|
1299 |
|
1300 |
axis_labels = {
|
1301 |
'x_label': x_label if x_label else 'Tiempo',
|
@@ -1304,18 +1175,18 @@ def create_interface():
|
|
1304 |
'product_label': product_label if product_label else 'Producto'
|
1305 |
}
|
1306 |
|
1307 |
-
if not models_sel:
|
1308 |
-
return [], pd.DataFrame(), "Error: Por favor, selecciona al menos un tipo de modelo de biomasa."
|
1309 |
-
|
1310 |
|
1311 |
figures, comparison_df, message = process_all_data(
|
1312 |
file, legend_pos, params_pos, models_sel, exp_names,
|
1313 |
low_bounds, up_bounds, analysis_mode, plot_style,
|
1314 |
line_col, point_col, line_sty, marker_sty,
|
1315 |
show_leg, show_par, use_diff, int(maxfev),
|
1316 |
-
axis_labels
|
|
|
1317 |
)
|
1318 |
-
return figures, comparison_df, message, comparison_df
|
1319 |
|
1320 |
simulate_btn.click(
|
1321 |
fn=run_simulation_interface,
|
@@ -1324,62 +1195,52 @@ def create_interface():
|
|
1324 |
lower_bounds_str, upper_bounds_str, style_dropdown,
|
1325 |
line_color_picker, point_color_picker, line_style_dropdown, marker_style_dropdown,
|
1326 |
show_legend, show_params, use_differential, maxfev_input,
|
1327 |
-
x_axis_label_input, biomass_axis_label_input, substrate_axis_label_input, product_axis_label_input
|
|
|
1328 |
],
|
1329 |
outputs=[output_gallery, output_table, status_message, state_df]
|
1330 |
)
|
1331 |
|
1332 |
def export_excel_interface(df_to_export):
|
1333 |
if df_to_export is None or df_to_export.empty:
|
1334 |
-
# Create a temporary empty file to satisfy Gradio's file output expectation
|
1335 |
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
1336 |
tmp.write(b"No hay datos para exportar.")
|
1337 |
-
return tmp.name
|
1338 |
-
# Alternatively, raise an error or return a specific message if Gradio handles None better
|
1339 |
-
# For now, returning a dummy file path is safer.
|
1340 |
-
|
1341 |
try:
|
1342 |
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False, mode='w+b') as tmp:
|
1343 |
df_to_export.to_excel(tmp.name, index=False)
|
1344 |
return tmp.name
|
1345 |
except Exception as e:
|
1346 |
-
# print(f"Error al exportar a Excel: {e}")
|
1347 |
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
1348 |
tmp.write(f"Error al exportar a Excel: {e}".encode())
|
1349 |
return tmp.name
|
1350 |
|
1351 |
-
|
1352 |
export_btn = gr.Button("Exportar Tabla a Excel")
|
1353 |
download_file_output = gr.File(label="Descargar archivo Excel", interactive=False)
|
1354 |
|
1355 |
export_btn.click(
|
1356 |
fn=export_excel_interface,
|
1357 |
-
inputs=state_df,
|
1358 |
outputs=download_file_output
|
1359 |
)
|
1360 |
|
1361 |
gr.Examples(
|
1362 |
examples=[
|
1363 |
-
[None, "best", "upper right", ["logistic"], "independent", "Exp A\nExp B", "", "", "whitegrid", "#0072B2", "#D55E00", "-", "o", True, True, False, 50000, "Tiempo (días)", "Células (millones/mL)", "Glucosa (mM)", "Anticuerpo (mg/L)"]
|
1364 |
],
|
1365 |
inputs=[
|
1366 |
file_input, legend_position, params_position, model_types_selected, mode, experiment_names_str,
|
1367 |
lower_bounds_str, upper_bounds_str, style_dropdown,
|
1368 |
line_color_picker, point_color_picker, line_style_dropdown, marker_style_dropdown,
|
1369 |
show_legend, show_params, use_differential, maxfev_input,
|
1370 |
-
x_axis_label_input, biomass_axis_label_input, substrate_axis_label_input, product_axis_label_input
|
|
|
1371 |
],
|
1372 |
label="Ejemplo de Configuración (subir archivo manualmente)"
|
1373 |
)
|
1374 |
-
|
1375 |
-
|
1376 |
return demo
|
1377 |
|
1378 |
if __name__ == '__main__':
|
1379 |
-
# For local execution without explicit share=True, Gradio might choose a local URL.
|
1380 |
-
# share=True is useful for Colab or when needing external access.
|
1381 |
-
# For robust execution, explicitly manage the server if needed.
|
1382 |
-
# Check if running in a Google Colab environment
|
1383 |
try:
|
1384 |
import google.colab
|
1385 |
IN_COLAB = True
|
@@ -1387,5 +1248,4 @@ if __name__ == '__main__':
|
|
1387 |
IN_COLAB = False
|
1388 |
|
1389 |
demo_instance = create_interface()
|
1390 |
-
|
1391 |
-
demo_instance.launch(share=True) # Force share for testing purposes
|
|
|
41 |
|
42 |
@staticmethod
|
43 |
def logistic(time, xo, xm, um):
|
44 |
+
if xm == 0 or (xo / xm == 1 and np.any(um * time > 0)):
|
45 |
+
return np.full_like(time, np.nan)
|
|
|
|
|
46 |
denominator = (1 - (xo / xm) * (1 - np.exp(um * time)))
|
47 |
+
denominator = np.where(denominator == 0, 1e-9, denominator)
|
48 |
return (xo * np.exp(um * time)) / denominator
|
49 |
|
|
|
50 |
@staticmethod
|
51 |
def gompertz(time, xm, um, lag):
|
|
|
52 |
if xm == 0:
|
53 |
return np.full_like(time, np.nan)
|
54 |
return xm * np.exp(-np.exp((um * np.e / xm) * (lag - time) + 1))
|
|
|
60 |
@staticmethod
|
61 |
def logistic_diff(X, t, params):
|
62 |
xo, xm, um = params
|
63 |
+
if xm == 0:
|
64 |
return 0
|
65 |
return um * X * (1 - X / xm)
|
66 |
|
67 |
@staticmethod
|
68 |
def gompertz_diff(X, t, params):
|
69 |
xm, um, lag = params
|
70 |
+
if xm == 0:
|
71 |
return 0
|
72 |
return X * (um * np.e / xm) * np.exp((um * np.e / xm) * (lag - t) + 1)
|
73 |
|
|
|
80 |
if self.biomass_model is None or not biomass_params:
|
81 |
return np.full_like(time, np.nan)
|
82 |
X_t = self.biomass_model(time, *biomass_params)
|
83 |
+
if np.any(np.isnan(X_t)):
|
84 |
return np.full_like(time, np.nan)
|
|
|
|
|
|
|
85 |
integral_X = np.zeros_like(X_t)
|
86 |
if len(time) > 1:
|
87 |
+
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
88 |
integral_X = np.cumsum(X_t * dt)
|
89 |
|
|
|
|
|
|
|
90 |
if self.model_type == 'logistic':
|
91 |
X0 = biomass_params[0]
|
92 |
elif self.model_type == 'gompertz':
|
|
|
93 |
X0 = self.gompertz(0, *biomass_params)
|
94 |
elif self.model_type == 'moser':
|
|
|
95 |
X0 = self.moser(0, *biomass_params)
|
96 |
else:
|
97 |
+
X0 = X_t[0]
|
|
|
98 |
return so - p * (X_t - X0) - q * integral_X
|
99 |
|
|
|
100 |
def product(self, time, po, alpha, beta, biomass_params):
|
101 |
if self.biomass_model is None or not biomass_params:
|
102 |
return np.full_like(time, np.nan)
|
103 |
X_t = self.biomass_model(time, *biomass_params)
|
104 |
+
if np.any(np.isnan(X_t)):
|
105 |
return np.full_like(time, np.nan)
|
|
|
|
|
106 |
integral_X = np.zeros_like(X_t)
|
107 |
if len(time) > 1:
|
108 |
+
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
109 |
integral_X = np.cumsum(X_t * dt)
|
110 |
|
111 |
if self.model_type == 'logistic':
|
|
|
116 |
X0 = self.moser(0, *biomass_params)
|
117 |
else:
|
118 |
X0 = X_t[0]
|
|
|
119 |
return po + alpha * (X_t - X0) + beta * integral_X
|
120 |
|
121 |
def process_data(self, df):
|
|
|
134 |
self.datax.append(data_biomass)
|
135 |
self.dataxp.append(np.mean(data_biomass, axis=0))
|
136 |
self.datax_std.append(np.std(data_biomass, axis=0, ddof=1))
|
137 |
+
else:
|
138 |
self.datax.append(np.array([]))
|
139 |
self.dataxp.append(np.array([]))
|
140 |
self.datax_std.append(np.array([]))
|
141 |
|
|
|
142 |
if len(substrate_cols) > 0:
|
143 |
data_substrate = [df[col].values for col in substrate_cols]
|
144 |
data_substrate = np.array(data_substrate)
|
|
|
160 |
self.datap.append(np.array([]))
|
161 |
self.datapp.append(np.array([]))
|
162 |
self.datap_std.append(np.array([]))
|
|
|
|
|
163 |
self.time = time
|
164 |
|
165 |
def fit_model(self):
|
|
|
175 |
|
176 |
def fit_biomass(self, time, biomass):
|
177 |
try:
|
178 |
+
if len(np.unique(biomass)) < 2 :
|
|
|
179 |
print(f"Biomasa constante para {self.model_type}, no se puede ajustar el modelo.")
|
180 |
return None
|
181 |
|
182 |
if self.model_type == 'logistic':
|
|
|
|
|
183 |
xo_guess = biomass[biomass > 1e-6][0] if np.any(biomass > 1e-6) else 1e-3
|
184 |
xm_guess = max(biomass) * 1.1 if max(biomass) > xo_guess else xo_guess * 2
|
185 |
+
if xm_guess <= xo_guess: xm_guess = xo_guess + 1e-3
|
186 |
p0 = [xo_guess, xm_guess, 0.1]
|
187 |
bounds = ([1e-9, 1e-9, 1e-9], [np.inf, np.inf, np.inf])
|
188 |
popt, _ = curve_fit(self.logistic, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9)
|
|
|
189 |
if popt[1] <= popt[0]:
|
190 |
print(f"Advertencia: En modelo logístico, Xm ({popt[1]:.2f}) no es mayor que Xo ({popt[0]:.2f}). Ajuste puede no ser válido.")
|
|
|
191 |
self.params['biomass'] = {'xo': popt[0], 'xm': popt[1], 'um': popt[2]}
|
192 |
y_pred = self.logistic(time, *popt)
|
193 |
|
194 |
elif self.model_type == 'gompertz':
|
195 |
xm_guess = max(biomass) if max(biomass) > 0 else 1.0
|
196 |
um_guess = 0.1
|
|
|
|
|
197 |
lag_guess = time[np.argmax(np.gradient(biomass))] if len(biomass) > 1 and np.any(np.gradient(biomass) > 1e-6) else time[0]
|
198 |
p0 = [xm_guess, um_guess, lag_guess]
|
199 |
+
bounds = ([1e-9, 1e-9, 0], [np.inf, np.inf, max(time) if len(time)>0 else 100])
|
200 |
popt, _ = curve_fit(self.gompertz, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9)
|
201 |
self.params['biomass'] = {'xm': popt[0], 'um': popt[1], 'lag': popt[2]}
|
202 |
y_pred = self.gompertz(time, *popt)
|
|
|
204 |
elif self.model_type == 'moser':
|
205 |
Xm_guess = max(biomass) if max(biomass) > 0 else 1.0
|
206 |
um_guess = 0.1
|
207 |
+
Ks_guess = time[0]
|
208 |
p0 = [Xm_guess, um_guess, Ks_guess]
|
|
|
209 |
bounds = ([1e-9, 1e-9, -np.inf], [np.inf, np.inf, max(time) if len(time)>0 else 100])
|
210 |
popt, _ = curve_fit(self.moser, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9)
|
211 |
self.params['biomass'] = {'Xm': popt[0], 'um': popt[1], 'Ks': popt[2]}
|
|
|
219 |
self.rmse['biomass'] = np.nan
|
220 |
return None
|
221 |
|
|
|
222 |
ss_res = np.sum((biomass - y_pred) ** 2)
|
223 |
ss_tot = np.sum((biomass - np.mean(biomass)) ** 2)
|
224 |
+
if ss_tot == 0:
|
225 |
+
self.r2['biomass'] = 1.0 if ss_res == 0 else 0.0
|
226 |
else:
|
227 |
self.r2['biomass'] = 1 - (ss_res / ss_tot)
|
228 |
self.rmse['biomass'] = np.sqrt(mean_squared_error(biomass, y_pred))
|
229 |
return y_pred
|
230 |
except RuntimeError as e:
|
231 |
print(f"Error de Runtime en fit_biomass_{self.model_type} (probablemente no se pudo ajustar): {e}")
|
232 |
+
self.params['biomass'] = {}
|
233 |
self.r2['biomass'] = np.nan
|
234 |
self.rmse['biomass'] = np.nan
|
235 |
return None
|
|
|
241 |
return None
|
242 |
|
243 |
def fit_substrate(self, time, substrate, biomass_params_dict):
|
244 |
+
if not biomass_params_dict:
|
245 |
print(f"Error en fit_substrate_{self.model_type}: Parámetros de biomasa no disponibles.")
|
246 |
return None
|
247 |
try:
|
|
|
248 |
if self.model_type == 'logistic':
|
249 |
biomass_params_values = [biomass_params_dict['xo'], biomass_params_dict['xm'], biomass_params_dict['um']]
|
250 |
elif self.model_type == 'gompertz':
|
|
|
255 |
return None
|
256 |
|
257 |
so_guess = substrate[0] if len(substrate) > 0 else 1.0
|
258 |
+
p_guess = 0.1
|
259 |
+
q_guess = 0.01
|
260 |
p0 = [so_guess, p_guess, q_guess]
|
261 |
+
bounds = ([0, 0, 0], [np.inf, np.inf, np.inf])
|
262 |
|
|
|
263 |
popt, _ = curve_fit(
|
264 |
lambda t, so, p, q: self.substrate(t, so, p, q, biomass_params_values),
|
265 |
time, substrate, p0=p0, maxfev=self.maxfev, bounds=bounds, ftol=1e-9, xtol=1e-9
|
|
|
309 |
return None
|
310 |
|
311 |
po_guess = product[0] if len(product) > 0 else 0.0
|
312 |
+
alpha_guess = 0.1
|
313 |
+
beta_guess = 0.01
|
314 |
p0 = [po_guess, alpha_guess, beta_guess]
|
315 |
+
bounds = ([0, 0, 0], [np.inf, np.inf, np.inf])
|
316 |
|
317 |
popt, _ = curve_fit(
|
318 |
lambda t, po, alpha, beta: self.product(t, po, alpha, beta, biomass_params_values),
|
|
|
350 |
|
351 |
def generate_fine_time_grid(self, time):
|
352 |
if time is None or len(time) == 0:
|
353 |
+
return np.array([0])
|
354 |
time_fine = np.linspace(time.min(), time.max(), 500)
|
355 |
return time_fine
|
356 |
|
357 |
def system(self, y, t, biomass_params_list, substrate_params_list, product_params_list, model_type):
|
358 |
+
X, S, P = y
|
359 |
|
|
|
360 |
if model_type == 'logistic':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
361 |
dXdt = self.logistic_diff(X, t, biomass_params_list)
|
362 |
elif model_type == 'gompertz':
|
|
|
363 |
dXdt = self.gompertz_diff(X, t, biomass_params_list)
|
364 |
elif model_type == 'moser':
|
|
|
365 |
dXdt = self.moser_diff(X, t, biomass_params_list)
|
366 |
else:
|
367 |
+
dXdt = 0.0
|
368 |
|
|
|
|
|
|
|
|
|
369 |
p_val = substrate_params_list[1] if len(substrate_params_list) > 1 else 0
|
370 |
q_val = substrate_params_list[2] if len(substrate_params_list) > 2 else 0
|
371 |
dSdt = -p_val * dXdt - q_val * X
|
372 |
|
|
|
|
|
|
|
|
|
373 |
alpha_val = product_params_list[1] if len(product_params_list) > 1 else 0
|
374 |
beta_val = product_params_list[2] if len(product_params_list) > 2 else 0
|
375 |
dPdt = alpha_val * dXdt + beta_val * X
|
376 |
|
377 |
return [dXdt, dSdt, dPdt]
|
378 |
|
|
|
379 |
def get_initial_conditions(self, time, biomass, substrate, product):
|
|
|
380 |
X0_exp = biomass[0] if len(biomass) > 0 else 0
|
381 |
S0_exp = substrate[0] if len(substrate) > 0 else 0
|
382 |
P0_exp = product[0] if len(product) > 0 else 0
|
383 |
|
|
|
384 |
if 'biomass' in self.params and self.params['biomass']:
|
385 |
if self.model_type == 'logistic':
|
|
|
386 |
X0 = self.params['biomass'].get('xo', X0_exp)
|
387 |
elif self.model_type == 'gompertz':
|
|
|
388 |
xm = self.params['biomass'].get('xm', 1)
|
389 |
um = self.params['biomass'].get('um', 0.1)
|
390 |
lag = self.params['biomass'].get('lag', 0)
|
391 |
+
X0 = self.gompertz(0, xm, um, lag)
|
392 |
+
if np.isnan(X0): X0 = X0_exp
|
393 |
elif self.model_type == 'moser':
|
|
|
394 |
Xm_param = self.params['biomass'].get('Xm', 1)
|
395 |
um_param = self.params['biomass'].get('um', 0.1)
|
396 |
Ks_param = self.params['biomass'].get('Ks', 0)
|
397 |
+
X0 = self.moser(0, Xm_param, um_param, Ks_param)
|
398 |
+
if np.isnan(X0): X0 = X0_exp
|
399 |
else:
|
400 |
+
X0 = X0_exp
|
401 |
else:
|
402 |
X0 = X0_exp
|
403 |
|
|
|
404 |
if 'substrate' in self.params and self.params['substrate']:
|
|
|
405 |
S0 = self.params['substrate'].get('so', S0_exp)
|
406 |
else:
|
407 |
S0 = S0_exp
|
408 |
|
|
|
409 |
if 'product' in self.params and self.params['product']:
|
|
|
410 |
P0 = self.params['product'].get('po', P0_exp)
|
411 |
else:
|
412 |
P0 = P0_exp
|
413 |
|
|
|
414 |
X0 = X0 if not np.isnan(X0) else 0.0
|
415 |
S0 = S0 if not np.isnan(S0) else 0.0
|
416 |
P0 = P0 if not np.isnan(P0) else 0.0
|
|
|
421 |
if 'biomass' not in self.params or not self.params['biomass']:
|
422 |
print("No hay parámetros de biomasa, no se pueden resolver las EDO.")
|
423 |
return None, None, None, time
|
424 |
+
if time is None or len(time) == 0 :
|
425 |
print("Tiempo no válido para resolver EDOs.")
|
426 |
return None, None, None, np.array([])
|
427 |
|
|
|
|
|
|
|
|
|
|
|
428 |
if self.model_type == 'logistic':
|
|
|
429 |
biomass_params_list = [self.params['biomass']['xo'], self.params['biomass']['xm'], self.params['biomass']['um']]
|
430 |
elif self.model_type == 'gompertz':
|
|
|
431 |
biomass_params_list = [self.params['biomass']['xm'], self.params['biomass']['um'], self.params['biomass']['lag']]
|
432 |
elif self.model_type == 'moser':
|
|
|
433 |
biomass_params_list = [self.params['biomass']['Xm'], self.params['biomass']['um'], self.params['biomass']['Ks']]
|
434 |
else:
|
435 |
print(f"Tipo de modelo de biomasa desconocido: {self.model_type}")
|
436 |
return None, None, None, time
|
437 |
|
|
|
|
|
|
|
438 |
substrate_params_list = [
|
439 |
self.params.get('substrate', {}).get('so', 0),
|
440 |
self.params.get('substrate', {}).get('p', 0),
|
441 |
self.params.get('substrate', {}).get('q', 0)
|
442 |
]
|
|
|
|
|
|
|
|
|
443 |
product_params_list = [
|
444 |
self.params.get('product', {}).get('po', 0),
|
445 |
self.params.get('product', {}).get('alpha', 0),
|
|
|
455 |
try:
|
456 |
sol = odeint(self.system, initial_conditions, time_fine,
|
457 |
args=(biomass_params_list, substrate_params_list, product_params_list, self.model_type),
|
458 |
+
rtol=1e-6, atol=1e-6)
|
459 |
except Exception as e:
|
460 |
print(f"Error al resolver EDOs con odeint: {e}")
|
|
|
461 |
try:
|
462 |
print("Intentando con método 'lsoda'...")
|
463 |
sol = odeint(self.system, initial_conditions, time_fine,
|
|
|
467 |
print(f"Error al resolver EDOs con odeint (método lsoda): {e_lsoda}")
|
468 |
return None, None, None, time_fine
|
469 |
|
|
|
470 |
X = sol[:, 0]
|
471 |
S = sol[:, 1]
|
472 |
P = sol[:, 2]
|
|
|
473 |
return X, S, P, time_fine
|
474 |
|
475 |
def plot_results(self, time, biomass, substrate, product,
|
|
|
479 |
show_legend=True, show_params=True,
|
480 |
style='whitegrid',
|
481 |
line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
|
482 |
+
use_differential=False, axis_labels=None,
|
483 |
+
show_error_bars=True, error_cap_size=3, error_line_width=1): # Added error bar parameters
|
484 |
|
485 |
+
if y_pred_biomass is None and not use_differential:
|
486 |
print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type} y no se usan EDO. Omitiendo figura.")
|
487 |
return None
|
488 |
if use_differential and ('biomass' not in self.params or not self.params['biomass']):
|
489 |
print(f"Se solicitó usar EDO pero no hay parámetros de biomasa para {experiment_name}. Omitiendo EDO.")
|
490 |
+
use_differential = False
|
|
|
491 |
|
|
|
492 |
if axis_labels is None:
|
493 |
axis_labels = {
|
494 |
'x_label': 'Tiempo',
|
|
|
498 |
}
|
499 |
|
500 |
sns.set_style(style)
|
501 |
+
time_to_plot = time
|
502 |
|
503 |
if use_differential and 'biomass' in self.params and self.params['biomass']:
|
504 |
X_ode, S_ode, P_ode, time_fine_ode = self.solve_differential_equations(time, biomass, substrate, product)
|
505 |
if X_ode is not None:
|
506 |
y_pred_biomass, y_pred_substrate, y_pred_product = X_ode, S_ode, P_ode
|
507 |
+
time_to_plot = time_fine_ode
|
508 |
else:
|
509 |
print(f"Fallo al resolver EDOs para {experiment_name}, usando resultados de curve_fit si existen.")
|
510 |
+
time_to_plot = time
|
|
|
511 |
else:
|
|
|
|
|
|
|
512 |
if not use_differential and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
513 |
time_fine_curvefit = self.generate_fine_time_grid(time)
|
514 |
if time_fine_curvefit is not None and len(time_fine_curvefit)>0:
|
|
|
521 |
else:
|
522 |
y_pred_substrate_fine = np.full_like(time_fine_curvefit, np.nan)
|
523 |
|
|
|
524 |
if 'product' in self.params and self.params['product']:
|
525 |
product_params_values = list(self.params['product'].values())
|
526 |
y_pred_product_fine = self.product(time_fine_curvefit, *product_params_values, biomass_params_values)
|
527 |
else:
|
528 |
y_pred_product_fine = np.full_like(time_fine_curvefit, np.nan)
|
529 |
|
|
|
530 |
if not np.all(np.isnan(y_pred_biomass_fine)):
|
531 |
y_pred_biomass = y_pred_biomass_fine
|
532 |
+
time_to_plot = time_fine_curvefit
|
533 |
if not np.all(np.isnan(y_pred_substrate_fine)):
|
534 |
y_pred_substrate = y_pred_substrate_fine
|
535 |
if not np.all(np.isnan(y_pred_product_fine)):
|
536 |
y_pred_product = y_pred_product_fine
|
537 |
|
|
|
538 |
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 15))
|
539 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()})', fontsize=16)
|
540 |
|
|
|
548 |
]
|
549 |
|
550 |
for idx, (ax, data_exp, y_pred_model, data_std_exp, ylabel, model_name_legend, params_dict, r2_val, rmse_val) in enumerate(plots_config):
|
|
|
551 |
if data_exp is not None and len(data_exp) > 0 and not np.all(np.isnan(data_exp)):
|
552 |
+
if show_error_bars and data_std_exp is not None and len(data_std_exp) == len(data_exp) and not np.all(np.isnan(data_std_exp)):
|
553 |
+
ax.errorbar(
|
554 |
+
time, data_exp, yerr=data_std_exp,
|
555 |
+
fmt=marker_style, color=point_color,
|
556 |
+
label='Datos experimentales',
|
557 |
+
capsize=error_cap_size,
|
558 |
+
elinewidth=error_line_width,
|
559 |
+
markeredgewidth=1
|
560 |
+
)
|
561 |
else:
|
562 |
ax.plot(time, data_exp, marker=marker_style, linestyle='', color=point_color,
|
563 |
label='Datos experimentales')
|
|
|
566 |
horizontalalignment='center', verticalalignment='center',
|
567 |
transform=ax.transAxes, fontsize=10, color='gray')
|
568 |
|
|
|
|
|
569 |
if y_pred_model is not None and len(y_pred_model) > 0 and not np.all(np.isnan(y_pred_model)):
|
570 |
ax.plot(time_to_plot, y_pred_model, linestyle=line_style, color=line_color, label=model_name_legend)
|
571 |
+
elif idx == 0 and y_pred_biomass is None:
|
572 |
ax.text(0.5, 0.6, 'Modelo de biomasa no ajustado.',
|
573 |
horizontalalignment='center', verticalalignment='center',
|
574 |
transform=ax.transAxes, fontsize=10, color='red')
|
|
|
582 |
horizontalalignment='center', verticalalignment='center',
|
583 |
transform=ax.transAxes, fontsize=10, color='orange')
|
584 |
|
|
|
585 |
ax.set_xlabel(axis_labels['x_label'])
|
586 |
ax.set_ylabel(ylabel)
|
587 |
if show_legend:
|
|
|
589 |
ax.set_title(f'{ylabel}')
|
590 |
|
591 |
if show_params and params_dict and all(isinstance(v, (int, float)) and np.isfinite(v) for v in params_dict.values()):
|
592 |
+
param_text = '\n'.join([f"{k} = {v:.3g}" for k, v in params_dict.items()])
|
|
|
593 |
r2_display = f"{r2_val:.3f}" if np.isfinite(r2_val) else "N/A"
|
594 |
rmse_display = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else "N/A"
|
595 |
text = f"{param_text}\nR² = {r2_display}\nRMSE = {rmse_display}"
|
596 |
|
597 |
if params_position == 'outside right':
|
598 |
bbox_props = dict(boxstyle='round,pad=0.3', facecolor='wheat', alpha=0.5)
|
599 |
+
fig.subplots_adjust(right=0.75)
|
|
|
600 |
ax.annotate(text, xy=(1.05, 0.5), xycoords='axes fraction',
|
601 |
+
xytext=(10,0), textcoords='offset points',
|
602 |
verticalalignment='center', horizontalalignment='left',
|
603 |
bbox=bbox_props)
|
604 |
else:
|
|
|
612 |
horizontalalignment='center', verticalalignment='center',
|
613 |
transform=ax.transAxes, fontsize=9, color='grey')
|
614 |
|
615 |
+
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
|
|
|
|
616 |
buf = io.BytesIO()
|
617 |
fig.savefig(buf, format='png', bbox_inches='tight')
|
618 |
buf.seek(0)
|
619 |
image = Image.open(buf).convert("RGB")
|
620 |
plt.close(fig)
|
|
|
621 |
return image
|
622 |
|
623 |
def plot_combined_results(self, time, biomass, substrate, product,
|
|
|
627 |
show_legend=True, show_params=True,
|
628 |
style='whitegrid',
|
629 |
line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
|
630 |
+
use_differential=False, axis_labels=None,
|
631 |
+
show_error_bars=True, error_cap_size=3, error_line_width=1): # Added error bar parameters
|
632 |
|
|
|
633 |
if y_pred_biomass is None and not use_differential:
|
634 |
print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type} (combinado). Omitiendo figura.")
|
635 |
return None
|
|
|
637 |
print(f"Se solicitó usar EDO (combinado) pero no hay parámetros de biomasa para {experiment_name}. Omitiendo EDO.")
|
638 |
use_differential = False
|
639 |
|
|
|
640 |
if axis_labels is None:
|
641 |
axis_labels = {
|
642 |
'x_label': 'Tiempo',
|
|
|
646 |
}
|
647 |
|
648 |
sns.set_style(style)
|
649 |
+
time_to_plot = time
|
650 |
|
651 |
if use_differential and 'biomass' in self.params and self.params['biomass']:
|
652 |
X_ode, S_ode, P_ode, time_fine_ode = self.solve_differential_equations(time, biomass, substrate, product)
|
|
|
655 |
time_to_plot = time_fine_ode
|
656 |
else:
|
657 |
print(f"Fallo al resolver EDOs para {experiment_name} (combinado), usando resultados de curve_fit si existen.")
|
658 |
+
time_to_plot = time
|
659 |
+
else:
|
660 |
if not use_differential and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
661 |
time_fine_curvefit = self.generate_fine_time_grid(time)
|
662 |
if time_fine_curvefit is not None and len(time_fine_curvefit)>0:
|
|
|
683 |
if not np.all(np.isnan(y_pred_product_fine)):
|
684 |
y_pred_product = y_pred_product_fine
|
685 |
|
686 |
+
fig, ax1 = plt.subplots(figsize=(12, 7))
|
|
|
687 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()})', fontsize=16)
|
688 |
|
689 |
colors = {'Biomasa': 'blue', 'Sustrato': 'green', 'Producto': 'red'}
|
690 |
data_colors = {'Biomasa': 'darkblue', 'Sustrato': 'darkgreen', 'Producto': 'darkred'}
|
691 |
model_colors = {'Biomasa': 'cornflowerblue', 'Sustrato': 'limegreen', 'Producto': 'salmon'}
|
692 |
|
|
|
693 |
ax1.set_xlabel(axis_labels['x_label'])
|
694 |
ax1.set_ylabel(axis_labels['biomass_label'], color=colors['Biomasa'])
|
695 |
if biomass is not None and len(biomass) > 0 and not np.all(np.isnan(biomass)):
|
696 |
+
if show_error_bars and biomass_std is not None and len(biomass_std) == len(biomass) and not np.all(np.isnan(biomass_std)):
|
697 |
+
ax1.errorbar(
|
698 |
+
time, biomass, yerr=biomass_std,
|
699 |
+
fmt=marker_style, color=data_colors['Biomasa'],
|
700 |
+
label=f'{axis_labels["biomass_label"]} (Datos)',
|
701 |
+
capsize=error_cap_size,
|
702 |
+
elinewidth=error_line_width,
|
703 |
+
markersize=5
|
704 |
+
)
|
705 |
else:
|
706 |
ax1.plot(time, biomass, marker=marker_style, linestyle='', color=data_colors['Biomasa'],
|
707 |
label=f'{axis_labels["biomass_label"]} (Datos)', markersize=5)
|
|
|
713 |
ax2 = ax1.twinx()
|
714 |
ax2.set_ylabel(axis_labels['substrate_label'], color=colors['Sustrato'])
|
715 |
if substrate is not None and len(substrate) > 0 and not np.all(np.isnan(substrate)):
|
716 |
+
if show_error_bars and substrate_std is not None and len(substrate_std) == len(substrate) and not np.all(np.isnan(substrate_std)):
|
717 |
+
ax2.errorbar(
|
718 |
+
time, substrate, yerr=substrate_std,
|
719 |
+
fmt=marker_style, color=data_colors['Sustrato'],
|
720 |
+
label=f'{axis_labels["substrate_label"]} (Datos)',
|
721 |
+
capsize=error_cap_size,
|
722 |
+
elinewidth=error_line_width,
|
723 |
+
markersize=5
|
724 |
+
)
|
725 |
else:
|
726 |
ax2.plot(time, substrate, marker=marker_style, linestyle='', color=data_colors['Sustrato'],
|
727 |
label=f'{axis_labels["substrate_label"]} (Datos)', markersize=5)
|
|
|
731 |
ax2.tick_params(axis='y', labelcolor=colors['Sustrato'])
|
732 |
|
733 |
ax3 = ax1.twinx()
|
734 |
+
ax3.spines["right"].set_position(("axes", 1.15))
|
735 |
ax3.set_frame_on(True)
|
736 |
ax3.patch.set_visible(False)
|
737 |
|
|
|
738 |
ax3.set_ylabel(axis_labels['product_label'], color=colors['Producto'])
|
739 |
if product is not None and len(product) > 0 and not np.all(np.isnan(product)):
|
740 |
+
if show_error_bars and product_std is not None and len(product_std) == len(product) and not np.all(np.isnan(product_std)):
|
741 |
+
ax3.errorbar(
|
742 |
+
time, product, yerr=product_std,
|
743 |
+
fmt=marker_style, color=data_colors['Producto'],
|
744 |
+
label=f'{axis_labels["product_label"]} (Datos)',
|
745 |
+
capsize=error_cap_size,
|
746 |
+
elinewidth=error_line_width,
|
747 |
+
markersize=5
|
748 |
+
)
|
749 |
else:
|
750 |
ax3.plot(time, product, marker=marker_style, linestyle='', color=data_colors['Producto'],
|
751 |
label=f'{axis_labels["product_label"]} (Datos)', markersize=5)
|
|
|
754 |
label=f'{axis_labels["product_label"]} (Modelo)')
|
755 |
ax3.tick_params(axis='y', labelcolor=colors['Producto'])
|
756 |
|
|
|
757 |
lines_labels_collect = []
|
758 |
for ax_current in [ax1, ax2, ax3]:
|
759 |
h, l = ax_current.get_legend_handles_labels()
|
760 |
+
if h:
|
761 |
lines_labels_collect.append((h,l))
|
762 |
|
763 |
if lines_labels_collect:
|
764 |
+
lines, labels = [sum(lol, []) for lol in zip(*[(h,l) for h,l in lines_labels_collect])]
|
|
|
765 |
unique_labels_dict = dict(zip(labels, lines))
|
766 |
if show_legend:
|
767 |
ax1.legend(unique_labels_dict.values(), unique_labels_dict.keys(), loc=legend_position)
|
768 |
|
|
|
769 |
if show_params:
|
770 |
texts_to_display = []
|
771 |
param_categories = [
|
|
|
780 |
r2_display = f"{r2_val:.3f}" if np.isfinite(r2_val) else "N/A"
|
781 |
rmse_display = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else "N/A"
|
782 |
texts_to_display.append(f"{label}:\n{param_text}\n R² = {r2_display}\n RMSE = {rmse_display}")
|
783 |
+
elif params_dict:
|
784 |
texts_to_display.append(f"{label}:\n Parámetros no válidos o N/A")
|
|
|
|
|
785 |
|
786 |
total_text = "\n\n".join(texts_to_display)
|
787 |
|
788 |
+
if total_text:
|
789 |
if params_position == 'outside right':
|
790 |
+
fig.subplots_adjust(right=0.70)
|
791 |
bbox_props = dict(boxstyle='round,pad=0.3', facecolor='wheat', alpha=0.7)
|
|
|
792 |
fig.text(0.72, 0.5, total_text, transform=fig.transFigure,
|
793 |
verticalalignment='center', horizontalalignment='left',
|
794 |
bbox=bbox_props, fontsize=8)
|
|
|
795 |
else:
|
796 |
text_x, ha = (0.95, 'right') if 'right' in params_position else (0.05, 'left')
|
797 |
text_y, va = (0.95, 'top') if 'upper' in params_position else (0.05, 'bottom')
|
|
|
800 |
bbox={'boxstyle':'round,pad=0.3', 'facecolor':'wheat', 'alpha':0.7}, fontsize=8)
|
801 |
|
802 |
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
|
|
803 |
if params_position == 'outside right':
|
804 |
fig.subplots_adjust(right=0.70)
|
805 |
|
|
|
806 |
buf = io.BytesIO()
|
807 |
fig.savefig(buf, format='png', bbox_inches='tight')
|
808 |
buf.seek(0)
|
809 |
image = Image.open(buf).convert("RGB")
|
810 |
plt.close(fig)
|
|
|
811 |
return image
|
812 |
|
813 |
def process_all_data(file, legend_position, params_position, model_types_selected, experiment_names_str,
|
814 |
+
lower_bounds_str, upper_bounds_str,
|
815 |
mode, style, line_color, point_color, line_style, marker_style,
|
816 |
show_legend, show_params, use_differential, maxfev_val,
|
817 |
+
axis_labels_dict,
|
818 |
+
show_error_bars, error_cap_size, error_line_width): # New error bar parameters
|
819 |
|
820 |
if file is None:
|
821 |
return [], pd.DataFrame(), "Por favor, sube un archivo Excel."
|
822 |
|
823 |
try:
|
|
|
824 |
try:
|
825 |
xls = pd.ExcelFile(file.name)
|
826 |
+
except AttributeError:
|
827 |
xls = pd.ExcelFile(file)
|
|
|
828 |
sheet_names = xls.sheet_names
|
829 |
if not sheet_names:
|
830 |
return [], pd.DataFrame(), "El archivo Excel está vacío o no contiene hojas."
|
|
|
831 |
except Exception as e:
|
832 |
return [], pd.DataFrame(), f"Error al leer el archivo Excel: {e}"
|
833 |
|
|
|
837 |
experiment_names_list = experiment_names_str.strip().split('\n') if experiment_names_str.strip() else []
|
838 |
all_plot_messages = []
|
839 |
|
|
|
840 |
for sheet_name_idx, sheet_name in enumerate(sheet_names):
|
841 |
current_experiment_name_base = (experiment_names_list[sheet_name_idx]
|
842 |
if sheet_name_idx < len(experiment_names_list) and experiment_names_list[sheet_name_idx]
|
|
|
846 |
if df.empty:
|
847 |
all_plot_messages.append(f"Hoja '{sheet_name}' está vacía.")
|
848 |
continue
|
|
|
849 |
if not any(col_level2 == 'Tiempo' for _, col_level2 in df.columns):
|
850 |
all_plot_messages.append(f"Hoja '{sheet_name}' no contiene la subcolumna 'Tiempo'. Saltando hoja.")
|
851 |
continue
|
|
|
852 |
except Exception as e:
|
853 |
all_plot_messages.append(f"Error al leer la hoja '{sheet_name}': {e}. Saltando hoja.")
|
854 |
continue
|
855 |
|
|
|
856 |
model_dummy_for_sheet = BioprocessModel()
|
857 |
try:
|
858 |
model_dummy_for_sheet.process_data(df)
|
859 |
+
except ValueError as e:
|
860 |
all_plot_messages.append(f"Error procesando datos de la hoja '{sheet_name}': {e}. Saltando hoja.")
|
861 |
continue
|
862 |
|
|
|
|
|
|
|
863 |
if mode == 'independent':
|
|
|
|
|
|
|
864 |
grouped_cols = df.columns.get_level_values(0).unique()
|
|
|
865 |
for exp_idx, exp_col_name in enumerate(grouped_cols):
|
866 |
current_experiment_name = f"{current_experiment_name_base} - Exp {exp_idx + 1} ({exp_col_name})"
|
867 |
+
exp_df = df[exp_col_name]
|
|
|
868 |
try:
|
869 |
time_exp = exp_df['Tiempo'].dropna().values
|
|
|
870 |
biomass_exp = exp_df['Biomasa'].dropna().astype(float).values if 'Biomasa' in exp_df else np.array([])
|
871 |
substrate_exp = exp_df['Sustrato'].dropna().astype(float).values if 'Sustrato' in exp_df else np.array([])
|
872 |
product_exp = exp_df['Producto'].dropna().astype(float).values if 'Producto' in exp_df else np.array([])
|
|
|
874 |
if len(time_exp) == 0:
|
875 |
all_plot_messages.append(f"No hay datos de tiempo para {current_experiment_name}. Saltando.")
|
876 |
continue
|
877 |
+
if len(biomass_exp) == 0 :
|
878 |
all_plot_messages.append(f"No hay datos de biomasa para {current_experiment_name}. Saltando modelos para este experimento.")
|
|
|
879 |
for model_type_iter in model_types_selected:
|
880 |
comparison_data.append({
|
881 |
'Experimento': current_experiment_name, 'Modelo': model_type_iter.capitalize(),
|
|
|
883 |
**{f'RMSE {comp}': np.nan for comp in ['Biomasa', 'Sustrato', 'Producto']}
|
884 |
})
|
885 |
continue
|
|
|
|
|
886 |
except KeyError as e:
|
887 |
all_plot_messages.append(f"Faltan columnas (Tiempo, Biomasa, Sustrato, Producto) en '{current_experiment_name}': {e}. Saltando.")
|
888 |
continue
|
|
|
890 |
all_plot_messages.append(f"Error extrayendo datos para '{current_experiment_name}': {e_data}. Saltando.")
|
891 |
continue
|
892 |
|
|
|
|
|
|
|
|
|
893 |
biomass_std_exp, substrate_std_exp, product_std_exp = None, None, None
|
894 |
|
895 |
for model_type_iter in model_types_selected:
|
896 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
897 |
+
model_instance.fit_model()
|
|
|
898 |
y_pred_biomass = model_instance.fit_biomass(time_exp, biomass_exp)
|
899 |
y_pred_substrate, y_pred_product = None, None
|
|
|
900 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
901 |
if len(substrate_exp) > 0 :
|
902 |
y_pred_substrate = model_instance.fit_substrate(time_exp, substrate_exp, model_instance.params['biomass'])
|
|
|
905 |
else:
|
906 |
all_plot_messages.append(f"Ajuste de biomasa falló para {current_experiment_name} con modelo {model_type_iter}.")
|
907 |
|
|
|
908 |
comparison_data.append({
|
909 |
+
'Experimento': current_experiment_name, 'Modelo': model_type_iter.capitalize(),
|
910 |
+
'R² Biomasa': model_instance.r2.get('biomass', np.nan), 'RMSE Biomasa': model_instance.rmse.get('biomass', np.nan),
|
911 |
+
'R² Sustrato': model_instance.r2.get('substrate', np.nan), 'RMSE Sustrato': model_instance.rmse.get('substrate', np.nan),
|
912 |
+
'R² Producto': model_instance.r2.get('product', np.nan), 'RMSE Producto': model_instance.rmse.get('product', np.nan)
|
|
|
|
|
|
|
|
|
913 |
})
|
914 |
|
915 |
fig = model_instance.plot_results(
|
|
|
919 |
current_experiment_name, legend_position, params_position,
|
920 |
show_legend, show_params, style,
|
921 |
line_color, point_color, line_style, marker_style,
|
922 |
+
use_differential, axis_labels_dict,
|
923 |
+
show_error_bars=show_error_bars, # Pass new parameters
|
924 |
+
error_cap_size=error_cap_size,
|
925 |
+
error_line_width=error_line_width
|
926 |
)
|
927 |
if fig: figures.append(fig)
|
928 |
experiment_counter +=1
|
929 |
|
|
|
|
|
930 |
elif mode in ['average', 'combinado']:
|
931 |
current_experiment_name = f"{current_experiment_name_base} - Promedio"
|
932 |
+
time_avg = model_dummy_for_sheet.time
|
|
|
|
|
|
|
933 |
biomass_avg = model_dummy_for_sheet.dataxp[-1] if model_dummy_for_sheet.dataxp else np.array([])
|
934 |
substrate_avg = model_dummy_for_sheet.datasp[-1] if model_dummy_for_sheet.datasp else np.array([])
|
935 |
product_avg = model_dummy_for_sheet.datapp[-1] if model_dummy_for_sheet.datapp else np.array([])
|
|
|
951 |
})
|
952 |
continue
|
953 |
|
|
|
954 |
for model_type_iter in model_types_selected:
|
955 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
956 |
model_instance.fit_model()
|
|
|
957 |
y_pred_biomass = model_instance.fit_biomass(time_avg, biomass_avg)
|
958 |
y_pred_substrate, y_pred_product = None, None
|
|
|
959 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
960 |
if len(substrate_avg) > 0:
|
961 |
y_pred_substrate = model_instance.fit_substrate(time_avg, substrate_avg, model_instance.params['biomass'])
|
|
|
964 |
else:
|
965 |
all_plot_messages.append(f"Ajuste de biomasa promedio falló para {current_experiment_name} con modelo {model_type_iter}.")
|
966 |
|
|
|
967 |
comparison_data.append({
|
968 |
+
'Experimento': current_experiment_name, 'Modelo': model_type_iter.capitalize(),
|
969 |
+
'R² Biomasa': model_instance.r2.get('biomass', np.nan), 'RMSE Biomasa': model_instance.rmse.get('biomass', np.nan),
|
970 |
+
'R² Sustrato': model_instance.r2.get('substrate', np.nan), 'RMSE Sustrato': model_instance.rmse.get('substrate', np.nan),
|
971 |
+
'R² Producto': model_instance.r2.get('product', np.nan), 'RMSE Producto': model_instance.rmse.get('product', np.nan)
|
|
|
|
|
|
|
|
|
972 |
})
|
973 |
|
974 |
plot_func = model_instance.plot_combined_results if mode == 'combinado' else model_instance.plot_results
|
|
|
979 |
current_experiment_name, legend_position, params_position,
|
980 |
show_legend, show_params, style,
|
981 |
line_color, point_color, line_style, marker_style,
|
982 |
+
use_differential, axis_labels_dict,
|
983 |
+
show_error_bars=show_error_bars, # Pass new parameters
|
984 |
+
error_cap_size=error_cap_size,
|
985 |
+
error_line_width=error_line_width
|
986 |
)
|
987 |
if fig: figures.append(fig)
|
988 |
experiment_counter +=1
|
989 |
|
|
|
990 |
comparison_df = pd.DataFrame(comparison_data)
|
991 |
if not comparison_df.empty:
|
|
|
992 |
for col in ['R² Biomasa', 'RMSE Biomasa', 'R² Sustrato', 'RMSE Sustrato', 'R² Producto', 'RMSE Producto']:
|
993 |
if col in comparison_df.columns:
|
994 |
comparison_df[col] = pd.to_numeric(comparison_df[col], errors='coerce')
|
|
|
995 |
comparison_df_sorted = comparison_df.sort_values(
|
996 |
by=['Experimento', 'Modelo', 'R² Biomasa', 'R² Sustrato', 'R² Producto', 'RMSE Biomasa', 'RMSE Sustrato', 'RMSE Producto'],
|
997 |
+
ascending=[True, True, False, False, False, True, True, True]
|
998 |
).reset_index(drop=True)
|
999 |
else:
|
1000 |
+
comparison_df_sorted = pd.DataFrame(columns=[
|
1001 |
'Experimento', 'Modelo', 'R² Biomasa', 'RMSE Biomasa',
|
1002 |
'R² Sustrato', 'RMSE Sustrato', 'R² Producto', 'RMSE Producto'
|
1003 |
])
|
|
|
1009 |
final_message += "\nNo se generaron gráficos, pero hay datos en la tabla."
|
1010 |
elif not figures and comparison_df_sorted.empty:
|
1011 |
final_message += "\nNo se generaron gráficos ni datos para la tabla."
|
|
|
|
|
1012 |
return figures, comparison_df_sorted, final_message
|
1013 |
|
|
|
1014 |
def create_interface():
|
1015 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
1016 |
gr.Markdown("# Modelos Cinéticos de Bioprocesos")
|
|
|
1123 |
with gr.Row():
|
1124 |
style_dropdown = gr.Dropdown(choices=['white', 'dark', 'whitegrid', 'darkgrid', 'ticks'],
|
1125 |
label="Estilo de Gráfico (Seaborn)", value='whitegrid')
|
1126 |
+
line_color_picker = gr.ColorPicker(label="Color de Línea (Modelo)", value='#0072B2')
|
1127 |
+
point_color_picker = gr.ColorPicker(label="Color de Puntos (Datos)", value='#D55E00')
|
1128 |
|
1129 |
with gr.Row():
|
1130 |
line_style_dropdown = gr.Dropdown(choices=['-', '--', '-.', ':'], label="Estilo de Línea", value='-')
|
|
|
1136 |
with gr.Row():
|
1137 |
substrate_axis_label_input = gr.Textbox(label="Título Eje Y (Sustrato)", value="Sustrato (g/L)", placeholder="Sustrato (unidades)")
|
1138 |
product_axis_label_input = gr.Textbox(label="Título Eje Y (Producto)", value="Producto (g/L)", placeholder="Producto (unidades)")
|
1139 |
+
|
1140 |
+
# ADDED ERROR BAR CONTROLS
|
1141 |
+
with gr.Row():
|
1142 |
+
show_error_bars_ui = gr.Checkbox(label="Mostrar barras de error", value=True)
|
1143 |
+
error_cap_size_ui = gr.Slider(label="Tamaño de tapa de barras de error", minimum=1, maximum=10, step=1, value=3)
|
1144 |
+
error_line_width_ui = gr.Slider(label="Grosor de línea de error", minimum=0.5, maximum=5, step=0.5, value=1.0)
|
1145 |
|
|
|
|
|
|
|
1146 |
with gr.Accordion("Configuración Avanzada de Ajuste (No implementado aún)", open=False):
|
1147 |
with gr.Row():
|
1148 |
lower_bounds_str = gr.Textbox(label="Lower Bounds (no usado actualmente)", lines=3)
|
1149 |
upper_bounds_str = gr.Textbox(label="Upper Bounds (no usado actualmente)", lines=3)
|
1150 |
|
1151 |
simulate_btn = gr.Button("Simular y Graficar", variant="primary")
|
|
|
1152 |
status_message = gr.Textbox(label="Estado del Procesamiento", interactive=False)
|
|
|
1153 |
output_gallery = gr.Gallery(label="Resultados Gráficos", columns=[2,1], height='auto', object_fit="contain")
|
|
|
1154 |
output_table = gr.Dataframe(
|
1155 |
label="Tabla Comparativa de Modelos (Ordenada por R² Biomasa Descendente)",
|
1156 |
headers=["Experimento", "Modelo", "R² Biomasa", "RMSE Biomasa",
|
1157 |
"R² Sustrato", "RMSE Sustrato", "R² Producto", "RMSE Producto"],
|
1158 |
+
interactive=False, wrap=True
|
1159 |
)
|
1160 |
+
state_df = gr.State(pd.DataFrame())
|
|
|
1161 |
|
1162 |
def run_simulation_interface(file, legend_pos, params_pos, models_sel, analysis_mode, exp_names,
|
1163 |
low_bounds, up_bounds, plot_style,
|
1164 |
line_col, point_col, line_sty, marker_sty,
|
1165 |
show_leg, show_par, use_diff, maxfev,
|
1166 |
+
x_label, biomass_label, substrate_label, product_label,
|
1167 |
+
show_error_bars_arg, error_cap_size_arg, error_line_width_arg): # New error bar args
|
1168 |
if file is None:
|
1169 |
+
return [], pd.DataFrame(), "Error: Por favor, sube un archivo Excel.", pd.DataFrame()
|
1170 |
|
1171 |
axis_labels = {
|
1172 |
'x_label': x_label if x_label else 'Tiempo',
|
|
|
1175 |
'product_label': product_label if product_label else 'Producto'
|
1176 |
}
|
1177 |
|
1178 |
+
if not models_sel:
|
1179 |
+
return [], pd.DataFrame(), "Error: Por favor, selecciona al menos un tipo de modelo de biomasa.", pd.DataFrame()
|
|
|
1180 |
|
1181 |
figures, comparison_df, message = process_all_data(
|
1182 |
file, legend_pos, params_pos, models_sel, exp_names,
|
1183 |
low_bounds, up_bounds, analysis_mode, plot_style,
|
1184 |
line_col, point_col, line_sty, marker_sty,
|
1185 |
show_leg, show_par, use_diff, int(maxfev),
|
1186 |
+
axis_labels,
|
1187 |
+
show_error_bars_arg, error_cap_size_arg, error_line_width_arg # Pass new args
|
1188 |
)
|
1189 |
+
return figures, comparison_df, message, comparison_df
|
1190 |
|
1191 |
simulate_btn.click(
|
1192 |
fn=run_simulation_interface,
|
|
|
1195 |
lower_bounds_str, upper_bounds_str, style_dropdown,
|
1196 |
line_color_picker, point_color_picker, line_style_dropdown, marker_style_dropdown,
|
1197 |
show_legend, show_params, use_differential, maxfev_input,
|
1198 |
+
x_axis_label_input, biomass_axis_label_input, substrate_axis_label_input, product_axis_label_input,
|
1199 |
+
show_error_bars_ui, error_cap_size_ui, error_line_width_ui # New UI inputs
|
1200 |
],
|
1201 |
outputs=[output_gallery, output_table, status_message, state_df]
|
1202 |
)
|
1203 |
|
1204 |
def export_excel_interface(df_to_export):
|
1205 |
if df_to_export is None or df_to_export.empty:
|
|
|
1206 |
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
1207 |
tmp.write(b"No hay datos para exportar.")
|
1208 |
+
return tmp.name
|
|
|
|
|
|
|
1209 |
try:
|
1210 |
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False, mode='w+b') as tmp:
|
1211 |
df_to_export.to_excel(tmp.name, index=False)
|
1212 |
return tmp.name
|
1213 |
except Exception as e:
|
|
|
1214 |
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
1215 |
tmp.write(f"Error al exportar a Excel: {e}".encode())
|
1216 |
return tmp.name
|
1217 |
|
|
|
1218 |
export_btn = gr.Button("Exportar Tabla a Excel")
|
1219 |
download_file_output = gr.File(label="Descargar archivo Excel", interactive=False)
|
1220 |
|
1221 |
export_btn.click(
|
1222 |
fn=export_excel_interface,
|
1223 |
+
inputs=state_df,
|
1224 |
outputs=download_file_output
|
1225 |
)
|
1226 |
|
1227 |
gr.Examples(
|
1228 |
examples=[
|
1229 |
+
[None, "best", "upper right", ["logistic"], "independent", "Exp A\nExp B", "", "", "whitegrid", "#0072B2", "#D55E00", "-", "o", True, True, False, 50000, "Tiempo (días)", "Células (millones/mL)", "Glucosa (mM)", "Anticuerpo (mg/L)", True, 3, 1.0]
|
1230 |
],
|
1231 |
inputs=[
|
1232 |
file_input, legend_position, params_position, model_types_selected, mode, experiment_names_str,
|
1233 |
lower_bounds_str, upper_bounds_str, style_dropdown,
|
1234 |
line_color_picker, point_color_picker, line_style_dropdown, marker_style_dropdown,
|
1235 |
show_legend, show_params, use_differential, maxfev_input,
|
1236 |
+
x_axis_label_input, biomass_axis_label_input, substrate_axis_label_input, product_axis_label_input,
|
1237 |
+
show_error_bars_ui, error_cap_size_ui, error_line_width_ui # Added example values for new inputs
|
1238 |
],
|
1239 |
label="Ejemplo de Configuración (subir archivo manualmente)"
|
1240 |
)
|
|
|
|
|
1241 |
return demo
|
1242 |
|
1243 |
if __name__ == '__main__':
|
|
|
|
|
|
|
|
|
1244 |
try:
|
1245 |
import google.colab
|
1246 |
IN_COLAB = True
|
|
|
1248 |
IN_COLAB = False
|
1249 |
|
1250 |
demo_instance = create_interface()
|
1251 |
+
demo_instance.launch(share=True) # Use share=IN_COLAB for conditional sharing
|
|