C2MV commited on
Commit
a3b612a
·
verified ·
1 Parent(s): 21df8ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -289
app.py CHANGED
@@ -41,18 +41,14 @@ class BioprocessModel:
41
 
42
  @staticmethod
43
  def logistic(time, xo, xm, um):
44
- # Ensure xm is not zero and xo/xm is not 1 to avoid division by zero or log(0)
45
- if xm == 0 or (xo / xm == 1 and np.any(um * time > 0)): # Simplified check
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) # Replace 0 with small number
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: # Prevent division by zero
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: # Prevent division by zero
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)): # If biomass model returned NaN
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)) # Estimate dt
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] # Fallback
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)): # If biomass model returned NaN
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)) # Estimate dt
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: # Handle case where Biomass columns might be missing
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
- # Ensure biomass has some variation, otherwise std dev can be 0
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 # ensure xm > xo
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]) # Lag can't be negative
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] # Ks is like a time shift
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: # Avoid division by zero if biomass is constant
254
- self.r2['biomass'] = 1.0 if ss_res == 0 else 0.0 # Perfect fit if residuals are also 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'] = {} # Clear params on failure
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: # Check if biomass_params_dict is empty
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 # Yxs inverse (biomass/substrate)
289
- q_guess = 0.01 # Maintenance
290
  p0 = [so_guess, p_guess, q_guess]
291
- bounds = ([0, 0, 0], [np.inf, np.inf, np.inf]) # Parameters should be non-negative
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 # Growth-associated
344
- beta_guess = 0.01 # Non-growth-associated
345
  p0 = [po_guess, alpha_guess, beta_guess]
346
- bounds = ([0, 0, 0], [np.inf, np.inf, np.inf]) # Parameters should be non-negative
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]) # Default if time is not set
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 # X, S, P current values
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 # Should not happen if model_type is validated
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) # Calculate X at t=0
448
- if np.isnan(X0): X0 = X0_exp # Fallback if calculation fails
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) # Calculate X at t=0
455
- if np.isnan(X0): X0 = X0_exp # Fallback
456
  else:
457
- X0 = X0_exp # Fallback for unknown model type
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 : # Check if time is valid
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) # Added tolerances
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: # If using differential, biomass params might still be there
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 # Fallback to curve_fit results if any
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 # Default time grid
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 # Use the fine time grid for ODE results
589
  else:
590
  print(f"Fallo al resolver EDOs para {experiment_name}, usando resultados de curve_fit si existen.")
591
- # Keep original y_pred_biomass etc. from curve_fit if ODE failed
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 # Update time_to_plot only if biomass_fine is valid
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(time, data_exp, yerr=data_std_exp, fmt=marker_style, color=point_color,
643
- label='Datos experimentales', capsize=5, elinewidth=1, markeredgewidth=1)
 
 
 
 
 
 
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: # Special message if biomass model failed
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()]) # Use .3g for general format
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
- # Adjust x position to be truly outside
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', # Small offset for padding
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 # Default
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 # Revert
751
- else: # Smoother curve_fit lines if not using ODE
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(time, biomass, yerr=biomass_std, fmt=marker_style, color=data_colors['Biomasa'],
792
- label=f'{axis_labels["biomass_label"]} (Datos)', capsize=3, elinewidth=1, markersize=5)
 
 
 
 
 
 
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(time, substrate, yerr=substrate_std, fmt=marker_style, color=data_colors['Sustrato'],
806
- label=f'{axis_labels["substrate_label"]} (Datos)', capsize=3, elinewidth=1, markersize=5)
 
 
 
 
 
 
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)) # Adjusted position for third axis
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(time, product, yerr=product_std, fmt=marker_style, color=data_colors['Producto'],
825
- label=f'{axis_labels["product_label"]} (Datos)', capsize=3, elinewidth=1, markersize=5)
 
 
 
 
 
 
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: # Only add if there are handles/labels
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])] # careful with empty h,l
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: # Some params but maybe not all finite, or model failed
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: # Only display if there's something to show
871
  if params_position == 'outside right':
872
- fig.subplots_adjust(right=0.70) # Make more space for text outside
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, # These are not used in current model fit, but kept for future
902
  mode, style, line_color, point_color, line_style, marker_style,
903
  show_legend, show_params, use_differential, maxfev_val,
904
- axis_labels_dict): # Added 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: # If file is already a path (e.g. from tempfile)
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: # Catch specific errors from process_data
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] # DataFrame for the current experiment
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 : # Biomass is essential for fitting other models
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() # Sets self.biomass_model and self.biomass_diff
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
- 'Modelo': model_type_iter.capitalize(),
1023
- 'R² Biomasa': model_instance.r2.get('biomass', np.nan),
1024
- 'RMSE Biomasa': model_instance.rmse.get('biomass', np.nan),
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 # Pass 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
- 'Modelo': model_type_iter.capitalize(),
1092
- 'R² Biomasa': model_instance.r2.get('biomass', np.nan),
1093
- 'RMSE Biomasa': model_instance.rmse.get('biomass', np.nan),
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 # Pass 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] # Sort R² descending, RMSE ascending
1124
  ).reset_index(drop=True)
1125
  else:
1126
- comparison_df_sorted = pd.DataFrame(columns=[ # Ensure empty DF has correct 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') # Seaborn blue
1256
- point_color_picker = gr.ColorPicker(label="Color de Puntos (Datos)", value='#D55E00') # Seaborn orange
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 # Remove height=400
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: # Check if no models are selected
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 # Pass the constructed dictionary
 
1317
  )
1318
- return figures, comparison_df, message, comparison_df # Pass df to state too
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 # New axis label inputs
 
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 # Return path to this dummy file
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, # Get the DataFrame from the state
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
- # demo_instance.launch(share=IN_COLAB) # Share only if in Colab, otherwise local
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