C2MV commited on
Commit
d248e5f
1 Parent(s): 98b9df2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +585 -336
app.py CHANGED
@@ -1,32 +1,25 @@
1
- # -*- coding: utf-8 -*-
2
- """PrediLectia - Gradio Final v2 with Multiple Y-Axes in Combined Plot.ipynb"""
3
-
4
- # Instalación de librerías necesarias
5
- #!pip install gradio seaborn scipy -q
6
- import os
7
- os.system('pip install gradio seaborn scipy scikit-learn openpyxl pydantic==1.10.0')
8
 
9
  from pydantic import BaseModel, ConfigDict
10
-
11
- class YourModel(BaseModel):
12
- class Config:
13
- arbitrary_types_allowed = True
14
-
15
  import numpy as np
16
  import pandas as pd
17
  import matplotlib.pyplot as plt
18
  import seaborn as sns
19
  from scipy.integrate import odeint
20
- from scipy.interpolate import interp1d
21
  from scipy.optimize import curve_fit
22
  from sklearn.metrics import mean_squared_error
23
  import gradio as gr
24
  import io
25
  from PIL import Image
 
 
 
 
 
26
 
27
- # Definición de la clase BioprocessModel
28
  class BioprocessModel:
29
- def __init__(self):
30
  self.params = {}
31
  self.r2 = {}
32
  self.rmse = {}
@@ -39,69 +32,70 @@ class BioprocessModel:
39
  self.datax_std = []
40
  self.datas_std = []
41
  self.datap_std = []
 
 
 
 
42
 
43
- # Funciones modelo analíticas
44
  @staticmethod
45
  def logistic(time, xo, xm, um):
46
  return (xo * np.exp(um * time)) / (1 - (xo / xm) * (1 - np.exp(um * time)))
47
 
48
  @staticmethod
49
- def substrate(time, so, p, q, xo, xm, um):
50
- return so - (p * xo * ((np.exp(um * time)) / (1 - (xo / xm) * (1 - np.exp(um * time))) - 1)) - \
51
- (q * (xm / um) * np.log(1 - (xo / xm) * (1 - np.exp(um * time))))
52
 
53
  @staticmethod
54
- def product(time, po, alpha, beta, xo, xm, um):
55
- return po + (alpha * xo * ((np.exp(um * time) / (1 - (xo / xm) * (1 - np.exp(um * time)))) - 1)) + \
56
- (beta * (xm / um) * np.log(1 - (xo / xm) * (1 - np.exp(um * time))))
57
 
58
- # Funciones modelo diferenciales
59
  @staticmethod
60
  def logistic_diff(X, t, params):
61
  xo, xm, um = params
62
- dXdt = um * X * (1 - X / xm)
63
- return dXdt
64
-
65
- def substrate_diff(self, S, t, params, biomass_params, X_func):
66
- so, p, q = params
67
- xo, xm, um = biomass_params
68
- X_t = X_func(t)
69
- dSdt = -p * (um * X_t * (1 - X_t / xm)) - q * X_t
70
- return dSdt
71
-
72
- def product_diff(self, P, t, params, biomass_params, X_func):
73
- po, alpha, beta = params
74
- xo, xm, um = biomass_params
75
- X_t = X_func(t)
76
- dPdt = alpha * (um * X_t * (1 - X_t / xm)) + beta * X_t
77
- return dPdt
78
-
79
- # Métodos de procesamiento y ajuste de datos
 
 
 
 
 
 
80
  def process_data(self, df):
81
- # Obtener todas las columnas que contengan "Biomasa", "Sustrato", y "Producto"
82
  biomass_cols = [col for col in df.columns if col[1] == 'Biomasa']
83
  substrate_cols = [col for col in df.columns if col[1] == 'Sustrato']
84
  product_cols = [col for col in df.columns if col[1] == 'Producto']
85
 
86
- # Procesar los datos de tiempo
87
  time_col = [col for col in df.columns if col[1] == 'Tiempo'][0]
88
  time = df[time_col].values
89
 
90
- # Procesar los datos de biomasa
91
  data_biomass = [df[col].values for col in biomass_cols]
92
- data_biomass = np.array(data_biomass) # shape (num_experiments, num_time_points)
93
  self.datax.append(data_biomass)
94
  self.dataxp.append(np.mean(data_biomass, axis=0))
95
  self.datax_std.append(np.std(data_biomass, axis=0, ddof=1))
96
 
97
- # Procesar los datos de sustrato
98
  data_substrate = [df[col].values for col in substrate_cols]
99
  data_substrate = np.array(data_substrate)
100
  self.datas.append(data_substrate)
101
  self.datasp.append(np.mean(data_substrate, axis=0))
102
  self.datas_std.append(np.std(data_substrate, axis=0, ddof=1))
103
 
104
- # Procesar los datos de producto
105
  data_product = [df[col].values for col in product_cols]
106
  data_product = np.array(data_product)
107
  self.datap.append(data_product)
@@ -110,69 +104,195 @@ class BioprocessModel:
110
 
111
  self.time = time
112
 
113
- def fit_model(self, model_type='logistic'):
114
- if model_type == 'logistic':
115
- self.fit_biomass = self.fit_biomass_logistic
116
- self.fit_substrate = self.fit_substrate_logistic
117
- self.fit_product = self.fit_product_logistic
118
- # Puedes agregar más modelos aquí si los necesitas.
119
-
120
- def fit_biomass_logistic(self, time, biomass, bounds):
121
- popt, _ = curve_fit(self.logistic, time, biomass, bounds=bounds, maxfev=10000)
122
- self.params['biomass'] = {'xo': popt[0], 'xm': popt[1], 'um': popt[2]}
123
- y_pred = self.logistic(time, *popt)
124
- self.r2['biomass'] = 1 - (np.sum((biomass - y_pred) ** 2) / np.sum((biomass - np.mean(biomass)) ** 2))
125
- self.rmse['biomass'] = np.sqrt(mean_squared_error(biomass, y_pred))
126
- return y_pred
127
-
128
- def fit_substrate_logistic(self, time, substrate, biomass_params, bounds):
129
- popt, _ = curve_fit(lambda t, so, p, q: self.substrate(t, so, p, q, *biomass_params.values()),
130
- time, substrate, bounds=bounds)
131
- self.params['substrate'] = {'so': popt[0], 'p': popt[1], 'q': popt[2]}
132
- y_pred = self.substrate(time, *popt, *biomass_params.values())
133
- self.r2['substrate'] = 1 - (np.sum((substrate - y_pred) ** 2) / np.sum((substrate - np.mean(substrate)) ** 2))
134
- self.rmse['substrate'] = np.sqrt(mean_squared_error(substrate, y_pred))
135
- return y_pred
136
-
137
- def fit_product_logistic(self, time, product, biomass_params, bounds):
138
- popt, _ = curve_fit(lambda t, po, alpha, beta: self.product(t, po, alpha, beta, *biomass_params.values()),
139
- time, product, bounds=bounds)
140
- self.params['product'] = {'po': popt[0], 'alpha': popt[1], 'beta': popt[2]}
141
- y_pred = self.product(time, *popt, *biomass_params.values())
142
- self.r2['product'] = 1 - (np.sum((product - y_pred) ** 2) / np.sum((product - np.mean(product)) ** 2))
143
- self.rmse['product'] = np.sqrt(mean_squared_error(product, y_pred))
144
- return y_pred
145
-
146
- # Métodos de visualización de resultados
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def generate_fine_time_grid(self, time):
148
- # Generar una malla temporal más fina para curvas suaves
149
  time_fine = np.linspace(time.min(), time.max(), 500)
150
  return time_fine
151
 
152
- def solve_differential_equations(self, time, initial_conditions, params):
153
- # Resolver la ecuación diferencial para biomasa
154
- xo, xm, um = params['biomass'].values()
155
- biomass_params = [xo, xm, um]
156
- time_fine = self.generate_fine_time_grid(time)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- # Resolver biomasa
159
- X0 = xo
160
- X = odeint(self.logistic_diff, X0, time_fine, args=(biomass_params,)).flatten()
 
161
 
162
- # Crear función de interpolación para X(t)
163
- X_func = interp1d(time_fine, X, kind='linear', fill_value="extrapolate")
 
 
 
 
 
 
 
 
 
 
 
164
 
165
- # Resolver sustrato
166
- so, p, q = params['substrate'].values()
167
- substrate_params = [so, p, q]
168
- S0 = so
169
- S = odeint(self.substrate_diff, S0, time_fine, args=(substrate_params, biomass_params, X_func)).flatten()
 
 
 
 
170
 
171
- # Resolver producto
172
- po, alpha, beta = params['product'].values()
173
- product_params = [po, alpha, beta]
174
- P0 = po
175
- P = odeint(self.product_diff, P0, time_fine, args=(product_params, biomass_params, X_func)).flatten()
176
 
177
  return X, S, P, time_fine
178
 
@@ -184,11 +304,19 @@ class BioprocessModel:
184
  style='whitegrid',
185
  line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
186
  use_differential=False):
187
- sns.set_style(style) # Establecer el estilo seleccionado
188
 
189
- if use_differential:
190
- y_pred_biomass, y_pred_substrate, y_pred_product, time_to_plot = self.solve_differential_equations(
191
- time, [biomass[0], substrate[0], product[0]], self.params)
 
 
 
 
 
 
 
 
 
192
  else:
193
  time_to_plot = time
194
 
@@ -196,12 +324,12 @@ class BioprocessModel:
196
  fig.suptitle(f'{experiment_name}', fontsize=16)
197
 
198
  plots = [
199
- (ax1, biomass, y_pred_biomass, biomass_std, 'Biomasa', 'Modelo', self.params['biomass'],
200
- self.r2['biomass'], self.rmse['biomass']),
201
- (ax2, substrate, y_pred_substrate, substrate_std, 'Sustrato', 'Modelo', self.params['substrate'],
202
- self.r2['substrate'], self.rmse['substrate']),
203
- (ax3, product, y_pred_product, product_std, 'Producto', 'Modelo', self.params['product'],
204
- self.r2['product'], self.rmse['product'])
205
  ]
206
 
207
  for idx, (ax, data, y_pred, data_std, ylabel, model_name, params, r2, rmse) in enumerate(plots):
@@ -211,21 +339,19 @@ class BioprocessModel:
211
  else:
212
  ax.plot(time, data, marker=marker_style, linestyle='', color=point_color,
213
  label='Datos experimentales')
214
- if use_differential:
 
215
  ax.plot(time_to_plot, y_pred, linestyle=line_style, color=line_color, label=model_name)
216
- else:
217
- ax.plot(time, y_pred, linestyle=line_style, color=line_color, label=model_name)
218
  ax.set_xlabel('Tiempo')
219
  ax.set_ylabel(ylabel)
220
  if show_legend:
221
  ax.legend(loc=legend_position)
222
  ax.set_title(f'{ylabel}')
223
 
224
- if show_params:
225
- param_text = '\n'.join([f"{k} = {v:.4f}" for k, v in params.items()])
226
- text = f"{param_text}\nR² = {r2:.4f}\nRMSE = {rmse:.4f}"
227
-
228
- # Si la posición es 'outside right', ajustar la posición del texto
229
  if params_position == 'outside right':
230
  bbox_props = dict(boxstyle='round', facecolor='white', alpha=0.5)
231
  ax.annotate(text, xy=(1.05, 0.5), xycoords='axes fraction',
@@ -247,10 +373,17 @@ class BioprocessModel:
247
 
248
  ax.text(text_x, text_y, text, transform=ax.transAxes,
249
  verticalalignment=va, horizontalalignment=ha,
250
- bbox={'boxstyle': 'round', 'facecolor': 'white', 'alpha': 0.5})
 
 
251
 
252
- plt.tight_layout()
253
- return fig
 
 
 
 
 
254
 
255
  def plot_combined_results(self, time, biomass, substrate, product,
256
  y_pred_biomass, y_pred_substrate, y_pred_product,
@@ -260,21 +393,27 @@ class BioprocessModel:
260
  style='whitegrid',
261
  line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
262
  use_differential=False):
263
- sns.set_style(style) # Establecer el estilo seleccionado
264
 
265
- if use_differential:
266
- y_pred_biomass, y_pred_substrate, y_pred_product, time_to_plot = self.solve_differential_equations(
267
- time, [biomass[0], substrate[0], product[0]], self.params)
 
 
 
 
 
 
 
 
 
268
  else:
269
  time_to_plot = time
270
 
271
  fig, ax1 = plt.subplots(figsize=(10, 7))
272
  fig.suptitle(f'{experiment_name}', fontsize=16)
273
 
274
- # Colores específicos para cada variable
275
  colors = {'Biomasa': 'blue', 'Sustrato': 'green', 'Producto': 'red'}
276
 
277
- # Plot Biomasa en ax1
278
  ax1.set_xlabel('Tiempo')
279
  ax1.set_ylabel('Biomasa', color=colors['Biomasa'])
280
  if biomass_std is not None:
@@ -283,15 +422,10 @@ class BioprocessModel:
283
  else:
284
  ax1.plot(time, biomass, marker=marker_style, linestyle='', color=colors['Biomasa'],
285
  label='Biomasa (Datos)')
286
- if use_differential:
287
- ax1.plot(time_to_plot, y_pred_biomass, linestyle=line_style, color=colors['Biomasa'],
288
- label='Biomasa (Modelo)')
289
- else:
290
- ax1.plot(time, y_pred_biomass, linestyle=line_style, color=colors['Biomasa'],
291
- label='Biomasa (Modelo)')
292
  ax1.tick_params(axis='y', labelcolor=colors['Biomasa'])
293
 
294
- # Crear segundo eje y para Sustrato
295
  ax2 = ax1.twinx()
296
  ax2.set_ylabel('Sustrato', color=colors['Sustrato'])
297
  if substrate_std is not None:
@@ -300,18 +434,13 @@ class BioprocessModel:
300
  else:
301
  ax2.plot(time, substrate, marker=marker_style, linestyle='', color=colors['Sustrato'],
302
  label='Sustrato (Datos)')
303
- if use_differential:
304
  ax2.plot(time_to_plot, y_pred_substrate, linestyle=line_style, color=colors['Sustrato'],
305
  label='Sustrato (Modelo)')
306
- else:
307
- ax2.plot(time, y_pred_substrate, linestyle=line_style, color=colors['Sustrato'],
308
- label='Sustrato (Modelo)')
309
  ax2.tick_params(axis='y', labelcolor=colors['Sustrato'])
310
 
311
- # Crear tercer eje y para Producto
312
  ax3 = ax1.twinx()
313
- # Desplazar el tercer eje para evitar superposición
314
- ax3.spines["right"].set_position(("axes", 1.1))
315
  ax3.set_frame_on(True)
316
  ax3.patch.set_visible(False)
317
  for sp in ax3.spines.values():
@@ -324,30 +453,31 @@ class BioprocessModel:
324
  else:
325
  ax3.plot(time, product, marker=marker_style, linestyle='', color=colors['Producto'],
326
  label='Producto (Datos)')
327
- if use_differential:
328
  ax3.plot(time_to_plot, y_pred_product, linestyle=line_style, color=colors['Producto'],
329
  label='Producto (Modelo)')
330
- else:
331
- ax3.plot(time, y_pred_product, linestyle=line_style, color=colors['Producto'],
332
- label='Producto (Modelo)')
333
  ax3.tick_params(axis='y', labelcolor=colors['Producto'])
334
 
335
- # Manejo de leyendas
336
  lines_labels = [ax.get_legend_handles_labels() for ax in [ax1, ax2, ax3]]
337
  lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
338
  if show_legend:
339
  ax1.legend(lines, labels, loc=legend_position)
340
 
341
- # Mostrar parámetros y estadísticas en el gráfico
342
  if show_params:
343
- param_text_biomass = '\n'.join([f"{k} = {v:.4f}" for k, v in self.params['biomass'].items()])
344
- text_biomass = f"Biomasa:\n{param_text_biomass}\nR² = {self.r2['biomass']:.4f}\nRMSE = {self.rmse['biomass']:.4f}"
 
 
345
 
346
- param_text_substrate = '\n'.join([f"{k} = {v:.4f}" for k, v in self.params['substrate'].items()])
347
- text_substrate = f"Sustrato:\n{param_text_substrate}\nR² = {self.r2['substrate']:.4f}\nRMSE = {self.rmse['substrate']:.4f}"
 
 
348
 
349
- param_text_product = '\n'.join([f"{k} = {v:.4f}" for k, v in self.params['product'].items()])
350
- text_product = f"Producto:\n{param_text_product}\nR² = {self.r2['product']:.4f}\nRMSE = {self.rmse['product']:.4f}"
 
 
351
 
352
  total_text = f"{text_biomass}\n\n{text_substrate}\n\n{text_product}"
353
 
@@ -372,47 +502,57 @@ class BioprocessModel:
372
 
373
  ax1.text(text_x, text_y, total_text, transform=ax1.transAxes,
374
  verticalalignment=va, horizontalalignment=ha,
375
- bbox={'boxstyle': 'round', 'facecolor': 'white', 'alpha': 0.5})
376
 
377
- plt.tight_layout()
378
- return fig
379
 
380
- # Función de procesamiento de datos
381
- def process_data(file, legend_position, params_position, model_type, experiment_names, lower_bounds, upper_bounds,
382
- mode='independent', style='whitegrid', line_color='#0000FF', point_color='#000000',
383
- line_style='-', marker_style='o', show_legend=True, show_params=True, use_differential=False):
384
- # Leer todas las hojas del archivo Excel
385
- xls = pd.ExcelFile(file.name)
386
- sheet_names = xls.sheet_names
387
 
388
- model = BioprocessModel()
389
- model.fit_model(model_type)
390
- figures = []
391
 
392
- # Si no se proporcionan suficientes límites, usar valores predeterminados
393
- default_lower_bounds = (0, 0, 0)
394
- default_upper_bounds = (np.inf, np.inf, np.inf)
395
 
396
- experiment_counter = 0 # Contador global de experimentos
 
 
 
 
 
 
 
 
 
397
 
398
  for sheet_name in sheet_names:
399
- df = pd.read_excel(file.name, sheet_name=sheet_name, header=[0, 1])
 
 
 
 
400
 
401
- # Procesar datos
402
- model.process_data(df)
403
- time = model.time
404
 
405
  if mode == 'independent':
406
- # Modo independiente: iterar sobre cada experimento
407
  num_experiments = len(df.columns.levels[0])
408
  for idx in range(num_experiments):
409
  col = df.columns.levels[0][idx]
410
- time = df[(col, 'Tiempo')].dropna().values
411
- biomass = df[(col, 'Biomasa')].dropna().values
412
- substrate = df[(col, 'Sustrato')].dropna().values
413
- product = df[(col, 'Producto')].dropna().values
 
 
 
 
414
 
415
- # Si hay replicados en el experimento, calcular la desviación estándar
416
  biomass_std = None
417
  substrate_std = None
418
  product_std = None
@@ -426,132 +566,202 @@ def process_data(file, legend_position, params_position, model_type, experiment_
426
  product_std = np.std(product, axis=0, ddof=1)
427
  product = np.mean(product, axis=0)
428
 
429
- # Obtener límites o usar valores predeterminados
430
- lower_bound = lower_bounds[experiment_counter] if experiment_counter < len(lower_bounds) else default_lower_bounds
431
- upper_bound = upper_bounds[experiment_counter] if experiment_counter < len(upper_bounds) else default_upper_bounds
432
- bounds = (lower_bound, upper_bound)
433
-
434
- # Ajustar el modelo
435
- y_pred_biomass = model.fit_biomass(time, biomass, bounds)
436
- y_pred_substrate = model.fit_substrate(time, substrate, model.params['biomass'], bounds)
437
- y_pred_product = model.fit_product(time, product, model.params['biomass'], bounds)
438
-
439
- # Usar el nombre del experimento proporcionado o un nombre por defecto
440
- experiment_name = experiment_names[experiment_counter] if experiment_counter < len(experiment_names) else f"Tratamiento {experiment_counter + 1}"
441
-
442
- if mode == 'combinado':
443
- fig = model.plot_combined_results(time, biomass, substrate, product,
444
- y_pred_biomass, y_pred_substrate, y_pred_product,
445
- biomass_std, substrate_std, product_std,
446
- experiment_name, legend_position, params_position,
447
- show_legend, show_params,
448
- style,
449
- line_color, point_color, line_style, marker_style,
450
- use_differential)
451
- else:
452
- fig = model.plot_results(time, biomass, substrate, product,
453
- y_pred_biomass, y_pred_substrate, y_pred_product,
454
- biomass_std, substrate_std, product_std,
455
- experiment_name, legend_position, params_position,
456
- show_legend, show_params,
457
- style,
458
- line_color, point_color, line_style, marker_style,
459
- use_differential)
460
- figures.append(fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
  experiment_counter += 1
463
 
464
- elif mode == 'average':
465
- # Modo promedio: usar dataxp, datasp y datapp
466
- time = df[(df.columns.levels[0][0], 'Tiempo')].dropna().values
467
- biomass = model.dataxp[-1]
468
- substrate = model.datasp[-1]
469
- product = model.datapp[-1]
470
-
471
- # Obtener las desviaciones estándar
472
- biomass_std = model.datax_std[-1]
473
- substrate_std = model.datas_std[-1]
474
- product_std = model.datap_std[-1]
475
-
476
- # Obtener límites o usar valores predeterminados
477
- lower_bound = lower_bounds[experiment_counter] if experiment_counter < len(lower_bounds) else default_lower_bounds
478
- upper_bound = upper_bounds[experiment_counter] if experiment_counter < len(upper_bounds) else default_upper_bounds
479
- bounds = (lower_bound, upper_bound)
480
-
481
- # Ajustar el modelo
482
- y_pred_biomass = model.fit_biomass(time, biomass, bounds)
483
- y_pred_substrate = model.fit_substrate(time, substrate, model.params['biomass'], bounds)
484
- y_pred_product = model.fit_product(time, product, model.params['biomass'], bounds)
485
-
486
- # Usar el nombre del experimento proporcionado o un nombre por defecto
487
- experiment_name = experiment_names[experiment_counter] if experiment_counter < len(experiment_names) else f"Tratamiento {experiment_counter + 1}"
488
-
489
- if mode == 'combinado':
490
- fig = model.plot_combined_results(time, biomass, substrate, product,
491
- y_pred_biomass, y_pred_substrate, y_pred_product,
492
- biomass_std, substrate_std, product_std,
493
- experiment_name, legend_position, params_position,
494
- show_legend, show_params,
495
- style,
496
- line_color, point_color, line_style, marker_style,
497
- use_differential)
498
- else:
499
- fig = model.plot_results(time, biomass, substrate, product,
500
- y_pred_biomass, y_pred_substrate, y_pred_product,
501
- biomass_std, substrate_std, product_std,
502
- experiment_name, legend_position, params_position,
503
- show_legend, show_params,
504
- style,
505
- line_color, point_color, line_style, marker_style,
506
- use_differential)
507
- figures.append(fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
  experiment_counter += 1
510
 
511
- elif mode == 'combinado':
512
- # Modo combinado: combinar las gráficas en una sola
513
- time = df[(df.columns.levels[0][0], 'Tiempo')].dropna().values
514
- biomass = model.dataxp[-1]
515
- substrate = model.datasp[-1]
516
- product = model.datapp[-1]
517
-
518
- # Obtener las desviaciones estándar
519
- biomass_std = model.datax_std[-1]
520
- substrate_std = model.datas_std[-1]
521
- product_std = model.datap_std[-1]
522
-
523
- # Obtener límites o usar valores predeterminados
524
- lower_bound = lower_bounds[experiment_counter] if experiment_counter < len(lower_bounds) else default_lower_bounds
525
- upper_bound = upper_bounds[experiment_counter] if experiment_counter < len(upper_bounds) else default_upper_bounds
526
- bounds = (lower_bound, upper_bound)
527
-
528
- # Ajustar el modelo
529
- y_pred_biomass = model.fit_biomass(time, biomass, bounds)
530
- y_pred_substrate = model.fit_substrate(time, substrate, model.params['biomass'], bounds)
531
- y_pred_product = model.fit_product(time, product, model.params['biomass'], bounds)
532
-
533
- # Usar el nombre del experimento proporcionado o un nombre por defecto
534
- experiment_name = experiment_names[experiment_counter] if experiment_counter < len(experiment_names) else f"Tratamiento {experiment_counter + 1}"
535
-
536
- fig = model.plot_combined_results(time, biomass, substrate, product,
537
- y_pred_biomass, y_pred_substrate, y_pred_product,
538
- biomass_std, substrate_std, product_std,
539
- experiment_name, legend_position, params_position,
540
- show_legend, show_params,
541
- style,
542
- line_color, point_color, line_style, marker_style,
543
- use_differential)
544
- figures.append(fig)
545
 
546
- experiment_counter += 1
 
 
 
 
 
 
547
 
548
- return figures
549
 
550
  def create_interface():
551
- with gr.Blocks(theme='upsatwal/mlsc_tiet') as demo:
552
- gr.Markdown("# Modelos de Bioproceso: Logístico y Luedeking-Piret")
553
- gr.Markdown(
554
- "Sube un archivo Excel con múltiples pestañas. Cada pestaña debe contener columnas 'Tiempo', 'Biomasa', 'Sustrato' y 'Producto' para cada experimento.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
556
  file_input = gr.File(label="Subir archivo Excel")
557
 
@@ -573,9 +783,12 @@ def create_interface():
573
  )
574
  show_params = gr.Checkbox(label="Mostrar Parámetros", value=True)
575
 
576
- model_type = gr.Radio(["logistic"], label="Tipo de Modelo", value="logistic")
 
 
 
 
577
  mode = gr.Radio(["independent", "average", "combinado"], label="Modo de Análisis", value="independent")
578
-
579
  use_differential = gr.Checkbox(label="Usar ecuaciones diferenciales para graficar", value=False)
580
 
581
  experiment_names = gr.Textbox(
@@ -587,72 +800,92 @@ def create_interface():
587
  with gr.Row():
588
  with gr.Column():
589
  lower_bounds = gr.Textbox(
590
- label="Lower Bounds (uno por línea, formato: xo,xm,um)",
591
  placeholder="0,0,0\n0,0,0\n...",
592
  lines=5
593
  )
594
 
595
  with gr.Column():
596
  upper_bounds = gr.Textbox(
597
- label="Upper Bounds (uno por línea, formato: xo,xm,um)",
598
  placeholder="inf,inf,inf\ninf,inf,inf\n...",
599
  lines=5
600
  )
601
 
602
- # Añadir un desplegable para seleccionar el estilo del gráfico
603
  styles = ['white', 'dark', 'whitegrid', 'darkgrid', 'ticks']
604
  style_dropdown = gr.Dropdown(choices=styles, label="Selecciona el estilo de gráfico", value='whitegrid')
605
 
606
- # Añadir color pickers para líneas y puntos
607
  line_color_picker = gr.ColorPicker(label="Color de la línea", value='#0000FF')
608
  point_color_picker = gr.ColorPicker(label="Color de los puntos", value='#000000')
609
 
610
- # Añadir listas desplegables para tipo de línea y tipo de punto
611
  line_style_options = ['-', '--', '-.', ':']
612
  line_style_dropdown = gr.Dropdown(choices=line_style_options, label="Estilo de línea", value='-')
613
 
614
  marker_style_options = ['o', 's', '^', 'v', 'D', 'x', '+', '*']
615
  marker_style_dropdown = gr.Dropdown(choices=marker_style_options, label="Estilo de punto", value='o')
616
 
 
 
617
  simulate_btn = gr.Button("Simular")
618
 
619
- # Definir un componente gr.Gallery para las salidas
620
  output_gallery = gr.Gallery(label="Resultados", columns=2, height='auto')
 
 
 
 
 
 
 
 
621
 
622
- def process_and_plot(file, legend_position, params_position, model_type, mode, experiment_names,
623
  lower_bounds, upper_bounds, style,
624
  line_color, point_color, line_style, marker_style,
625
- show_legend, show_params, use_differential):
626
- # Dividir los nombres de experimentos y límites en listas
627
  experiment_names_list = experiment_names.strip().split('\n') if experiment_names.strip() else []
628
- lower_bounds_list = [tuple(map(float, lb.split(','))) for lb in
629
- lower_bounds.strip().split('\n')] if lower_bounds.strip() else []
630
- upper_bounds_list = [tuple(map(float, ub.split(','))) for ub in
631
- upper_bounds.strip().split('\n')] if upper_bounds.strip() else []
632
-
633
- # Procesar los datos y generar gráficos
634
- figures = process_data(file, legend_position, params_position, model_type, experiment_names_list,
635
- lower_bounds_list, upper_bounds_list, mode, style,
636
- line_color, point_color, line_style, marker_style,
637
- show_legend, show_params, use_differential)
638
-
639
- # Convertir las figuras a imágenes y devolverlas como lista
640
- image_list = []
641
- for fig in figures:
642
- buf = io.BytesIO()
643
- fig.savefig(buf, format='png')
644
- buf.seek(0)
645
- image = Image.open(buf)
646
- image_list.append(image)
647
-
648
- return image_list
649
-
650
- simulate_btn.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  fn=process_and_plot,
652
  inputs=[file_input,
653
  legend_position,
654
  params_position,
655
- model_type,
656
  mode,
657
  experiment_names,
658
  lower_bounds,
@@ -664,12 +897,28 @@ def create_interface():
664
  marker_style_dropdown,
665
  show_legend,
666
  show_params,
667
- use_differential],
668
- outputs=output_gallery
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  )
670
 
671
  return demo
672
 
673
- # Crear y lanzar la interfaz
674
  demo = create_interface()
675
  demo.launch(share=True)
 
1
+ #import os
2
+ #!pip install gradio seaborn scipy scikit-learn openpyxl pydantic==1.10.0 -q
 
 
 
 
 
3
 
4
  from pydantic import BaseModel, ConfigDict
 
 
 
 
 
5
  import numpy as np
6
  import pandas as pd
7
  import matplotlib.pyplot as plt
8
  import seaborn as sns
9
  from scipy.integrate import odeint
 
10
  from scipy.optimize import curve_fit
11
  from sklearn.metrics import mean_squared_error
12
  import gradio as gr
13
  import io
14
  from PIL import Image
15
+ import tempfile
16
+
17
+ class YourModel(BaseModel):
18
+ class Config:
19
+ arbitrary_types_allowed = True
20
 
 
21
  class BioprocessModel:
22
+ def __init__(self, model_type='logistic', maxfev=50000):
23
  self.params = {}
24
  self.r2 = {}
25
  self.rmse = {}
 
32
  self.datax_std = []
33
  self.datas_std = []
34
  self.datap_std = []
35
+ self.biomass_model = None
36
+ self.biomass_diff = None
37
+ self.model_type = model_type
38
+ self.maxfev = maxfev
39
 
 
40
  @staticmethod
41
  def logistic(time, xo, xm, um):
42
  return (xo * np.exp(um * time)) / (1 - (xo / xm) * (1 - np.exp(um * time)))
43
 
44
  @staticmethod
45
+ def gompertz(time, xm, um, lag):
46
+ return xm * np.exp(-np.exp((um * np.e / xm) * (lag - time) + 1))
 
47
 
48
  @staticmethod
49
+ def moser(time, Xm, um, Ks):
50
+ return Xm * (1 - np.exp(-um * (time - Ks)))
 
51
 
 
52
  @staticmethod
53
  def logistic_diff(X, t, params):
54
  xo, xm, um = params
55
+ return um * X * (1 - X / xm)
56
+
57
+ @staticmethod
58
+ def gompertz_diff(X, t, params):
59
+ xm, um, lag = params
60
+ return X * (um * np.e / xm) * np.exp((um * np.e / xm) * (lag - t) + 1)
61
+
62
+ @staticmethod
63
+ def moser_diff(X, t, params):
64
+ Xm, um, Ks = params
65
+ return um * (Xm - X)
66
+
67
+ def substrate(self, time, so, p, q, biomass_params):
68
+ X_t = self.biomass_model(time, *biomass_params)
69
+ dXdt = np.gradient(X_t, time)
70
+ integral_X = np.cumsum(X_t) * np.gradient(time)
71
+ return so - p * (X_t - biomass_params[0]) - q * integral_X
72
+
73
+ def product(self, time, po, alpha, beta, biomass_params):
74
+ X_t = self.biomass_model(time, *biomass_params)
75
+ dXdt = np.gradient(X_t, time)
76
+ integral_X = np.cumsum(X_t) * np.gradient(time)
77
+ return po + alpha * (X_t - biomass_params[0]) + beta * integral_X
78
+
79
  def process_data(self, df):
 
80
  biomass_cols = [col for col in df.columns if col[1] == 'Biomasa']
81
  substrate_cols = [col for col in df.columns if col[1] == 'Sustrato']
82
  product_cols = [col for col in df.columns if col[1] == 'Producto']
83
 
 
84
  time_col = [col for col in df.columns if col[1] == 'Tiempo'][0]
85
  time = df[time_col].values
86
 
 
87
  data_biomass = [df[col].values for col in biomass_cols]
88
+ data_biomass = np.array(data_biomass)
89
  self.datax.append(data_biomass)
90
  self.dataxp.append(np.mean(data_biomass, axis=0))
91
  self.datax_std.append(np.std(data_biomass, axis=0, ddof=1))
92
 
 
93
  data_substrate = [df[col].values for col in substrate_cols]
94
  data_substrate = np.array(data_substrate)
95
  self.datas.append(data_substrate)
96
  self.datasp.append(np.mean(data_substrate, axis=0))
97
  self.datas_std.append(np.std(data_substrate, axis=0, ddof=1))
98
 
 
99
  data_product = [df[col].values for col in product_cols]
100
  data_product = np.array(data_product)
101
  self.datap.append(data_product)
 
104
 
105
  self.time = time
106
 
107
+ def fit_model(self):
108
+ if self.model_type == 'logistic':
109
+ self.biomass_model = self.logistic
110
+ self.biomass_diff = self.logistic_diff
111
+ elif self.model_type == 'gompertz':
112
+ self.biomass_model = self.gompertz
113
+ self.biomass_diff = self.gompertz_diff
114
+ elif self.model_type == 'moser':
115
+ self.biomass_model = self.moser
116
+ self.biomass_diff = self.moser_diff
117
+
118
+ def fit_biomass(self, time, biomass):
119
+ try:
120
+ if self.model_type == 'logistic':
121
+ p0 = [min(biomass), max(biomass)*1.5 if max(biomass)>0 else 1.0, 0.1]
122
+ popt, _ = curve_fit(self.logistic, time, biomass, p0=p0, maxfev=self.maxfev)
123
+ self.params['biomass'] = {'xo': popt[0], 'xm': popt[1], 'um': popt[2]}
124
+ y_pred = self.logistic(time, *popt)
125
+ elif self.model_type == 'gompertz':
126
+ p0 = [max(biomass) if max(biomass)>0 else 1.0, 0.1, time[np.argmax(np.gradient(biomass))]]
127
+ popt, _ = curve_fit(self.gompertz, time, biomass, p0=p0, maxfev=self.maxfev)
128
+ self.params['biomass'] = {'xm': popt[0], 'um': popt[1], 'lag': popt[2]}
129
+ y_pred = self.gompertz(time, *popt)
130
+ elif self.model_type == 'moser':
131
+ p0 = [max(biomass) if max(biomass)>0 else 1.0, 0.1, min(time)]
132
+ popt, _ = curve_fit(self.moser, time, biomass, p0=p0, maxfev=self.maxfev)
133
+ self.params['biomass'] = {'Xm': popt[0], 'um': popt[1], 'Ks': popt[2]}
134
+ y_pred = self.moser(time, *popt)
135
+
136
+ self.r2['biomass'] = 1 - (np.sum((biomass - y_pred) ** 2) / np.sum((biomass - np.mean(biomass)) ** 2))
137
+ self.rmse['biomass'] = np.sqrt(mean_squared_error(biomass, y_pred))
138
+ return y_pred
139
+ except Exception as e:
140
+ print(f"Error en fit_biomass_{self.model_type}: {e}")
141
+ return None
142
+
143
+ def fit_substrate(self, time, substrate, biomass_params):
144
+ try:
145
+ if self.model_type == 'logistic':
146
+ p0 = [min(substrate), 0.01, 0.01]
147
+ popt, _ = curve_fit(
148
+ lambda t, so, p, q: self.substrate(t, so, p, q, [biomass_params['xo'], biomass_params['xm'], biomass_params['um']]),
149
+ time, substrate, p0=p0, maxfev=self.maxfev
150
+ )
151
+ self.params['substrate'] = {'so': popt[0], 'p': popt[1], 'q': popt[2]}
152
+ y_pred = self.substrate(time, *popt, [biomass_params['xo'], biomass_params['xm'], biomass_params['um']])
153
+ elif self.model_type == 'gompertz':
154
+ p0 = [min(substrate), 0.01, 0.01]
155
+ popt, _ = curve_fit(
156
+ lambda t, so, p, q: self.substrate(t, so, p, q, [biomass_params['xm'], biomass_params['um'], biomass_params['lag']]),
157
+ time, substrate, p0=p0, maxfev=self.maxfev
158
+ )
159
+ self.params['substrate'] = {'so': popt[0], 'p': popt[1], 'q': popt[2]}
160
+ y_pred = self.substrate(time, *popt, [biomass_params['xm'], biomass_params['um'], biomass_params['lag']])
161
+ elif self.model_type == 'moser':
162
+ p0 = [min(substrate), 0.01, 0.01]
163
+ popt, _ = curve_fit(
164
+ lambda t, so, p, q: self.substrate(t, so, p, q, [biomass_params['Xm'], biomass_params['um'], biomass_params['Ks']]),
165
+ time, substrate, p0=p0, maxfev=self.maxfev
166
+ )
167
+ self.params['substrate'] = {'so': popt[0], 'p': popt[1], 'q': popt[2]}
168
+ y_pred = self.substrate(time, *popt, [biomass_params['Xm'], biomass_params['um'], biomass_params['Ks']])
169
+ self.r2['substrate'] = 1 - (np.sum((substrate - y_pred) ** 2) / np.sum((substrate - np.mean(substrate)) ** 2))
170
+ self.rmse['substrate'] = np.sqrt(mean_squared_error(substrate, y_pred))
171
+ return y_pred
172
+ except Exception as e:
173
+ print(f"Error en fit_substrate_{self.model_type}: {e}")
174
+ return None
175
+
176
+ def fit_product(self, time, product, biomass_params):
177
+ try:
178
+ if self.model_type == 'logistic':
179
+ p0 = [min(product), 0.01, 0.01]
180
+ popt, _ = curve_fit(
181
+ lambda t, po, alpha, beta: self.product(t, po, alpha, beta, [biomass_params['xo'], biomass_params['xm'], biomass_params['um']]),
182
+ time, product, p0=p0, maxfev=self.maxfev
183
+ )
184
+ self.params['product'] = {'po': popt[0], 'alpha': popt[1], 'beta': popt[2]}
185
+ y_pred = self.product(time, *popt, [biomass_params['xo'], biomass_params['xm'], biomass_params['um']])
186
+ elif self.model_type == 'gompertz':
187
+ p0 = [min(product), 0.01, 0.01]
188
+ popt, _ = curve_fit(
189
+ lambda t, po, alpha, beta: self.product(t, po, alpha, beta, [biomass_params['xm'], biomass_params['um'], biomass_params['lag']]),
190
+ time, product, p0=p0, maxfev=self.maxfev
191
+ )
192
+ self.params['product'] = {'po': popt[0], 'alpha': popt[1], 'beta': popt[2]}
193
+ y_pred = self.product(time, *popt, [biomass_params['xm'], biomass_params['um'], biomass_params['lag']])
194
+ elif self.model_type == 'moser':
195
+ p0 = [min(product), 0.01, 0.01]
196
+ popt, _ = curve_fit(
197
+ lambda t, po, alpha, beta: self.product(t, po, alpha, beta, [biomass_params['Xm'], biomass_params['um'], biomass_params['Ks']]),
198
+ time, product, p0=p0, maxfev=self.maxfev
199
+ )
200
+ self.params['product'] = {'po': popt[0], 'alpha': popt[1], 'beta': popt[2]}
201
+ y_pred = self.product(time, *popt, [biomass_params['Xm'], biomass_params['um'], biomass_params['Ks']])
202
+ self.r2['product'] = 1 - (np.sum((product - y_pred) ** 2) / np.sum((product - np.mean(product)) ** 2))
203
+ self.rmse['product'] = np.sqrt(mean_squared_error(product, y_pred))
204
+ return y_pred
205
+ except Exception as e:
206
+ print(f"Error en fit_product_{self.model_type}: {e}")
207
+ return None
208
+
209
  def generate_fine_time_grid(self, time):
 
210
  time_fine = np.linspace(time.min(), time.max(), 500)
211
  return time_fine
212
 
213
+ def system(self, y, t, biomass_params, substrate_params, product_params, model_type):
214
+ X, S, P = y
215
+
216
+ if model_type == 'logistic':
217
+ dXdt = self.logistic_diff(X, t, biomass_params)
218
+ elif model_type == 'gompertz':
219
+ dXdt = self.gompertz_diff(X, t, biomass_params)
220
+ elif model_type == 'moser':
221
+ dXdt = self.moser_diff(X, t, biomass_params)
222
+ else:
223
+ dXdt = 0.0
224
+
225
+ so, p, q = substrate_params
226
+ po, alpha, beta = product_params
227
+
228
+ dSdt = -p * dXdt - q * X
229
+ dPdt = alpha * dXdt + beta * X
230
+ return [dXdt, dSdt, dPdt]
231
+
232
+ def get_initial_conditions(self, time, biomass, substrate, product):
233
+ if 'biomass' in self.params:
234
+ if self.model_type == 'logistic':
235
+ xo = self.params['biomass']['xo']
236
+ X0 = xo
237
+ elif self.model_type == 'gompertz':
238
+ xm = self.params['biomass']['xm']
239
+ um = self.params['biomass']['um']
240
+ lag = self.params['biomass']['lag']
241
+ X0 = xm * np.exp(-np.exp((um * np.e / xm)*(lag - 0)+1))
242
+ elif self.model_type == 'moser':
243
+ Xm = self.params['biomass']['Xm']
244
+ um = self.params['biomass']['um']
245
+ Ks = self.params['biomass']['Ks']
246
+ X0 = Xm*(1 - np.exp(-um*(0 - Ks)))
247
+ else:
248
+ X0 = biomass[0]
249
+
250
+ if 'substrate' in self.params:
251
+ so = self.params['substrate']['so']
252
+ S0 = so
253
+ else:
254
+ S0 = substrate[0]
255
+
256
+ if 'product' in self.params:
257
+ po = self.params['product']['po']
258
+ P0 = po
259
+ else:
260
+ P0 = product[0]
261
+
262
+ return [X0, S0, P0]
263
 
264
+ def solve_differential_equations(self, time, biomass, substrate, product):
265
+ if 'biomass' not in self.params or not self.params['biomass']:
266
+ print("No hay parámetros de biomasa, no se pueden resolver las EDO.")
267
+ return None, None, None, time
268
 
269
+ if self.model_type == 'logistic':
270
+ biomass_params = [self.params['biomass']['xo'], self.params['biomass']['xm'], self.params['biomass']['um']]
271
+ elif self.model_type == 'gompertz':
272
+ biomass_params = [self.params['biomass']['xm'], self.params['biomass']['um'], self.params['biomass']['lag']]
273
+ elif self.model_type == 'moser':
274
+ biomass_params = [self.params['biomass']['Xm'], self.params['biomass']['um'], self.params['biomass']['Ks']]
275
+ else:
276
+ biomass_params = [0,0,0]
277
+
278
+ if 'substrate' in self.params:
279
+ substrate_params = [self.params['substrate']['so'], self.params['substrate']['p'], self.params['substrate']['q']]
280
+ else:
281
+ substrate_params = [0,0,0]
282
 
283
+ if 'product' in self.params:
284
+ product_params = [self.params['product']['po'], self.params['product']['alpha'], self.params['product']['beta']]
285
+ else:
286
+ product_params = [0,0,0]
287
+
288
+ initial_conditions = self.get_initial_conditions(time, biomass, substrate, product)
289
+ time_fine = self.generate_fine_time_grid(time)
290
+ sol = odeint(self.system, initial_conditions, time_fine,
291
+ args=(biomass_params, substrate_params, product_params, self.model_type))
292
 
293
+ X = sol[:, 0]
294
+ S = sol[:, 1]
295
+ P = sol[:, 2]
 
 
296
 
297
  return X, S, P, time_fine
298
 
 
304
  style='whitegrid',
305
  line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
306
  use_differential=False):
 
307
 
308
+ if y_pred_biomass is None:
309
+ print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type}. Omitiendo figura.")
310
+ return None
311
+
312
+ sns.set_style(style)
313
+
314
+ if use_differential and 'biomass' in self.params and self.params['biomass']:
315
+ X, S, P, time_to_plot = self.solve_differential_equations(time, biomass, substrate, product)
316
+ if X is not None:
317
+ y_pred_biomass, y_pred_substrate, y_pred_product = X, S, P
318
+ else:
319
+ time_to_plot = time
320
  else:
321
  time_to_plot = time
322
 
 
324
  fig.suptitle(f'{experiment_name}', fontsize=16)
325
 
326
  plots = [
327
+ (ax1, biomass, y_pred_biomass, biomass_std, 'Biomasa', 'Modelo', self.params.get('biomass', {}),
328
+ self.r2.get('biomass', np.nan), self.rmse.get('biomass', np.nan)),
329
+ (ax2, substrate, y_pred_substrate, substrate_std, 'Sustrato', 'Modelo', self.params.get('substrate', {}),
330
+ self.r2.get('substrate', np.nan), self.rmse.get('substrate', np.nan)),
331
+ (ax3, product, y_pred_product, product_std, 'Producto', 'Modelo', self.params.get('product', {}),
332
+ self.r2.get('product', np.nan), self.rmse.get('product', np.nan))
333
  ]
334
 
335
  for idx, (ax, data, y_pred, data_std, ylabel, model_name, params, r2, rmse) in enumerate(plots):
 
339
  else:
340
  ax.plot(time, data, marker=marker_style, linestyle='', color=point_color,
341
  label='Datos experimentales')
342
+
343
+ if y_pred is not None:
344
  ax.plot(time_to_plot, y_pred, linestyle=line_style, color=line_color, label=model_name)
345
+
 
346
  ax.set_xlabel('Tiempo')
347
  ax.set_ylabel(ylabel)
348
  if show_legend:
349
  ax.legend(loc=legend_position)
350
  ax.set_title(f'{ylabel}')
351
 
352
+ if show_params and params and all(np.isfinite(list(map(float, params.values())))):
353
+ param_text = '\n'.join([f"{k} = {v:.3f}" for k, v in params.items()])
354
+ text = f"{param_text}\nR² = {r2:.3f}\nRMSE = {rmse:.3f}"
 
 
355
  if params_position == 'outside right':
356
  bbox_props = dict(boxstyle='round', facecolor='white', alpha=0.5)
357
  ax.annotate(text, xy=(1.05, 0.5), xycoords='axes fraction',
 
373
 
374
  ax.text(text_x, text_y, text, transform=ax.transAxes,
375
  verticalalignment=va, horizontalalignment=ha,
376
+ bbox={'boxstyle': 'round', 'facecolor':'white', 'alpha':0.5})
377
+
378
+ plt.tight_layout(rect=[0, 0.03, 1, 0.95])
379
 
380
+ buf = io.BytesIO()
381
+ fig.savefig(buf, format='png')
382
+ buf.seek(0)
383
+ image = Image.open(buf).convert("RGB")
384
+ plt.close(fig)
385
+
386
+ return image
387
 
388
  def plot_combined_results(self, time, biomass, substrate, product,
389
  y_pred_biomass, y_pred_substrate, y_pred_product,
 
393
  style='whitegrid',
394
  line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
395
  use_differential=False):
 
396
 
397
+ if y_pred_biomass is None:
398
+ print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type}. Omitiendo figura.")
399
+ return None
400
+
401
+ sns.set_style(style)
402
+
403
+ if use_differential and 'biomass' in self.params and self.params['biomass']:
404
+ X, S, P, time_to_plot = self.solve_differential_equations(time, biomass, substrate, product)
405
+ if X is not None:
406
+ y_pred_biomass, y_pred_substrate, y_pred_product = X, S, P
407
+ else:
408
+ time_to_plot = time
409
  else:
410
  time_to_plot = time
411
 
412
  fig, ax1 = plt.subplots(figsize=(10, 7))
413
  fig.suptitle(f'{experiment_name}', fontsize=16)
414
 
 
415
  colors = {'Biomasa': 'blue', 'Sustrato': 'green', 'Producto': 'red'}
416
 
 
417
  ax1.set_xlabel('Tiempo')
418
  ax1.set_ylabel('Biomasa', color=colors['Biomasa'])
419
  if biomass_std is not None:
 
422
  else:
423
  ax1.plot(time, biomass, marker=marker_style, linestyle='', color=colors['Biomasa'],
424
  label='Biomasa (Datos)')
425
+ ax1.plot(time_to_plot, y_pred_biomass, linestyle=line_style, color=colors['Biomasa'],
426
+ label='Biomasa (Modelo)')
 
 
 
 
427
  ax1.tick_params(axis='y', labelcolor=colors['Biomasa'])
428
 
 
429
  ax2 = ax1.twinx()
430
  ax2.set_ylabel('Sustrato', color=colors['Sustrato'])
431
  if substrate_std is not None:
 
434
  else:
435
  ax2.plot(time, substrate, marker=marker_style, linestyle='', color=colors['Sustrato'],
436
  label='Sustrato (Datos)')
437
+ if y_pred_substrate is not None:
438
  ax2.plot(time_to_plot, y_pred_substrate, linestyle=line_style, color=colors['Sustrato'],
439
  label='Sustrato (Modelo)')
 
 
 
440
  ax2.tick_params(axis='y', labelcolor=colors['Sustrato'])
441
 
 
442
  ax3 = ax1.twinx()
443
+ ax3.spines["right"].set_position(("axes", 1.2))
 
444
  ax3.set_frame_on(True)
445
  ax3.patch.set_visible(False)
446
  for sp in ax3.spines.values():
 
453
  else:
454
  ax3.plot(time, product, marker=marker_style, linestyle='', color=colors['Producto'],
455
  label='Producto (Datos)')
456
+ if y_pred_product is not None:
457
  ax3.plot(time_to_plot, y_pred_product, linestyle=line_style, color=colors['Producto'],
458
  label='Producto (Modelo)')
 
 
 
459
  ax3.tick_params(axis='y', labelcolor=colors['Producto'])
460
 
 
461
  lines_labels = [ax.get_legend_handles_labels() for ax in [ax1, ax2, ax3]]
462
  lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
463
  if show_legend:
464
  ax1.legend(lines, labels, loc=legend_position)
465
 
 
466
  if show_params:
467
+ param_text_biomass = ''
468
+ if 'biomass' in self.params:
469
+ param_text_biomass = '\n'.join([f"{k} = {v:.3f}" for k, v in self.params['biomass'].items()])
470
+ text_biomass = f"Biomasa:\n{param_text_biomass}\nR² = {self.r2.get('biomass', np.nan):.3f}\nRMSE = {self.rmse.get('biomass', np.nan):.3f}"
471
 
472
+ param_text_substrate = ''
473
+ if 'substrate' in self.params:
474
+ param_text_substrate = '\n'.join([f"{k} = {v:.3f}" for k, v in self.params['substrate'].items()])
475
+ text_substrate = f"Sustrato:\n{param_text_substrate}\nR² = {self.r2.get('substrate', np.nan):.3f}\nRMSE = {self.rmse.get('substrate', np.nan):.3f}"
476
 
477
+ param_text_product = ''
478
+ if 'product' in self.params:
479
+ param_text_product = '\n'.join([f"{k} = {v:.3f}" for k, v in self.params['product'].items()])
480
+ text_product = f"Producto:\n{param_text_product}\nR² = {self.r2.get('product', np.nan):.3f}\nRMSE = {self.rmse.get('product', np.nan):.3f}"
481
 
482
  total_text = f"{text_biomass}\n\n{text_substrate}\n\n{text_product}"
483
 
 
502
 
503
  ax1.text(text_x, text_y, total_text, transform=ax1.transAxes,
504
  verticalalignment=va, horizontalalignment=ha,
505
+ bbox={'boxstyle':'round', 'facecolor':'white', 'alpha':0.5})
506
 
507
+ plt.tight_layout(rect=[0, 0.03, 1, 0.95])
 
508
 
509
+ buf = io.BytesIO()
510
+ fig.savefig(buf, format='png')
511
+ buf.seek(0)
512
+ image = Image.open(buf).convert("RGB")
513
+ plt.close(fig)
 
 
514
 
515
+ return image
 
 
516
 
517
+ def process_all_data(file, legend_position, params_position, model_types, experiment_names, lower_bounds, upper_bounds,
518
+ mode='independent', style='whitegrid', line_color='#0000FF', point_color='#000000',
519
+ line_style='-', marker_style='o', show_legend=True, show_params=True, use_differential=False, maxfev_val=50000):
520
 
521
+ try:
522
+ xls = pd.ExcelFile(file.name)
523
+ except Exception as e:
524
+ print(f"Error al leer el archivo Excel: {e}")
525
+ return [], pd.DataFrame()
526
+
527
+ sheet_names = xls.sheet_names
528
+ figures = []
529
+ comparison_data = []
530
+ experiment_counter = 0
531
 
532
  for sheet_name in sheet_names:
533
+ try:
534
+ df = pd.read_excel(file.name, sheet_name=sheet_name, header=[0, 1])
535
+ except Exception as e:
536
+ print(f"Error al leer la hoja '{sheet_name}': {e}")
537
+ continue
538
 
539
+ model_dummy = BioprocessModel()
540
+ model_dummy.process_data(df)
541
+ time = model_dummy.time
542
 
543
  if mode == 'independent':
 
544
  num_experiments = len(df.columns.levels[0])
545
  for idx in range(num_experiments):
546
  col = df.columns.levels[0][idx]
547
+ try:
548
+ time_exp = df[(col, 'Tiempo')].dropna().values
549
+ biomass = df[(col, 'Biomasa')].dropna().values
550
+ substrate = df[(col, 'Sustrato')].dropna().values
551
+ product = df[(col, 'Producto')].dropna().values
552
+ except KeyError as e:
553
+ print(f"Error al procesar el experimento '{col}': {e}")
554
+ continue
555
 
 
556
  biomass_std = None
557
  substrate_std = None
558
  product_std = None
 
566
  product_std = np.std(product, axis=0, ddof=1)
567
  product = np.mean(product, axis=0)
568
 
569
+ experiment_name = (experiment_names[experiment_counter] if experiment_counter < len(experiment_names)
570
+ else f"Tratamiento {experiment_counter + 1}")
571
+
572
+ for model_type in model_types:
573
+ model = BioprocessModel(model_type=model_type, maxfev=maxfev_val)
574
+ model.fit_model()
575
+
576
+ y_pred_biomass = model.fit_biomass(time_exp, biomass)
577
+ if y_pred_biomass is None:
578
+ comparison_data.append({
579
+ 'Experimento': experiment_name,
580
+ 'Modelo': model_type.capitalize(),
581
+ 'R² Biomasa': np.nan,
582
+ 'RMSE Biomasa': np.nan,
583
+ 'R² Sustrato': np.nan,
584
+ 'RMSE Sustrato': np.nan,
585
+ 'R² Producto': np.nan,
586
+ 'RMSE Producto': np.nan
587
+ })
588
+ continue
589
+ else:
590
+ if 'biomass' in model.params and model.params['biomass']:
591
+ y_pred_substrate = model.fit_substrate(time_exp, substrate, model.params['biomass'])
592
+ y_pred_product = model.fit_product(time_exp, product, model.params['biomass'])
593
+ else:
594
+ y_pred_substrate = None
595
+ y_pred_product = None
596
+
597
+ comparison_data.append({
598
+ 'Experimento': experiment_name,
599
+ 'Modelo': model_type.capitalize(),
600
+ 'R² Biomasa': model.r2.get('biomass', np.nan),
601
+ 'RMSE Biomasa': model.rmse.get('biomass', np.nan),
602
+ 'R² Sustrato': model.r2.get('substrate', np.nan),
603
+ 'RMSE Sustrato': model.rmse.get('substrate', np.nan),
604
+ 'R² Producto': model.r2.get('product', np.nan),
605
+ 'RMSE Producto': model.rmse.get('product', np.nan)
606
+ })
607
+
608
+ if mode == 'combinado':
609
+ fig = model.plot_combined_results(time_exp, biomass, substrate, product,
610
+ y_pred_biomass, y_pred_substrate, y_pred_product,
611
+ biomass_std, substrate_std, product_std,
612
+ experiment_name,
613
+ legend_position, params_position,
614
+ show_legend, show_params,
615
+ style,
616
+ line_color, point_color, line_style, marker_style,
617
+ use_differential)
618
+ else:
619
+ fig = model.plot_results(time_exp, biomass, substrate, product,
620
+ y_pred_biomass, y_pred_substrate, y_pred_product,
621
+ biomass_std, substrate_std, product_std,
622
+ experiment_name,
623
+ legend_position, params_position,
624
+ show_legend, show_params,
625
+ style,
626
+ line_color, point_color, line_style, marker_style,
627
+ use_differential)
628
+ if fig is not None:
629
+ figures.append(fig)
630
 
631
  experiment_counter += 1
632
 
633
+ elif mode in ['average', 'combinado']:
634
+ try:
635
+ time_exp = df[(df.columns.levels[0][0], 'Tiempo')].dropna().values
636
+ biomass = model_dummy.dataxp[-1]
637
+ substrate = model_dummy.datasp[-1]
638
+ product = model_dummy.datapp[-1]
639
+ except IndexError as e:
640
+ print(f"Error al obtener los datos promedio de la hoja '{sheet_name}': {e}")
641
+ continue
642
+
643
+ biomass_std = model_dummy.datax_std[-1]
644
+ substrate_std = model_dummy.datas_std[-1]
645
+ product_std = model_dummy.datap_std[-1]
646
+
647
+ experiment_name = (experiment_names[experiment_counter] if experiment_counter < len(experiment_names)
648
+ else f"Tratamiento {experiment_counter + 1}")
649
+
650
+ for model_type in model_types:
651
+ model = BioprocessModel(model_type=model_type, maxfev=maxfev_val)
652
+ model.fit_model()
653
+
654
+ y_pred_biomass = model.fit_biomass(time_exp, biomass)
655
+ if y_pred_biomass is None:
656
+ comparison_data.append({
657
+ 'Experimento': experiment_name,
658
+ 'Modelo': model_type.capitalize(),
659
+ 'R² Biomasa': np.nan,
660
+ 'RMSE Biomasa': np.nan,
661
+ 'R² Sustrato': np.nan,
662
+ 'RMSE Sustrato': np.nan,
663
+ 'R² Producto': np.nan,
664
+ 'RMSE Producto': np.nan
665
+ })
666
+ continue
667
+ else:
668
+ if 'biomass' in model.params and model.params['biomass']:
669
+ y_pred_substrate = model.fit_substrate(time_exp, substrate, model.params['biomass'])
670
+ y_pred_product = model.fit_product(time_exp, product, model.params['biomass'])
671
+ else:
672
+ y_pred_substrate = None
673
+ y_pred_product = None
674
+
675
+ comparison_data.append({
676
+ 'Experimento': experiment_name,
677
+ 'Modelo': model_type.capitalize(),
678
+ 'R² Biomasa': model.r2.get('biomass', np.nan),
679
+ 'RMSE Biomasa': model.rmse.get('biomass', np.nan),
680
+ 'R² Sustrato': model.r2.get('substrate', np.nan),
681
+ 'RMSE Sustrato': model.rmse.get('substrate', np.nan),
682
+ 'R² Producto': model.r2.get('product', np.nan),
683
+ 'RMSE Producto': model.rmse.get('product', np.nan)
684
+ })
685
+
686
+ if mode == 'combinado':
687
+ fig = model.plot_combined_results(time_exp, biomass, substrate, product,
688
+ y_pred_biomass, y_pred_substrate, y_pred_product,
689
+ biomass_std, substrate_std, product_std,
690
+ experiment_name,
691
+ legend_position, params_position,
692
+ show_legend, show_params,
693
+ style,
694
+ line_color, point_color, line_style, marker_style,
695
+ use_differential)
696
+ else:
697
+ fig = model.plot_results(time_exp, biomass, substrate, product,
698
+ y_pred_biomass, y_pred_substrate, y_pred_product,
699
+ biomass_std, substrate_std, product_std,
700
+ experiment_name,
701
+ legend_position, params_position,
702
+ show_legend, show_params,
703
+ style,
704
+ line_color, point_color, line_style, marker_style,
705
+ use_differential)
706
+ if fig is not None:
707
+ figures.append(fig)
708
 
709
  experiment_counter += 1
710
 
711
+ comparison_df = pd.DataFrame(comparison_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
+ if not comparison_df.empty:
714
+ comparison_df_sorted = comparison_df.sort_values(
715
+ by=['R² Biomasa', 'R² Sustrato', 'R² Producto', 'RMSE Biomasa', 'RMSE Sustrato', 'RMSE Producto'],
716
+ ascending=[False, False, False, True, True, True]
717
+ ).reset_index(drop=True)
718
+ else:
719
+ comparison_df_sorted = comparison_df
720
 
721
+ return figures, comparison_df_sorted
722
 
723
  def create_interface():
724
+ with gr.Blocks() as demo:
725
+ gr.Markdown("# Modelos de Bioproceso: Logístico, Gompertz, Moser y Luedeking-Piret")
726
+
727
+ gr.Markdown(r"""
728
+ ## Ecuaciones Diferenciales Utilizadas
729
+
730
+ **Biomasa:**
731
+
732
+ - Logístico:
733
+ $$
734
+ \frac{dX}{dt} = \mu_m X\left(1 - \frac{X}{X_m}\right)
735
+ $$
736
+
737
+ - Gompertz:
738
+ $$
739
+ X(t) = X_m \exp\left(-\exp\left(\left(\frac{\mu_m e}{X_m}\right)(\text{lag}-t)+1\right)\right)
740
+ $$
741
+
742
+ Ecuación diferencial:
743
+ $$
744
+ \frac{dX}{dt} = X(t)\left(\frac{\mu_m e}{X_m}\right)\exp\left(\left(\frac{\mu_m e}{X_m}\right)(\text{lag}-t)+1\right)
745
+ $$
746
+
747
+ - Moser (simplificado):
748
+ $$
749
+ X(t)=X_m(1-e^{-\mu_m(t-K_s)})
750
+ $$
751
+
752
+ $$
753
+ \frac{dX}{dt}=\mu_m(X_m - X)
754
+ $$
755
+
756
+ **Sustrato y Producto (Luedeking-Piret):**
757
+ $$
758
+ \frac{dS}{dt} = -p \frac{dX}{dt} - q X
759
+ $$
760
+
761
+ $$
762
+ \frac{dP}{dt} = \alpha \frac{dX}{dt} + \beta X
763
+ $$
764
+ """)
765
 
766
  file_input = gr.File(label="Subir archivo Excel")
767
 
 
783
  )
784
  show_params = gr.Checkbox(label="Mostrar Parámetros", value=True)
785
 
786
+ model_types = gr.CheckboxGroup(
787
+ choices=["logistic", "gompertz", "moser"],
788
+ label="Tipo(s) de Modelo",
789
+ value=["logistic"]
790
+ )
791
  mode = gr.Radio(["independent", "average", "combinado"], label="Modo de Análisis", value="independent")
 
792
  use_differential = gr.Checkbox(label="Usar ecuaciones diferenciales para graficar", value=False)
793
 
794
  experiment_names = gr.Textbox(
 
800
  with gr.Row():
801
  with gr.Column():
802
  lower_bounds = gr.Textbox(
803
+ label="Lower Bounds (uno por línea, formato: param1,param2,param3)",
804
  placeholder="0,0,0\n0,0,0\n...",
805
  lines=5
806
  )
807
 
808
  with gr.Column():
809
  upper_bounds = gr.Textbox(
810
+ label="Upper Bounds (uno por línea, formato: param1,param2,param3)",
811
  placeholder="inf,inf,inf\ninf,inf,inf\n...",
812
  lines=5
813
  )
814
 
 
815
  styles = ['white', 'dark', 'whitegrid', 'darkgrid', 'ticks']
816
  style_dropdown = gr.Dropdown(choices=styles, label="Selecciona el estilo de gráfico", value='whitegrid')
817
 
 
818
  line_color_picker = gr.ColorPicker(label="Color de la línea", value='#0000FF')
819
  point_color_picker = gr.ColorPicker(label="Color de los puntos", value='#000000')
820
 
 
821
  line_style_options = ['-', '--', '-.', ':']
822
  line_style_dropdown = gr.Dropdown(choices=line_style_options, label="Estilo de línea", value='-')
823
 
824
  marker_style_options = ['o', 's', '^', 'v', 'D', 'x', '+', '*']
825
  marker_style_dropdown = gr.Dropdown(choices=marker_style_options, label="Estilo de punto", value='o')
826
 
827
+ maxfev_input = gr.Number(label="maxfev (Máx. evaluaciones para el ajuste)", value=50000)
828
+
829
  simulate_btn = gr.Button("Simular")
830
 
 
831
  output_gallery = gr.Gallery(label="Resultados", columns=2, height='auto')
832
+ output_table = gr.Dataframe(
833
+ label="Tabla Comparativa de Modelos",
834
+ headers=["Experimento", "Modelo", "R² Biomasa", "RMSE Biomasa",
835
+ "R² Sustrato", "RMSE Sustrato", "R² Producto", "RMSE Producto"],
836
+ interactive=False
837
+ )
838
+
839
+ state_df = gr.State()
840
 
841
+ def process_and_plot(file, legend_position, params_position, model_types, mode, experiment_names,
842
  lower_bounds, upper_bounds, style,
843
  line_color, point_color, line_style, marker_style,
844
+ show_legend, show_params, use_differential, maxfev_input):
845
+
846
  experiment_names_list = experiment_names.strip().split('\n') if experiment_names.strip() else []
847
+ lower_bounds_list = []
848
+ if lower_bounds.strip():
849
+ for lb in lower_bounds.strip().split('\n'):
850
+ lb_values = []
851
+ for val in lb.split(','):
852
+ val = val.strip().lower()
853
+ if val in ['inf', 'infty', 'infinity']:
854
+ lb_values.append(-np.inf)
855
+ else:
856
+ try:
857
+ lb_values.append(float(val))
858
+ except ValueError:
859
+ lb_values.append(0.0)
860
+ lower_bounds_list.append(tuple(lb_values))
861
+ upper_bounds_list = []
862
+ if upper_bounds.strip():
863
+ for ub in upper_bounds.strip().split('\n'):
864
+ ub_values = []
865
+ for val in ub.split(','):
866
+ val = val.strip().lower()
867
+ if val in ['inf', 'infty', 'infinity']:
868
+ ub_values.append(np.inf)
869
+ else:
870
+ try:
871
+ ub_values.append(float(val))
872
+ except ValueError:
873
+ ub_values.append(np.inf)
874
+ upper_bounds_list.append(tuple(ub_values))
875
+
876
+ figures, comparison_df = process_all_data(file, legend_position, params_position, model_types, experiment_names_list,
877
+ lower_bounds_list, upper_bounds_list, mode, style,
878
+ line_color, point_color, line_style, marker_style,
879
+ show_legend, show_params, use_differential, maxfev_val=int(maxfev_input))
880
+
881
+ return figures, comparison_df, comparison_df
882
+
883
+ simulate_output = simulate_btn.click(
884
  fn=process_and_plot,
885
  inputs=[file_input,
886
  legend_position,
887
  params_position,
888
+ model_types,
889
  mode,
890
  experiment_names,
891
  lower_bounds,
 
897
  marker_style_dropdown,
898
  show_legend,
899
  show_params,
900
+ use_differential,
901
+ maxfev_input],
902
+ outputs=[output_gallery, output_table, state_df]
903
+ )
904
+
905
+ def export_excel(df):
906
+ if df.empty:
907
+ return None
908
+ with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
909
+ df.to_excel(tmp.name, index=False)
910
+ return tmp.name
911
+
912
+ export_btn = gr.Button("Exportar Tabla a Excel")
913
+ file_output = gr.File()
914
+
915
+ export_btn.click(
916
+ fn=export_excel,
917
+ inputs=state_df,
918
+ outputs=file_output
919
  )
920
 
921
  return demo
922
 
 
923
  demo = create_interface()
924
  demo.launch(share=True)