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
|
|
|