C2MV commited on
Commit
c4f09ad
·
verified ·
1 Parent(s): 9929579

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +225 -1319
app.py CHANGED
@@ -1,1374 +1,280 @@
1
- # --- INSTALACIÓN DE DEPENDENCIAS ADICIONALES ---
2
- import os
3
- import sys
4
- import subprocess
5
-
6
- os.system("pip install gradio==5.38.1")
7
-
8
- # --- IMPORTACIONES ---
9
- import os
10
- import io
11
- import tempfile
12
- import traceback
13
- import zipfile
14
- from typing import List, Tuple, Dict, Any, Optional, Union
15
- from abc import ABC, abstractmethod
16
- from unittest.mock import MagicMock
17
- from dataclasses import dataclass
18
- from enum import Enum
19
- import json
20
-
21
- from PIL import Image
22
  import gradio as gr
 
 
23
  import plotly.graph_objects as go
24
  from plotly.subplots import make_subplots
25
- import numpy as np
26
- import pandas as pd
27
- import matplotlib.pyplot as plt
28
- import seaborn as sns
29
- from scipy.integrate import odeint
30
  from scipy.optimize import curve_fit, differential_evolution
31
  from sklearn.metrics import mean_squared_error, r2_score
32
- from docx import Document
33
- from docx.shared import Inches
34
- from fpdf import FPDF
35
- from fpdf.enums import XPos, YPos
36
- from fastapi import FastAPI
37
- import uvicorn
38
-
39
- # --- SISTEMA DE INTERNACIONALIZACIÓN ---
40
- class Language(Enum):
41
- ES = "Español"
42
- EN = "English"
43
- PT = "Português"
44
- FR = "Français"
45
- DE = "Deutsch"
46
- ZH = "中文"
47
- JA = "日本語"
48
-
49
- TRANSLATIONS = {
50
- Language.ES: {
51
- "title": "🔬 Analizador de Cinéticas de Bioprocesos",
52
- "subtitle": "Análisis avanzado de modelos matemáticos biotecnológicos",
53
- "welcome": "Bienvenido al Analizador de Cinéticas",
54
- "upload": "Sube tu archivo Excel (.xlsx)",
55
- "select_models": "Modelos a Probar",
56
- "analysis_mode": "Modo de Análisis",
57
- "analyze": "Analizar y Graficar",
58
- "results": "Resultados",
59
- "download": "Descargar",
60
- "biomass": "Biomasa",
61
- "substrate": "Sustrato",
62
- "product": "Producto",
63
- "time": "Tiempo",
64
- "parameters": "Parámetros",
65
- "model_comparison": "Comparación de Modelos",
66
- "dark_mode": "Modo Oscuro",
67
- "light_mode": "Modo Claro",
68
- "language": "Idioma",
69
- "theory": "Teoría y Modelos",
70
- "guide": "Guía de Uso",
71
- "api_docs": "Documentación API"
72
- },
73
- Language.EN: {
74
- "title": "🔬 Bioprocess Kinetics Analyzer",
75
- "subtitle": "Advanced analysis of biotechnological mathematical models",
76
- "welcome": "Welcome to the Kinetics Analyzer",
77
- "upload": "Upload your Excel file (.xlsx)",
78
- "select_models": "Models to Test",
79
- "analysis_mode": "Analysis Mode",
80
- "analyze": "Analyze and Plot",
81
- "results": "Results",
82
- "download": "Download",
83
- "biomass": "Biomass",
84
- "substrate": "Substrate",
85
- "product": "Product",
86
- "time": "Time",
87
- "parameters": "Parameters",
88
- "model_comparison": "Model Comparison",
89
- "dark_mode": "Dark Mode",
90
- "light_mode": "Light Mode",
91
- "language": "Language",
92
- "theory": "Theory and Models",
93
- "guide": "User Guide",
94
- "api_docs": "API Documentation"
95
- },
96
- }
97
-
98
- # --- CONSTANTES MEJORADAS ---
99
- C_TIME = 'tiempo'
100
- C_BIOMASS = 'biomass'
101
- C_SUBSTRATE = 'substrate'
102
- C_PRODUCT = 'product'
103
- C_OXYGEN = 'oxygen'
104
- C_CO2 = 'co2'
105
- C_PH = 'ph'
106
- COMPONENTS = [C_BIOMASS, C_SUBSTRATE, C_PRODUCT]
107
-
108
- # --- SISTEMA DE TEMAS ---
109
- THEMES = {
110
- "light": gr.themes.Soft(
111
- primary_hue="blue",
112
- secondary_hue="sky",
113
- neutral_hue="gray",
114
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "sans-serif"]
115
- ),
116
- "dark": gr.themes.Base(
117
- primary_hue="blue",
118
- secondary_hue="cyan",
119
- neutral_hue="slate",
120
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "sans-serif"]
121
- ).set(
122
- body_background_fill="*neutral_950",
123
- body_background_fill_dark="*neutral_950",
124
- button_primary_background_fill="*primary_600",
125
- button_primary_background_fill_hover="*primary_700",
126
- )
127
- }
128
-
129
- # --- MODELOS CINÉTICOS COMPLETOS ---
130
-
131
- class KineticModel(ABC):
132
- def __init__(self, name: str, display_name: str, param_names: List[str],
133
- description: str = "", equation: str = "", reference: str = ""):
134
- self.name = name
135
- self.display_name = display_name
136
- self.param_names = param_names
137
- self.num_params = len(param_names)
138
- self.description = description
139
- self.equation = equation
140
- self.reference = reference
141
-
142
- @abstractmethod
143
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
144
- pass
145
-
146
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
147
- return 0.0
148
-
149
- @abstractmethod
150
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
151
- pass
152
-
153
- @abstractmethod
154
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
155
- pass
156
-
157
- # Modelo Logístico
158
- class LogisticModel(KineticModel):
159
- def __init__(self):
160
- super().__init__(
161
- "logistic",
162
- "Logístico",
163
- ["X0", "Xm", "μm"],
164
- "Modelo de crecimiento logístico clásico para poblaciones limitadas",
165
- r"X(t) = \frac{X_0 X_m e^{\mu_m t}}{X_m - X_0 + X_0 e^{\mu_m t}}",
166
- "Verhulst (1838)"
167
- )
168
-
169
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
170
- X0, Xm, um = params
171
- if Xm <= 0 or X0 <= 0 or Xm < X0:
172
- return np.full_like(t, np.nan)
173
- exp_arg = np.clip(um * t, -700, 700)
174
- term_exp = np.exp(exp_arg)
175
- denominator = Xm - X0 + X0 * term_exp
176
- denominator = np.where(denominator == 0, 1e-9, denominator)
177
- return (X0 * term_exp * Xm) / denominator
178
-
179
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
180
- _, Xm, um = params
181
- return um * X * (1 - X / Xm) if Xm > 0 else 0.0
182
-
183
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
184
- return [
185
- biomass[0] if len(biomass) > 0 and biomass[0] > 1e-6 else 1e-3,
186
- max(biomass) if len(biomass) > 0 else 1.0,
187
- 0.1
188
- ]
189
-
190
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
191
- initial_biomass = biomass[0] if len(biomass) > 0 else 1e-9
192
- max_biomass = max(biomass) if len(biomass) > 0 else 1.0
193
- return ([1e-9, initial_biomass, 1e-9], [max_biomass * 1.2, max_biomass * 5, np.inf])
194
-
195
- # Modelo Gompertz
196
- class GompertzModel(KineticModel):
197
- def __init__(self):
198
- super().__init__(
199
- "gompertz",
200
- "Gompertz",
201
- ["Xm", "μm", "λ"],
202
- "Modelo de crecimiento asimétrico con fase lag",
203
- r"X(t) = X_m \exp\left(-\exp\left(\frac{\mu_m e}{X_m}(\lambda-t)+1\right)\right)",
204
- "Gompertz (1825)"
205
- )
206
-
207
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
208
- Xm, um, lag = params
209
- if Xm <= 0 or um <= 0:
210
- return np.full_like(t, np.nan)
211
- exp_term = (um * np.e / Xm) * (lag - t) + 1
212
- exp_term_clipped = np.clip(exp_term, -700, 700)
213
- return Xm * np.exp(-np.exp(exp_term_clipped))
214
-
215
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
216
- Xm, um, lag = params
217
- k_val = um * np.e / Xm
218
- u_val = k_val * (lag - t) + 1
219
- u_val_clipped = np.clip(u_val, -np.inf, 700)
220
- return X * k_val * np.exp(u_val_clipped) if Xm > 0 and X > 0 else 0.0
221
-
222
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
223
- return [
224
- max(biomass) if len(biomass) > 0 else 1.0,
225
- 0.1,
226
- time[np.argmax(np.gradient(biomass))] if len(biomass) > 1 else 0
227
- ]
228
-
229
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
230
- initial_biomass = min(biomass) if len(biomass) > 0 else 1e-9
231
- max_biomass = max(biomass) if len(biomass) > 0 else 1.0
232
- return ([max(1e-9, initial_biomass), 1e-9, 0], [max_biomass * 5, np.inf, max(time) if len(time) > 0 else 1])
233
-
234
- # Modelo Moser
235
- class MoserModel(KineticModel):
236
- def __init__(self):
237
- super().__init__(
238
- "moser",
239
- "Moser",
240
- ["Xm", "μm", "Ks"],
241
- "Modelo exponencial simple de Moser",
242
- r"X(t) = X_m (1 - e^{-\mu_m (t - K_s)})",
243
- "Moser (1958)"
244
- )
245
-
246
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
247
- Xm, um, Ks = params
248
- return Xm * (1 - np.exp(-um * (t - Ks))) if Xm > 0 and um > 0 else np.full_like(t, np.nan)
249
-
250
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
251
- Xm, um, _ = params
252
- return um * (Xm - X) if Xm > 0 else 0.0
253
-
254
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
255
- return [max(biomass) if len(biomass) > 0 else 1.0, 0.1, 0]
256
-
257
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
258
- initial_biomass = min(biomass) if len(biomass) > 0 else 1e-9
259
- max_biomass = max(biomass) if len(biomass) > 0 else 1.0
260
- return ([max(1e-9, initial_biomass), 1e-9, -np.inf], [max_biomass * 5, np.inf, np.inf])
261
-
262
- # Modelo Baranyi
263
- class BaranyiModel(KineticModel):
264
- def __init__(self):
265
- super().__init__(
266
- "baranyi",
267
- "Baranyi",
268
- ["X0", "Xm", "μm", "λ"],
269
- "Modelo de Baranyi con fase lag explícita",
270
- r"X(t) = X_m / [1 + ((X_m/X_0) - 1) \exp(-\mu_m A(t))]",
271
- "Baranyi & Roberts (1994)"
272
- )
273
-
274
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
275
- X0, Xm, um, lag = params
276
- if X0 <= 0 or Xm <= X0 or um <= 0 or lag < 0:
277
- return np.full_like(t, np.nan)
278
- A_t = t + (1 / um) * np.log(np.exp(-um * t) + np.exp(-um * lag) - np.exp(-um * (t + lag)))
279
- exp_um_At = np.exp(np.clip(um * A_t, -700, 700))
280
- numerator = Xm
281
- denominator = 1 + ((Xm / X0) - 1) * (1 / exp_um_At)
282
- return numerator / np.where(denominator == 0, 1e-9, denominator)
283
-
284
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
285
- return [
286
- biomass[0] if len(biomass) > 0 and biomass[0] > 1e-6 else 1e-3,
287
- max(biomass) if len(biomass) > 0 else 1.0,
288
- 0.1,
289
- time[np.argmax(np.gradient(biomass))] if len(biomass) > 1 else 0.0
290
- ]
291
-
292
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
293
- initial_biomass = biomass[0] if len(biomass) > 0 else 1e-9
294
- max_biomass = max(biomass) if len(biomass) > 0 else 1.0
295
- return ([1e-9, max(1e-9, initial_biomass), 1e-9, 0], [max_biomass * 1.2, max_biomass * 10, np.inf, max(time) if len(time) > 0 else 1])
296
 
297
- # Modelo Monod
298
- class MonodModel(KineticModel):
299
- def __init__(self):
300
- super().__init__(
301
- "monod",
302
- "Monod",
303
- ["μmax", "Ks", "Y", "m"],
304
- "Modelo de Monod con mantenimiento celular",
305
- r"\mu = \frac{\mu_{max} \cdot S}{K_s + S} - m",
306
- "Monod (1949)"
307
- )
308
-
309
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
310
- # Implementación simplificada para ajuste
311
- μmax, Ks, Y, m = params
312
- # Este es un modelo más complejo que requiere integración numérica
313
- return np.full_like(t, np.nan) # Se usa solo con EDO
314
-
315
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
316
- μmax, Ks, Y, m = params
317
- S = 10.0 # Valor placeholder, necesita integrarse con sustrato
318
- μ = (μmax * S / (Ks + S)) - m
319
- return μ * X
320
-
321
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
322
- return [0.5, 0.1, 0.5, 0.01]
323
-
324
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
325
- return ([0.01, 0.001, 0.1, 0.0], [2.0, 5.0, 1.0, 0.1])
326
 
327
- # Modelo Contois
328
- class ContoisModel(KineticModel):
329
  def __init__(self):
330
- super().__init__(
331
- "contois",
332
- "Contois",
333
- ["μmax", "Ksx", "Y", "m"],
334
- "Modelo de Contois para alta densidad celular",
335
- r"\mu = \frac{\mu_{max} \cdot S}{K_{sx} \cdot X + S} - m",
336
- "Contois (1959)"
337
- )
338
-
339
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
340
- return np.full_like(t, np.nan) # Requiere EDO
341
-
342
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
343
- μmax, Ksx, Y, m = params
344
- S = 10.0 # Placeholder
345
- μ = (μmax * S / (Ksx * X + S)) - m
346
- return μ * X
347
-
348
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
349
- return [0.5, 0.5, 0.5, 0.01]
350
-
351
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
352
- return ([0.01, 0.01, 0.1, 0.0], [2.0, 10.0, 1.0, 0.1])
353
-
354
- # Modelo Andrews
355
- class AndrewsModel(KineticModel):
356
- def __init__(self):
357
- super().__init__(
358
- "andrews",
359
- "Andrews (Haldane)",
360
- ["μmax", "Ks", "Ki", "Y", "m"],
361
- "Modelo de inhibición por sustrato",
362
- r"\mu = \frac{\mu_{max} \cdot S}{K_s + S + \frac{S^2}{K_i}} - m",
363
- "Andrews (1968)"
364
- )
365
-
366
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
367
- return np.full_like(t, np.nan)
368
-
369
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
370
- μmax, Ks, Ki, Y, m = params
371
- S = 10.0 # Placeholder
372
- μ = (μmax * S / (Ks + S + S**2/Ki)) - m
373
- return μ * X
374
-
375
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
376
- return [0.5, 0.1, 50.0, 0.5, 0.01]
377
-
378
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
379
- return ([0.01, 0.001, 1.0, 0.1, 0.0], [2.0, 5.0, 200.0, 1.0, 0.1])
380
-
381
- # Modelo Tessier
382
- class TessierModel(KineticModel):
383
- def __init__(self):
384
- super().__init__(
385
- "tessier",
386
- "Tessier",
387
- ["μmax", "Ks", "X0"],
388
- "Modelo exponencial de Tessier",
389
- r"\mu = \mu_{max} \cdot (1 - e^{-S/K_s})",
390
- "Tessier (1942)"
391
- )
392
-
393
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
394
- μmax, Ks, X0 = params
395
- # Implementación simplificada
396
- return X0 * np.exp(μmax * t * 0.5) # Aproximación
397
-
398
- def diff_function(self, X: float, t: float, params: List[float]) -> float:
399
- μmax, Ks, X0 = params
400
- return μmax * X * 0.5 # Simplificado
401
-
402
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
403
- return [0.5, 1.0, biomass[0] if len(biomass) > 0 else 0.1]
404
-
405
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
406
- return ([0.01, 0.1, 1e-9], [2.0, 10.0, 1.0])
407
-
408
- # Modelo Richards
409
- class RichardsModel(KineticModel):
410
- def __init__(self):
411
- super().__init__(
412
- "richards",
413
- "Richards",
414
- ["A", "μm", "λ", "ν", "X0"],
415
- "Modelo generalizado de Richards",
416
- r"X(t) = A \cdot [1 + \nu \cdot e^{-\mu_m(t-\lambda)}]^{-1/\nu}",
417
- "Richards (1959)"
418
- )
419
-
420
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
421
- A, μm, λ, ν, X0 = params
422
- if A <= 0 or μm <= 0 or ν <= 0:
423
- return np.full_like(t, np.nan)
424
- exp_term = np.exp(-μm * (t - λ))
425
- return A * (1 + ν * exp_term) ** (-1/ν)
426
-
427
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
428
- return [
429
- max(biomass) if len(biomass) > 0 else 1.0,
430
- 0.5,
431
- time[len(time)//4] if len(time) > 0 else 1.0,
432
- 1.0,
433
- biomass[0] if len(biomass) > 0 else 0.1
434
- ]
435
-
436
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
437
- max_biomass = max(biomass) if len(biomass) > 0 else 10.0
438
- max_time = max(time) if len(time) > 0 else 100.0
439
- return (
440
- [0.1, 0.01, 0.0, 0.1, 1e-9],
441
- [max_biomass * 2, 5.0, max_time, 10.0, max_biomass]
442
- )
443
-
444
- # Modelo Stannard
445
- class StannardModel(KineticModel):
446
- def __init__(self):
447
- super().__init__(
448
- "stannard",
449
- "Stannard",
450
- ["Xm", "μm", "λ", "α"],
451
- "Modelo de Stannard modificado",
452
- r"X(t) = X_m \cdot [1 - e^{-\mu_m(t-\lambda)^\alpha}]",
453
- "Stannard et al. (1985)"
454
- )
455
-
456
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
457
- Xm, μm, λ, α = params
458
- if Xm <= 0 or μm <= 0 or α <= 0:
459
  return np.full_like(t, np.nan)
460
- t_shifted = np.maximum(t - λ, 0)
461
- return Xm * (1 - np.exp(-μm * t_shifted ** α))
462
-
463
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
464
- return [
465
- max(biomass) if len(biomass) > 0 else 1.0,
466
- 0.5,
467
- 0.0,
468
- 1.0
469
- ]
470
-
471
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
472
- max_biomass = max(biomass) if len(biomass) > 0 else 10.0
473
- max_time = max(time) if len(time) > 0 else 100.0
474
- return ([0.1, 0.01, -max_time/10, 0.1], [max_biomass * 2, 5.0, max_time/2, 3.0])
475
 
476
- # Modelo Huang
477
- class HuangModel(KineticModel):
478
  def __init__(self):
479
- super().__init__(
480
- "huang",
481
- "Huang",
482
- ["Xm", "μm", "λ", "n", "m"],
483
- "Modelo de Huang para fase lag variable",
484
- r"X(t) = X_m \cdot \frac{1}{1 + e^{-\mu_m(t-\lambda-m/n)}}",
485
- "Huang (2008)"
486
- )
487
-
488
- def model_function(self, t: np.ndarray, *params: float) -> np.ndarray:
489
- Xm, μm, λ, n, m = params
490
- if Xm <= 0 or μm <= 0 or n <= 0:
491
  return np.full_like(t, np.nan)
492
- return Xm / (1 + np.exp(-μm * (t - λ - m/n)))
493
-
494
- def get_initial_params(self, time: np.ndarray, biomass: np.ndarray) -> List[float]:
495
- return [
496
- max(biomass) if len(biomass) > 0 else 1.0,
497
- 0.5,
498
- time[len(time)//4] if len(time) > 0 else 1.0,
499
- 1.0,
500
- 0.5
501
- ]
502
-
503
- def get_param_bounds(self, time: np.ndarray, biomass: np.ndarray) -> Tuple[List[float], List[float]]:
504
- max_biomass = max(biomass) if len(biomass) > 0 else 10.0
505
- max_time = max(time) if len(time) > 0 else 100.0
506
- return (
507
- [0.1, 0.01, 0.0, 0.1, 0.0],
508
- [max_biomass * 2, 5.0, max_time/2, 10.0, 5.0]
509
- )
510
 
511
- # --- REGISTRO ACTUALIZADO DE MODELOS ---
512
- AVAILABLE_MODELS: Dict[str, KineticModel] = {
513
- model.name: model for model in [
514
- LogisticModel(),
515
- GompertzModel(),
516
- MoserModel(),
517
- BaranyiModel(),
518
- MonodModel(),
519
- ContoisModel(),
520
- AndrewsModel(),
521
- TessierModel(),
522
- RichardsModel(),
523
- StannardModel(),
524
- HuangModel()
525
- ]
526
  }
527
 
528
- # --- CLASE MEJORADA DE AJUSTE ---
529
- class BioprocessFitter:
530
- def __init__(self, kinetic_model: KineticModel, maxfev: int = 50000,
531
- use_differential_evolution: bool = False):
532
- self.model = kinetic_model
533
- self.maxfev = maxfev
534
- self.use_differential_evolution = use_differential_evolution
535
- self.params: Dict[str, Dict[str, float]] = {c: {} for c in COMPONENTS}
536
- self.r2: Dict[str, float] = {}
537
- self.rmse: Dict[str, float] = {}
538
- self.mae: Dict[str, float] = {} # Mean Absolute Error
539
- self.aic: Dict[str, float] = {} # Akaike Information Criterion
540
- self.bic: Dict[str, float] = {} # Bayesian Information Criterion
541
- self.data_time: Optional[np.ndarray] = None
542
- self.data_means: Dict[str, Optional[np.ndarray]] = {c: None for c in COMPONENTS}
543
- self.data_stds: Dict[str, Optional[np.ndarray]] = {c: None for c in COMPONENTS}
544
-
545
- def _get_biomass_at_t(self, t: np.ndarray, p: List[float]) -> np.ndarray:
546
- return self.model.model_function(t, *p)
547
-
548
- def _get_initial_biomass(self, p: List[float]) -> float:
549
- if not p: return 0.0
550
- if any(k in self.model.param_names for k in ["Xo", "X0"]):
551
- try:
552
- idx = self.model.param_names.index("Xo") if "Xo" in self.model.param_names else self.model.param_names.index("X0")
553
- return p[idx]
554
- except (ValueError, IndexError): pass
555
- return float(self.model.model_function(np.array([0]), *p)[0])
556
-
557
- def _calc_integral(self, t: np.ndarray, p: List[float]) -> Tuple[np.ndarray, np.ndarray]:
558
- X_t = self._get_biomass_at_t(t, p)
559
- if np.any(np.isnan(X_t)): return np.full_like(t, np.nan), np.full_like(t, np.nan)
560
- integral_X = np.zeros_like(X_t)
561
- if len(t) > 1:
562
- dt = np.diff(t, prepend=t[0] - (t[1] - t[0] if len(t) > 1 else 1))
563
- integral_X = np.cumsum(X_t * dt)
564
- return integral_X, X_t
565
-
566
- def substrate(self, t: np.ndarray, so: float, p_c: float, q: float, bio_p: List[float]) -> np.ndarray:
567
- integral, X_t = self._calc_integral(t, bio_p)
568
- X0 = self._get_initial_biomass(bio_p)
569
- return so - p_c * (X_t - X0) - q * integral
570
-
571
- def product(self, t: np.ndarray, po: float, alpha: float, beta: float, bio_p: List[float]) -> np.ndarray:
572
- integral, X_t = self._calc_integral(t, bio_p)
573
- X0 = self._get_initial_biomass(bio_p)
574
- return po + alpha * (X_t - X0) + beta * integral
575
-
576
- def process_data_from_df(self, df: pd.DataFrame) -> None:
577
- try:
578
- time_col = [c for c in df.columns if c[1].strip().lower() == C_TIME][0]
579
- self.data_time = df[time_col].dropna().to_numpy()
580
- min_len = len(self.data_time)
581
-
582
- def extract(name: str) -> Tuple[np.ndarray, np.ndarray]:
583
- cols = [c for c in df.columns if c[1].strip().lower() == name.lower()]
584
- if not cols: return np.array([]), np.array([])
585
- reps = [df[c].dropna().values[:min_len] for c in cols]
586
- reps = [r for r in reps if len(r) == min_len]
587
- if not reps: return np.array([]), np.array([])
588
- arr = np.array(reps)
589
- mean = np.mean(arr, axis=0)
590
- std = np.std(arr, axis=0, ddof=1) if arr.shape[0] > 1 else np.zeros_like(mean)
591
- return mean, std
592
-
593
- self.data_means[C_BIOMASS], self.data_stds[C_BIOMASS] = extract('Biomasa')
594
- self.data_means[C_SUBSTRATE], self.data_stds[C_SUBSTRATE] = extract('Sustrato')
595
- self.data_means[C_PRODUCT], self.data_stds[C_PRODUCT] = extract('Producto')
596
- except (IndexError, KeyError) as e:
597
- raise ValueError(f"Estructura de DataFrame inválida. Error: {e}")
598
 
599
- def _calculate_metrics(self, y_true: np.ndarray, y_pred: np.ndarray,
600
- n_params: int) -> Dict[str, float]:
601
- """Calcula métricas adicionales de bondad de ajuste"""
602
- n = len(y_true)
603
- residuals = y_true - y_pred
604
- ss_res = np.sum(residuals**2)
605
- ss_tot = np.sum((y_true - np.mean(y_true))**2)
606
 
607
- r2 = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
608
- rmse = np.sqrt(ss_res / n)
609
- mae = np.mean(np.abs(residuals))
 
610
 
611
- # AIC y BIC
612
- if n > n_params + 1:
613
- aic = n * np.log(ss_res/n) + 2 * n_params
614
- bic = n * np.log(ss_res/n) + n_params * np.log(n)
615
- else:
616
- aic = bic = np.inf
617
-
618
  return {
 
 
619
  'r2': r2,
620
  'rmse': rmse,
621
- 'mae': mae,
622
- 'aic': aic,
623
- 'bic': bic
624
  }
625
-
626
- def _fit_component_de(self, func, t, data, bounds, *args):
627
- """Ajuste usando evolución diferencial para optimización global"""
628
- def objective(params):
629
- try:
630
- pred = func(t, *params, *args)
631
- if np.any(np.isnan(pred)):
632
- return 1e10
633
- return np.sum((data - pred)**2)
634
- except:
635
- return 1e10
636
-
637
- result = differential_evolution(objective, bounds=list(zip(*bounds)),
638
- maxiter=1000, seed=42)
639
- if result.success:
640
- popt = result.x
641
- pred = func(t, *popt, *args)
642
- metrics = self._calculate_metrics(data, pred, len(popt))
643
- return list(popt), metrics
644
- return None, {'r2': np.nan, 'rmse': np.nan, 'mae': np.nan,
645
- 'aic': np.nan, 'bic': np.nan}
646
-
647
- def _fit_component(self, func, t, data, p0, bounds, sigma=None, *args):
648
- try:
649
- if self.use_differential_evolution:
650
- return self._fit_component_de(func, t, data, bounds, *args)
651
-
652
- if sigma is not None:
653
- sigma = np.where(sigma == 0, 1e-9, sigma)
654
-
655
- popt, _ = curve_fit(func, t, data, p0, bounds=bounds,
656
- maxfev=self.maxfev, ftol=1e-9, xtol=1e-9,
657
- sigma=sigma, absolute_sigma=bool(sigma is not None))
658
-
659
- pred = func(t, *popt, *args)
660
- if np.any(np.isnan(pred)):
661
- return None, {'r2': np.nan, 'rmse': np.nan, 'mae': np.nan,
662
- 'aic': np.nan, 'bic': np.nan}
663
-
664
- metrics = self._calculate_metrics(data, pred, len(popt))
665
- return list(popt), metrics
666
-
667
- except (RuntimeError, ValueError):
668
- return None, {'r2': np.nan, 'rmse': np.nan, 'mae': np.nan,
669
- 'aic': np.nan, 'bic': np.nan}
670
-
671
- def fit_all_models(self) -> None:
672
- t, bio_m, bio_s = self.data_time, self.data_means[C_BIOMASS], self.data_stds[C_BIOMASS]
673
- if t is None or bio_m is None or len(bio_m) == 0: return
674
- popt_bio = self._fit_biomass_model(t, bio_m, bio_s)
675
- if popt_bio:
676
- bio_p = list(self.params[C_BIOMASS].values())
677
- if self.data_means[C_SUBSTRATE] is not None and len(self.data_means[C_SUBSTRATE]) > 0:
678
- self._fit_substrate_model(t, self.data_means[C_SUBSTRATE], self.data_stds[C_SUBSTRATE], bio_p)
679
- if self.data_means[C_PRODUCT] is not None and len(self.data_means[C_PRODUCT]) > 0:
680
- self._fit_product_model(t, self.data_means[C_PRODUCT], self.data_stds[C_PRODUCT], bio_p)
681
-
682
- def _fit_biomass_model(self, t, data, std):
683
- p0, bounds = self.model.get_initial_params(t, data), self.model.get_param_bounds(t, data)
684
- popt, metrics = self._fit_component(self.model.model_function, t, data, p0, bounds, std)
685
- if popt:
686
- self.params[C_BIOMASS] = dict(zip(self.model.param_names, popt))
687
- self.r2[C_BIOMASS] = metrics['r2']
688
- self.rmse[C_BIOMASS] = metrics['rmse']
689
- self.mae[C_BIOMASS] = metrics['mae']
690
- self.aic[C_BIOMASS] = metrics['aic']
691
- self.bic[C_BIOMASS] = metrics['bic']
692
- return popt
693
-
694
- def _fit_substrate_model(self, t, data, std, bio_p):
695
- p0, b = [data[0], 0.1, 0.01], ([0, -np.inf, -np.inf], [np.inf, np.inf, np.inf])
696
- popt, metrics = self._fit_component(lambda t, so, p, q: self.substrate(t, so, p, q, bio_p), t, data, p0, b, std)
697
- if popt:
698
- self.params[C_SUBSTRATE] = {'So': popt[0], 'p': popt[1], 'q': popt[2]}
699
- self.r2[C_SUBSTRATE] = metrics['r2']
700
- self.rmse[C_SUBSTRATE] = metrics['rmse']
701
- self.mae[C_SUBSTRATE] = metrics['mae']
702
- self.aic[C_SUBSTRATE] = metrics['aic']
703
- self.bic[C_SUBSTRATE] = metrics['bic']
704
-
705
- def _fit_product_model(self, t, data, std, bio_p):
706
- p0, b = [data[0] if len(data)>0 else 0, 0.1, 0.01], ([0, -np.inf, -np.inf], [np.inf, np.inf, np.inf])
707
- popt, metrics = self._fit_component(lambda t, po, a, b: self.product(t, po, a, b, bio_p), t, data, p0, b, std)
708
- if popt:
709
- self.params[C_PRODUCT] = {'Po': popt[0], 'alpha': popt[1], 'beta': popt[2]}
710
- self.r2[C_PRODUCT] = metrics['r2']
711
- self.rmse[C_PRODUCT] = metrics['rmse']
712
- self.mae[C_PRODUCT] = metrics['mae']
713
- self.aic[C_PRODUCT] = metrics['aic']
714
- self.bic[C_PRODUCT] = metrics['bic']
715
-
716
- def system_ode(self, y, t, bio_p, sub_p, prod_p):
717
- X, _, _ = y
718
- dXdt = self.model.diff_function(X, t, bio_p)
719
- return [dXdt, -sub_p.get('p',0)*dXdt - sub_p.get('q',0)*X, prod_p.get('alpha',0)*dXdt + prod_p.get('beta',0)*X]
720
-
721
- def solve_odes(self, t_fine):
722
- p = self.params
723
- bio_d, sub_d, prod_d = p[C_BIOMASS], p[C_SUBSTRATE], p[C_PRODUCT]
724
- if not bio_d: return None, None, None
725
- try:
726
- bio_p = list(bio_d.values())
727
- y0 = [self._get_initial_biomass(bio_p), sub_d.get('So',0), prod_d.get('Po',0)]
728
- sol = odeint(self.system_ode, y0, t_fine, args=(bio_p, sub_d, prod_d))
729
- return sol[:, 0], sol[:, 1], sol[:, 2]
730
- except:
731
- return None, None, None
732
-
733
- def _generate_fine_time_grid(self, t_exp):
734
- return np.linspace(min(t_exp), max(t_exp), 500) if t_exp is not None and len(t_exp) > 1 else np.array([])
735
-
736
- def get_model_curves_for_plot(self, t_fine, use_diff):
737
- if use_diff and self.model.diff_function(1, 1, [1]*self.model.num_params) != 0:
738
- return self.solve_odes(t_fine)
739
- X, S, P = None, None, None
740
- if self.params[C_BIOMASS]:
741
- bio_p = list(self.params[C_BIOMASS].values())
742
- X = self.model.model_function(t_fine, *bio_p)
743
- if self.params[C_SUBSTRATE]:
744
- S = self.substrate(t_fine, *list(self.params[C_SUBSTRATE].values()), bio_p)
745
- if self.params[C_PRODUCT]:
746
- P = self.product(t_fine, *list(self.params[C_PRODUCT].values()), bio_p)
747
- return X, S, P
748
-
749
- # --- FUNCIONES AUXILIARES ---
750
-
751
- def format_number(value: Any, decimals: int) -> str:
752
- """Formatea un número para su visualización"""
753
- if not isinstance(value, (int, float, np.number)) or pd.isna(value):
754
- return "" if pd.isna(value) else str(value)
755
-
756
- decimals = int(decimals)
757
-
758
- if decimals == 0:
759
- if 0 < abs(value) < 1:
760
- return f"{value:.2e}"
761
- else:
762
- return str(int(round(value, 0)))
763
-
764
- return str(round(value, decimals))
765
-
766
- # --- FUNCIONES DE PLOTEO MEJORADAS CON PLOTLY ---
767
-
768
- def create_interactive_plot(plot_config: Dict, models_results: List[Dict],
769
- selected_component: str = "all") -> go.Figure:
770
- """Crea un gráfico interactivo mejorado con Plotly"""
771
- time_exp = plot_config['time_exp']
772
- time_fine = np.linspace(min(time_exp), max(time_exp), 500)
773
-
774
- # Configuración de subplots si se muestran todos los componentes
775
- if selected_component == "all":
776
- fig = make_subplots(
777
- rows=3, cols=1,
778
- subplot_titles=('Biomasa', 'Sustrato', 'Producto'),
779
- vertical_spacing=0.08,
780
- shared_xaxes=True
781
- )
782
- components_to_plot = [C_BIOMASS, C_SUBSTRATE, C_PRODUCT]
783
- rows = [1, 2, 3]
784
- else:
785
- fig = go.Figure()
786
- components_to_plot = [selected_component]
787
- rows = [None]
788
-
789
- # Colores para diferentes modelos
790
- colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
791
- '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
792
-
793
- # Agregar datos experimentales
794
- for comp, row in zip(components_to_plot, rows):
795
- data_exp = plot_config.get(f'{comp}_exp')
796
- data_std = plot_config.get(f'{comp}_std')
797
-
798
- if data_exp is not None:
799
- error_y = dict(
800
- type='data',
801
- array=data_std,
802
- visible=True
803
- ) if data_std is not None and np.any(data_std > 0) else None
804
-
805
- trace = go.Scatter(
806
- x=time_exp,
807
- y=data_exp,
808
- mode='markers',
809
- name=f'{comp.capitalize()} (Experimental)',
810
- marker=dict(size=10, symbol='circle'),
811
- error_y=error_y,
812
- legendgroup=comp,
813
- showlegend=True
814
- )
815
-
816
- if selected_component == "all":
817
- fig.add_trace(trace, row=row, col=1)
818
- else:
819
- fig.add_trace(trace)
820
-
821
- # Agregar curvas de modelos
822
- for i, res in enumerate(models_results):
823
- color = colors[i % len(colors)]
824
- model_name = AVAILABLE_MODELS[res["name"]].display_name
825
-
826
- for comp, row, key in zip(components_to_plot, rows, ['X', 'S', 'P']):
827
- if res.get(key) is not None:
828
- trace = go.Scatter(
829
- x=time_fine,
830
- y=res[key],
831
- mode='lines',
832
- name=f'{model_name} - {comp.capitalize()}',
833
- line=dict(color=color, width=2),
834
- legendgroup=f'{res["name"]}_{comp}',
835
- showlegend=True
836
- )
837
-
838
- if selected_component == "all":
839
- fig.add_trace(trace, row=row, col=1)
840
- else:
841
- fig.add_trace(trace)
842
-
843
- # Actualizar diseño
844
- theme = plot_config.get('theme', 'light')
845
- template = "plotly_white" if theme == 'light' else "plotly_dark"
846
-
847
- fig.update_layout(
848
- title=f"Análisis de Cinéticas: {plot_config.get('exp_name', '')}",
849
- template=template,
850
- hovermode='x unified',
851
- legend=dict(
852
- orientation="v",
853
- yanchor="middle",
854
- y=0.5,
855
- xanchor="left",
856
- x=1.02
857
- ),
858
- margin=dict(l=80, r=250, t=100, b=80)
859
- )
860
-
861
- # Actualizar ejes
862
- if selected_component == "all":
863
- fig.update_xaxes(title_text="Tiempo", row=3, col=1)
864
- fig.update_yaxes(title_text="Biomasa (g/L)", row=1, col=1)
865
- fig.update_yaxes(title_text="Sustrato (g/L)", row=2, col=1)
866
- fig.update_yaxes(title_text="Producto (g/L)", row=3, col=1)
867
- else:
868
- fig.update_xaxes(title_text="Tiempo")
869
- labels = {
870
- C_BIOMASS: "Biomasa (g/L)",
871
- C_SUBSTRATE: "Sustrato (g/L)",
872
- C_PRODUCT: "Producto (g/L)"
873
  }
874
- fig.update_yaxes(title_text=labels.get(selected_component, "Valor"))
875
-
876
- # Agregar botones para cambiar entre modos de visualización
877
- fig.update_layout(
878
- updatemenus=[
879
- dict(
880
- type="dropdown",
881
- showactive=True,
882
- buttons=[
883
- dict(label="Todos los componentes",
884
- method="update",
885
- args=[{"visible": [True] * len(fig.data)}]),
886
- dict(label="Solo Biomasa",
887
- method="update",
888
- args=[{"visible": [i < len(fig.data)//3 for i in range(len(fig.data))]}]),
889
- dict(label="Solo Sustrato",
890
- method="update",
891
- args=[{"visible": [len(fig.data)//3 <= i < 2*len(fig.data)//3 for i in range(len(fig.data))]}]),
892
- dict(label="Solo Producto",
893
- method="update",
894
- args=[{"visible": [i >= 2*len(fig.data)//3 for i in range(len(fig.data))]}]),
895
- ],
896
- x=0.1,
897
- y=1.15,
898
- xanchor="left",
899
- yanchor="top"
900
- )
901
- ]
902
- )
903
-
904
- return fig
905
 
906
- # --- FUNCIÓN PRINCIPAL DE ANÁLISIS ---
907
- def run_analysis(file, model_names, component, use_de, maxfev, exp_names, theme='light'):
908
- if not file: return None, pd.DataFrame(), "Error: Sube un archivo Excel."
909
- if not model_names: return None, pd.DataFrame(), "Error: Selecciona un modelo."
910
-
911
- try:
912
- xls = pd.ExcelFile(file.name)
913
- except Exception as e:
914
- return None, pd.DataFrame(), f"Error al leer archivo: {e}"
915
-
916
- results_data, msgs = [], []
917
- models_results = []
918
-
919
- exp_list = [n.strip() for n in exp_names.split('\n') if n.strip()] if exp_names else []
920
-
921
- for i, sheet in enumerate(xls.sheet_names):
922
- exp_name = exp_list[i] if i < len(exp_list) else f"Hoja '{sheet}'"
923
- try:
924
- df = pd.read_excel(xls, sheet_name=sheet, header=[0,1])
925
- reader = BioprocessFitter(list(AVAILABLE_MODELS.values())[0])
926
- reader.process_data_from_df(df)
927
-
928
- if reader.data_time is None:
929
- msgs.append(f"WARN: Sin datos de tiempo en '{sheet}'.")
930
- continue
931
-
932
- plot_config = {
933
- 'exp_name': exp_name,
934
- 'time_exp': reader.data_time,
935
- 'theme': theme
936
- }
937
 
938
- for c in COMPONENTS:
939
- plot_config[f'{c}_exp'] = reader.data_means[c]
940
- plot_config[f'{c}_std'] = reader.data_stds[c]
941
 
942
- t_fine = reader._generate_fine_time_grid(reader.data_time)
 
 
 
 
943
 
944
- for m_name in model_names:
945
- if m_name not in AVAILABLE_MODELS:
946
- msgs.append(f"WARN: Modelo '{m_name}' no disponible.")
947
- continue
948
-
949
- fitter = BioprocessFitter(
950
- AVAILABLE_MODELS[m_name],
951
- maxfev=int(maxfev),
952
- use_differential_evolution=use_de
953
- )
954
- fitter.data_time = reader.data_time
955
- fitter.data_means = reader.data_means
956
- fitter.data_stds = reader.data_stds
957
- fitter.fit_all_models()
958
-
959
- row = {'Experimento': exp_name, 'Modelo': fitter.model.display_name}
960
- for c in COMPONENTS:
961
- if fitter.params[c]:
962
- row.update({f'{c.capitalize()}_{k}': v for k, v in fitter.params[c].items()})
963
- row[f'R2_{c.capitalize()}'] = fitter.r2.get(c)
964
- row[f'RMSE_{c.capitalize()}'] = fitter.rmse.get(c)
965
- row[f'MAE_{c.capitalize()}'] = fitter.mae.get(c)
966
- row[f'AIC_{c.capitalize()}'] = fitter.aic.get(c)
967
- row[f'BIC_{c.capitalize()}'] = fitter.bic.get(c)
968
-
969
- results_data.append(row)
970
-
971
- X, S, P = fitter.get_model_curves_for_plot(t_fine, False)
972
- models_results.append({
973
- 'name': m_name,
974
- 'X': X,
975
- 'S': S,
976
- 'P': P,
977
- 'params': fitter.params,
978
- 'r2': fitter.r2,
979
- 'rmse': fitter.rmse
980
- })
981
-
982
- except Exception as e:
983
- msgs.append(f"ERROR en '{sheet}': {e}")
984
- traceback.print_exc()
985
-
986
- msg = "Análisis completado." + ("\n" + "\n".join(msgs) if msgs else "")
987
- df_res = pd.DataFrame(results_data).dropna(axis=1, how='all')
988
-
989
- # Crear gráfico interactivo
990
- fig = None
991
- if models_results and reader.data_time is not None:
992
- fig = create_interactive_plot(plot_config, models_results, component)
993
-
994
- return fig, df_res, msg
995
-
996
- # --- API ENDPOINTS PARA AGENTES DE IA ---
997
-
998
- app = FastAPI(title="Bioprocess Kinetics API", version="2.0")
999
-
1000
- @app.get("/")
1001
- def read_root():
1002
- return {"message": "Bioprocess Kinetics Analysis API", "version": "2.0"}
1003
-
1004
- @app.post("/api/analyze")
1005
- async def analyze_data(
1006
- data: Dict[str, List[float]],
1007
- models: List[str],
1008
- options: Optional[Dict[str, Any]] = None
1009
- ):
1010
- """Endpoint para análisis de datos cinéticos"""
1011
- try:
1012
- results = {}
1013
-
1014
- for model_name in models:
1015
- if model_name not in AVAILABLE_MODELS:
1016
  continue
1017
 
1018
- model = AVAILABLE_MODELS[model_name]
1019
- fitter = BioprocessFitter(model)
 
1020
 
1021
- # Configurar datos
1022
- fitter.data_time = np.array(data['time'])
1023
- fitter.data_means[C_BIOMASS] = np.array(data.get('biomass', []))
1024
- fitter.data_means[C_SUBSTRATE] = np.array(data.get('substrate', []))
1025
- fitter.data_means[C_PRODUCT] = np.array(data.get('product', []))
1026
 
1027
- # Ajustar modelo
1028
- fitter.fit_all_models()
1029
-
1030
- results[model_name] = {
1031
- 'parameters': fitter.params,
1032
- 'metrics': {
1033
- 'r2': fitter.r2,
1034
- 'rmse': fitter.rmse,
1035
- 'mae': fitter.mae,
1036
- 'aic': fitter.aic,
1037
- 'bic': fitter.bic
1038
- }
1039
- }
1040
-
1041
- return {"status": "success", "results": results}
1042
 
 
1043
  except Exception as e:
1044
- return {"status": "error", "message": str(e)}
1045
-
1046
- @app.get("/api/models")
1047
- def get_available_models():
1048
- """Retorna lista de modelos disponibles con su información"""
1049
- models_info = {}
1050
- for name, model in AVAILABLE_MODELS.items():
1051
- models_info[name] = {
1052
- "display_name": model.display_name,
1053
- "parameters": model.param_names,
1054
- "description": model.description,
1055
- "equation": model.equation,
1056
- "reference": model.reference,
1057
- "num_params": model.num_params
1058
- }
1059
- return {"models": models_info}
1060
 
1061
- @app.post("/api/predict")
1062
- async def predict_kinetics(
1063
- model_name: str,
1064
- parameters: Dict[str, float],
1065
- time_points: List[float]
1066
- ):
1067
- """Predice valores usando un modelo y parámetros específicos"""
1068
- if model_name not in AVAILABLE_MODELS:
1069
- return {"status": "error", "message": f"Model {model_name} not found"}
1070
-
1071
  try:
1072
- model = AVAILABLE_MODELS[model_name]
1073
- time_array = np.array(time_points)
1074
- params = [parameters[name] for name in model.param_names]
1075
 
1076
- predictions = model.model_function(time_array, *params)
 
1077
 
1078
- return {
1079
- "status": "success",
1080
- "predictions": predictions.tolist(),
1081
- "time_points": time_points
1082
- }
1083
- except Exception as e:
1084
- return {"status": "error", "message": str(e)}
1085
-
1086
- # --- INTERFAZ GRADIO MEJORADA ---
1087
-
1088
- def create_gradio_interface() -> gr.Blocks:
1089
- """Crea la interfaz mejorada con soporte multiidioma y tema"""
1090
-
1091
- def change_language(lang_key: str) -> Dict:
1092
- """Cambia el idioma de la interfaz"""
1093
- lang = Language[lang_key]
1094
- trans = TRANSLATIONS.get(lang, TRANSLATIONS[Language.ES])
1095
 
1096
- return trans["title"], trans["subtitle"]
1097
-
1098
- # Obtener opciones de modelo
1099
- MODEL_CHOICES = [(model.display_name, model.name) for model in AVAILABLE_MODELS.values()]
1100
- DEFAULT_MODELS = [m.name for m in list(AVAILABLE_MODELS.values())[:4]]
1101
-
1102
- with gr.Blocks(theme=THEMES["light"], css="""
1103
- .gradio-container {font-family: 'Inter', sans-serif;}
1104
- .theory-box {background-color: #f0f9ff; padding: 20px; border-radius: 10px; margin: 10px 0;}
1105
- .dark .theory-box {background-color: #1e293b;}
1106
- .model-card {border: 1px solid #e5e7eb; padding: 15px; border-radius: 8px; margin: 10px 0;}
1107
- .dark .model-card {border-color: #374151;}
1108
- """) as demo:
 
 
 
 
 
 
1109
 
1110
- # Estado para tema e idioma
1111
- current_theme = gr.State("light")
1112
- current_language = gr.State("ES")
1113
 
1114
- # Header con controles de tema e idioma
1115
- with gr.Row():
1116
- with gr.Column(scale=8):
1117
- title_text = gr.Markdown("# 🔬 Analizador de Cinéticas de Bioprocesos")
1118
- subtitle_text = gr.Markdown("Análisis avanzado de modelos matemáticos biotecnológicos")
1119
- with gr.Column(scale=2):
1120
- with gr.Row():
1121
- theme_toggle = gr.Checkbox(label="🌙 Modo Oscuro", value=False)
1122
- language_select = gr.Dropdown(
1123
- choices=[(lang.value, lang.name) for lang in Language],
1124
- value="ES",
1125
- label="🌐 Idioma"
1126
- )
1127
 
1128
- with gr.Tabs() as tabs:
1129
- # --- TAB 1: TEORÍA Y MODELOS ---
1130
- with gr.TabItem("📚 Teoría y Modelos"):
1131
- gr.Markdown("""
1132
- ## Introducción a los Modelos Cinéticos
1133
-
1134
- Los modelos cinéticos en biotecnología describen el comportamiento dinámico
1135
- de los microorganismos durante su crecimiento. Estos modelos son fundamentales
1136
- para:
1137
-
1138
- - **Optimización de procesos**: Determinar condiciones óptimas de operación
1139
- - **Escalamiento**: Predecir comportamiento a escala industrial
1140
- - **Control de procesos**: Diseñar estrategias de control efectivas
1141
- - **Análisis económico**: Evaluar viabilidad de procesos
1142
- """)
1143
-
1144
- # Cards para cada modelo
1145
- for model_name, model in AVAILABLE_MODELS.items():
1146
- with gr.Accordion(f"📊 {model.display_name}", open=False):
1147
- with gr.Row():
1148
- with gr.Column(scale=3):
1149
- gr.Markdown(f"""
1150
- **Descripción**: {model.description}
1151
-
1152
- **Ecuación**: ${model.equation}$
1153
-
1154
- **Parámetros**: {', '.join(model.param_names)}
1155
-
1156
- **Referencia**: {model.reference}
1157
- """)
1158
- with gr.Column(scale=1):
1159
- gr.Markdown(f"""
1160
- **Características**:
1161
- - Parámetros: {model.num_params}
1162
- - Complejidad: {'⭐' * min(model.num_params, 5)}
1163
- """)
1164
 
1165
- # --- TAB 2: ANÁLISIS ---
1166
- with gr.TabItem("🔬 Análisis"):
1167
- with gr.Row():
1168
- with gr.Column(scale=1):
1169
- file_input = gr.File(
1170
- label="📁 Sube tu archivo Excel (.xlsx)",
1171
- file_types=['.xlsx']
1172
- )
1173
-
1174
- exp_names_input = gr.Textbox(
1175
- label="🏷️ Nombres de Experimentos",
1176
- placeholder="Experimento 1\nExperimento 2\n...",
1177
- lines=3
1178
- )
1179
-
1180
- model_selection_input = gr.CheckboxGroup(
1181
- choices=MODEL_CHOICES,
1182
- label="📊 Modelos a Probar",
1183
- value=DEFAULT_MODELS
1184
- )
1185
-
1186
- with gr.Accordion("⚙️ Opciones Avanzadas", open=False):
1187
- use_de_input = gr.Checkbox(
1188
- label="Usar Evolución Diferencial",
1189
- value=False,
1190
- info="Optimización global más robusta pero más lenta"
1191
- )
1192
-
1193
- maxfev_input = gr.Number(
1194
- label="Iteraciones máximas",
1195
- value=50000
1196
- )
1197
-
1198
- with gr.Column(scale=2):
1199
- # Selector de componente para visualización
1200
- component_selector = gr.Dropdown(
1201
- choices=[
1202
- ("Todos los componentes", "all"),
1203
- ("Solo Biomasa", C_BIOMASS),
1204
- ("Solo Sustrato", C_SUBSTRATE),
1205
- ("Solo Producto", C_PRODUCT)
1206
- ],
1207
- value="all",
1208
- label="📈 Componente a visualizar"
1209
- )
1210
-
1211
- plot_output = gr.Plot(label="Visualización Interactiva")
1212
 
1213
- analyze_button = gr.Button("🚀 Analizar y Graficar", variant="primary")
 
 
 
 
 
 
1214
 
1215
- # --- TAB 3: RESULTADOS ---
1216
- with gr.TabItem("📊 Resultados"):
1217
- status_output = gr.Textbox(
1218
- label="Estado del Análisis",
1219
- interactive=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1220
  )
1221
 
1222
- results_table = gr.DataFrame(
1223
- label="Tabla de Resultados",
1224
- wrap=True
 
1225
  )
1226
 
1227
- with gr.Row():
1228
- download_excel = gr.Button("📥 Descargar Excel")
1229
- download_json = gr.Button("📥 Descargar JSON")
1230
- api_docs_button = gr.Button("📖 Ver Documentación API")
1231
-
1232
- download_file = gr.File(label="Archivo descargado")
1233
 
1234
- # --- TAB 4: API ---
1235
- with gr.TabItem("🔌 API"):
1236
- gr.Markdown("""
1237
- ## Documentación de la API
1238
-
1239
- La API REST permite integrar el análisis de cinéticas en aplicaciones externas
1240
- y agentes de IA.
1241
-
1242
- ### Endpoints disponibles:
1243
-
1244
- #### 1. `GET /api/models`
1245
- Retorna la lista de modelos disponibles con su información.
1246
-
1247
- ```python
1248
- import requests
1249
- response = requests.get("http://localhost:8000/api/models")
1250
- models = response.json()
1251
- ```
1252
-
1253
- #### 2. `POST /api/analyze`
1254
- Analiza datos con los modelos especificados.
1255
-
1256
- ```python
1257
- data = {
1258
- "data": {
1259
- "time": [0, 1, 2, 3, 4],
1260
- "biomass": [0.1, 0.3, 0.8, 1.5, 2.0],
1261
- "substrate": [10, 8, 5, 2, 0.5]
1262
- },
1263
- "models": ["logistic", "gompertz"],
1264
- "options": {"maxfev": 50000}
1265
- }
1266
- response = requests.post("http://localhost:8000/api/analyze", json=data)
1267
- results = response.json()
1268
- ```
1269
-
1270
- #### 3. `POST /api/predict`
1271
- Predice valores usando un modelo y parámetros específicos.
1272
-
1273
- ```python
1274
- data = {
1275
- "model_name": "logistic",
1276
- "parameters": {"X0": 0.1, "Xm": 10.0, "μm": 0.5},
1277
- "time_points": [0, 1, 2, 3, 4, 5]
1278
- }
1279
- response = requests.post("http://localhost:8000/api/predict", json=data)
1280
- predictions = response.json()
1281
- ```
1282
-
1283
- ### Iniciar servidor API:
1284
- ```bash
1285
- uvicorn script_name:app --reload --port 8000
1286
- ```
1287
- """)
1288
-
1289
- # Botón para copiar comando
1290
- gr.Textbox(
1291
- value="uvicorn bioprocess_analyzer:app --reload --port 8000",
1292
- label="Comando para iniciar API",
1293
- interactive=False
1294
- )
1295
-
1296
- # --- EVENTOS ---
1297
 
1298
- def run_analysis_wrapper(file, models, component, use_de, maxfev, exp_names, theme):
1299
- """Wrapper para ejecutar el análisis"""
1300
- try:
1301
- return run_analysis(file, models, component, use_de, maxfev, exp_names,
1302
- 'dark' if theme else 'light')
1303
- except Exception as e:
1304
- print(f"--- ERROR EN ANÁLISIS ---\n{traceback.format_exc()}")
1305
- return None, pd.DataFrame(), f"Error: {str(e)}"
1306
-
1307
- analyze_button.click(
1308
- fn=run_analysis_wrapper,
1309
- inputs=[
1310
- file_input,
1311
- model_selection_input,
1312
- component_selector,
1313
- use_de_input,
1314
- maxfev_input,
1315
- exp_names_input,
1316
- theme_toggle
1317
- ],
1318
- outputs=[plot_output, results_table, status_output]
1319
- )
1320
-
1321
- # Cambio de idioma
1322
- language_select.change(
1323
- fn=change_language,
1324
- inputs=[language_select],
1325
- outputs=[title_text, subtitle_text]
1326
- )
1327
-
1328
- # Cambio de tema
1329
- def apply_theme(is_dark):
1330
- return gr.Info("Tema cambiado. Los gráficos nuevos usarán el tema seleccionado.")
1331
-
1332
- theme_toggle.change(
1333
- fn=apply_theme,
1334
- inputs=[theme_toggle],
1335
- outputs=[]
1336
- )
1337
-
1338
- # Funciones de descarga
1339
- def download_results_excel(df):
1340
- if df is None or df.empty:
1341
- gr.Warning("No hay datos para descargar")
1342
- return None
1343
- with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
1344
- df.to_excel(tmp.name, index=False)
1345
- return tmp.name
1346
-
1347
- def download_results_json(df):
1348
- if df is None or df.empty:
1349
- gr.Warning("No hay datos para descargar")
1350
- return None
1351
- with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
1352
- df.to_json(tmp.name, orient='records', indent=2)
1353
- return tmp.name
1354
-
1355
- download_excel.click(
1356
- fn=download_results_excel,
1357
- inputs=[results_table],
1358
- outputs=[download_file]
1359
- )
1360
-
1361
- download_json.click(
1362
- fn=download_results_json,
1363
- inputs=[results_table],
1364
- outputs=[download_file]
1365
  )
1366
 
1367
  return demo
1368
 
1369
  # --- PUNTO DE ENTRADA ---
1370
-
1371
- if __name__ == '__main__':
1372
- # Lanzar aplicación Gradio
1373
- gradio_app = create_gradio_interface()
1374
- gradio_app.launch(share=True, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py - Versión simplificada para Hugging Face Spaces
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import gradio as gr
3
+ import pandas as pd
4
+ import numpy as np
5
  import plotly.graph_objects as go
6
  from plotly.subplots import make_subplots
7
+ import tempfile
8
+ import traceback
9
+ from typing import List, Dict, Any, Optional, Tuple
 
 
10
  from scipy.optimize import curve_fit, differential_evolution
11
  from sklearn.metrics import mean_squared_error, r2_score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ # Configuración básica
14
+ print("Iniciando aplicación...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # --- MODELOS BÁSICOS ---
17
+ class LogisticModel:
18
  def __init__(self):
19
+ self.name = "logistic"
20
+ self.display_name = "Logístico"
21
+ self.param_names = ["X0", "Xm", "μm"]
22
+
23
+ def model_function(self, t, X0, Xm, um):
24
+ try:
25
+ if Xm <= 0 or X0 <= 0 or Xm < X0:
26
+ return np.full_like(t, np.nan)
27
+ exp_arg = np.clip(um * t, -700, 700)
28
+ term_exp = np.exp(exp_arg)
29
+ denominator = Xm - X0 + X0 * term_exp
30
+ denominator = np.where(denominator == 0, 1e-9, denominator)
31
+ return (X0 * term_exp * Xm) / denominator
32
+ except:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  return np.full_like(t, np.nan)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ class GompertzModel:
 
36
  def __init__(self):
37
+ self.name = "gompertz"
38
+ self.display_name = "Gompertz"
39
+ self.param_names = ["Xm", "μm", "λ"]
40
+
41
+ def model_function(self, t, Xm, um, lag):
42
+ try:
43
+ if Xm <= 0 or um <= 0:
44
+ return np.full_like(t, np.nan)
45
+ exp_term = (um * np.e / Xm) * (lag - t) + 1
46
+ exp_term_clipped = np.clip(exp_term, -700, 700)
47
+ return Xm * np.exp(-np.exp(exp_term_clipped))
48
+ except:
49
  return np.full_like(t, np.nan)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ # Modelos disponibles
52
+ MODELS = {
53
+ "logistic": LogisticModel(),
54
+ "gompertz": GompertzModel()
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
+ # --- FUNCIONES DE ANÁLISIS SIMPLIFICADAS ---
58
+ def fit_model(model, time_data, biomass_data):
59
+ """Ajusta un modelo a los datos"""
60
+ try:
61
+ # Parámetros iniciales
62
+ if model.name == "logistic":
63
+ p0 = [biomass_data[0], max(biomass_data), 0.1]
64
+ bounds = ([1e-9, biomass_data[0], 1e-9],
65
+ [max(biomass_data)*2, max(biomass_data)*5, 10])
66
+ else: # gompertz
67
+ p0 = [max(biomass_data), 0.1, 0]
68
+ bounds = ([biomass_data[0], 1e-9, 0],
69
+ [max(biomass_data)*5, 10, max(time_data)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ popt, _ = curve_fit(model.model_function, time_data, biomass_data,
72
+ p0=p0, bounds=bounds, maxfev=10000)
 
 
 
 
 
73
 
74
+ # Calcular métricas
75
+ y_pred = model.model_function(time_data, *popt)
76
+ r2 = r2_score(biomass_data, y_pred)
77
+ rmse = np.sqrt(mean_squared_error(biomass_data, y_pred))
78
 
 
 
 
 
 
 
 
79
  return {
80
+ 'success': True,
81
+ 'parameters': dict(zip(model.param_names, popt)),
82
  'r2': r2,
83
  'rmse': rmse,
84
+ 'predictions': y_pred
 
 
85
  }
86
+ except Exception as e:
87
+ return {
88
+ 'success': False,
89
+ 'error': str(e),
90
+ 'parameters': {},
91
+ 'r2': np.nan,
92
+ 'rmse': np.nan,
93
+ 'predictions': np.full_like(time_data, np.nan)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ def process_excel_file(file_path):
97
+ """Procesa archivo Excel y extrae datos"""
98
+ try:
99
+ # Leer archivo Excel
100
+ xls = pd.ExcelFile(file_path)
101
+ results = []
102
+
103
+ for sheet_name in xls.sheet_names:
104
+ df = pd.read_excel(xls, sheet_name=sheet_name, header=[0,1])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ # Buscar columnas de tiempo y biomasa
107
+ time_col = None
108
+ biomass_cols = []
109
 
110
+ for col in df.columns:
111
+ if 'tiempo' in str(col[1]).lower():
112
+ time_col = col
113
+ elif 'biomasa' in str(col[1]).lower():
114
+ biomass_cols.append(col)
115
 
116
+ if time_col is None or not biomass_cols:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  continue
118
 
119
+ # Extraer datos
120
+ time_data = df[time_col].dropna().values
121
+ biomass_data = df[biomass_cols[0]].dropna().values
122
 
123
+ # Asegurar mismo tamaño
124
+ min_len = min(len(time_data), len(biomass_data))
125
+ time_data = time_data[:min_len]
126
+ biomass_data = biomass_data[:min_len]
 
127
 
128
+ results.append({
129
+ 'sheet': sheet_name,
130
+ 'time': time_data,
131
+ 'biomass': biomass_data
132
+ })
 
 
 
 
 
 
 
 
 
 
133
 
134
+ return results
135
  except Exception as e:
136
+ print(f"Error procesando archivo: {e}")
137
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ def analyze_data(file, selected_models):
140
+ """Función principal de análisis"""
141
+ if file is None:
142
+ return None, "Error: No se ha subido ningún archivo"
143
+
 
 
 
 
 
144
  try:
145
+ # Procesar archivo
146
+ datasets = process_excel_file(file.name)
 
147
 
148
+ if not datasets:
149
+ return None, "Error: No se encontraron datos válidos en el archivo"
150
 
151
+ all_results = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ # Por cada dataset
154
+ for dataset in datasets:
155
+ sheet_name = dataset['sheet']
156
+ time_data = dataset['time']
157
+ biomass_data = dataset['biomass']
158
+
159
+ # Por cada modelo seleccionado
160
+ for model_name in selected_models:
161
+ if model_name in MODELS:
162
+ model = MODELS[model_name]
163
+ result = fit_model(model, time_data, biomass_data)
164
+
165
+ all_results.append({
166
+ 'Experimento': sheet_name,
167
+ 'Modelo': model.display_name,
168
+ 'R²': result['r2'],
169
+ 'RMSE': result['rmse'],
170
+ **{f'Param_{k}': v for k, v in result['parameters'].items()}
171
+ })
172
 
173
+ # Crear DataFrame de resultados
174
+ results_df = pd.DataFrame(all_results)
 
175
 
176
+ # Crear gráfico simple
177
+ fig = go.Figure()
 
 
 
 
 
 
 
 
 
 
 
178
 
179
+ if datasets:
180
+ dataset = datasets[0] # Usar primer dataset para el gráfico
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ # Datos experimentales
183
+ fig.add_trace(go.Scatter(
184
+ x=dataset['time'],
185
+ y=dataset['biomass'],
186
+ mode='markers',
187
+ name='Datos Experimentales',
188
+ marker=dict(size=8)
189
+ ))
190
+
191
+ # Predicciones de modelos
192
+ colors = ['red', 'blue', 'green', 'orange']
193
+ for i, model_name in enumerate(selected_models):
194
+ if model_name in MODELS:
195
+ model = MODELS[model_name]
196
+ result = fit_model(model, dataset['time'], dataset['biomass'])
197
+ if result['success']:
198
+ t_fine = np.linspace(min(dataset['time']), max(dataset['time']), 100)
199
+ y_pred = model.model_function(t_fine, *result['parameters'].values())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
+ fig.add_trace(go.Scatter(
202
+ x=t_fine,
203
+ y=y_pred,
204
+ mode='lines',
205
+ name=f'{model.display_name} (R²={result["r2"]:.3f})',
206
+ line=dict(color=colors[i % len(colors)])
207
+ ))
208
 
209
+ fig.update_layout(
210
+ title='Análisis de Cinéticas de Crecimiento',
211
+ xaxis_title='Tiempo',
212
+ yaxis_title='Biomasa',
213
+ template='plotly_white'
214
+ )
215
+
216
+ return fig, f"Análisis completado exitosamente. Procesados {len(datasets)} experimentos."
217
+
218
+ except Exception as e:
219
+ error_msg = f"Error en el análisis: {str(e)}"
220
+ print(error_msg)
221
+ print(traceback.format_exc())
222
+ return None, error_msg
223
+
224
+ # --- INTERFAZ GRADIO SIMPLIFICADA ---
225
+ def create_interface():
226
+ """Crear interfaz Gradio simplificada"""
227
+
228
+ with gr.Blocks(title="Analizador de Cinéticas") as demo:
229
+ gr.Markdown("# 🔬 Analizador de Cinéticas de Bioprocesos")
230
+ gr.Markdown("Versión simplificada para análisis de modelos de crecimiento")
231
+
232
+ with gr.Row():
233
+ with gr.Column():
234
+ file_input = gr.File(
235
+ label="📁 Subir archivo Excel (.xlsx)",
236
+ file_types=['.xlsx']
237
  )
238
 
239
+ model_selection = gr.CheckboxGroup(
240
+ choices=[("Logístico", "logistic"), ("Gompertz", "gompertz")],
241
+ label="🔬 Seleccionar Modelos",
242
+ value=["logistic"]
243
  )
244
 
245
+ analyze_btn = gr.Button("🚀 Analizar", variant="primary")
 
 
 
 
 
246
 
247
+ with gr.Column():
248
+ plot_output = gr.Plot(label="📊 Resultados")
249
+ status_output = gr.Textbox(label="📋 Estado", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
+ # Conectar eventos
252
+ analyze_btn.click(
253
+ fn=analyze_data,
254
+ inputs=[file_input, model_selection],
255
+ outputs=[plot_output, status_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  )
257
 
258
  return demo
259
 
260
  # --- PUNTO DE ENTRADA ---
261
+ if __name__ == "__main__":
262
+ print("Creando interfaz...")
263
+
264
+ try:
265
+ demo = create_interface()
266
+ print("Interfaz creada, lanzando aplicación...")
267
+
268
+ # Configuración para Hugging Face Spaces
269
+ demo.launch(
270
+ server_name="0.0.0.0",
271
+ server_port=7860,
272
+ share=False, # No usar share en HF Spaces
273
+ debug=False, # Desactivar debug en producción
274
+ show_error=True,
275
+ quiet=False
276
+ )
277
+
278
+ except Exception as e:
279
+ print(f"Error al lanzar la aplicación: {e}")
280
+ print(traceback.format_exc())