BiotechU5 / app.py
C2MV's picture
Update app.py
ffa837b verified
raw
history blame
70.7 kB
#import os
#!pip install gradio seaborn scipy scikit-learn openpyxl pydantic==1.10.0 -q
from pydantic import BaseModel, ConfigDict
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.integrate import odeint
from scipy.optimize import curve_fit
from sklearn.metrics import mean_squared_error
import gradio as gr
import io
from PIL import Image
import tempfile
class YourModel(BaseModel):
class Config:
arbitrary_types_allowed = True
class BioprocessModel:
def __init__(self, model_type='logistic', maxfev=50000):
self.params = {}
self.r2 = {}
self.rmse = {}
self.datax = []
self.datas = []
self.datap = []
self.dataxp = []
self.datasp = []
self.datapp = []
self.datax_std = []
self.datas_std = []
self.datap_std = []
self.biomass_model = None
self.biomass_diff = None
self.model_type = model_type
self.maxfev = maxfev
@staticmethod
def logistic(time, xo, xm, um):
return (xo * np.exp(um * time)) / (1 - (xo / xm) * (1 - np.exp(um * time)))
@staticmethod
def gompertz(time, xm, um, lag):
return xm * np.exp(-np.exp((um * np.e / xm) * (lag - time) + 1))
@staticmethod
def moser(time, Xm, um, Ks):
# Asegurarse de que el argumento de np.exp no sea demasiado grande
arg = -um * (time - Ks)
# Evitar overflow estableciendo un límite inferior para el exponente
# (o manejar valores muy pequeños de tiempo - Ks de otra manera si es necesario)
# Por ahora, simplemente calculamos como está, pero es un punto a considerar si hay errores.
return Xm * (1 - np.exp(arg))
@staticmethod
def logistic_diff(X, t, params):
xo, xm, um = params
return um * X * (1 - X / xm)
@staticmethod
def gompertz_diff(X, t, params):
xm, um, lag = params
# Evitar división por cero si X es muy pequeño o cero
if X == 0: return 0
# Evitar overflow en np.exp
exponent_val = (um * np.e / xm) * (lag - t) + 1
if exponent_val > np.log(np.finfo(float).max / (um * np.e / xm)) - np.log(abs(X) if X != 0 else 1): # Aproximación para evitar overflow
return X * (um * np.e / xm) * 1e10 # Un valor grande pero no infinito
return X * (um * np.e / xm) * np.exp(exponent_val)
@staticmethod
def moser_diff(X, t, params):
Xm, um, Ks = params
return um * (Xm - X)
def substrate(self, time, so, p, q, biomass_params):
X_t = self.biomass_model(time, *biomass_params)
# dXdt = np.gradient(X_t, time, edge_order=2) # Usar edge_order=2 para mejor estimación en bordes
# Usar una diferencia central más robusta si es posible, o manejar bordes con cuidado
if len(time) < 2:
dXdt = np.zeros_like(X_t)
else:
dXdt = np.gradient(X_t, time, edge_order=1) # edge_order=1 es más seguro para datos ruidosos
integral_X = np.zeros_like(X_t)
if len(time) > 1:
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1)) # Estimar dt para el primer punto
integral_X = np.cumsum(X_t * dt)
return so - p * (X_t - biomass_params[0]) - q * integral_X
def product(self, time, po, alpha, beta, biomass_params):
X_t = self.biomass_model(time, *biomass_params)
# dXdt = np.gradient(X_t, time, edge_order=2)
if len(time) < 2:
dXdt = np.zeros_like(X_t)
else:
dXdt = np.gradient(X_t, time, edge_order=1)
integral_X = np.zeros_like(X_t)
if len(time) > 1:
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
integral_X = np.cumsum(X_t * dt)
return po + alpha * (X_t - biomass_params[0]) + beta * integral_X
def process_data(self, df):
# Asegurar que los nombres de columna de nivel 1 existan
valid_level1_cols = df.columns.get_level_values(1)
biomass_cols = [col for col in df.columns if col[1] == 'Biomasa' and col[0] in df.columns.levels[0]]
substrate_cols = [col for col in df.columns if col[1] == 'Sustrato' and col[0] in df.columns.levels[0]]
product_cols = [col for col in df.columns if col[1] == 'Producto' and col[0] in df.columns.levels[0]]
time_cols = [col for col in df.columns if col[1] == 'Tiempo' and col[0] in df.columns.levels[0]]
if not time_cols:
raise ValueError("No se encontró la columna 'Tiempo' en el DataFrame.")
time_col = time_cols[0] # Asumimos que el tiempo es el mismo para todos los experimentos en una hoja
time = df[time_col].dropna().values # Usar dropna() para manejar NaNs si los hay
if biomass_cols:
data_biomass = [df[col].dropna().values for col in biomass_cols]
# Asegurar que todos los arrays tengan la misma longitud que el tiempo
data_biomass = [arr[:len(time)] for arr in data_biomass if len(arr) >= len(time)]
if data_biomass: # Solo procesar si hay datos válidos
data_biomass_np = np.array(data_biomass)
self.datax.append(data_biomass_np)
self.dataxp.append(np.mean(data_biomass_np, axis=0))
self.datax_std.append(np.std(data_biomass_np, axis=0, ddof=1))
else: # Si no hay datos válidos, añadir arrays vacíos o de ceros con la forma correcta
self.datax.append(np.array([]).reshape(0,len(time)) if len(time)>0 else np.array([]))
self.dataxp.append(np.zeros(len(time)))
self.datax_std.append(np.zeros(len(time)))
if substrate_cols:
data_substrate = [df[col].dropna().values for col in substrate_cols]
data_substrate = [arr[:len(time)] for arr in data_substrate if len(arr) >= len(time)]
if data_substrate:
data_substrate_np = np.array(data_substrate)
self.datas.append(data_substrate_np)
self.datasp.append(np.mean(data_substrate_np, axis=0))
self.datas_std.append(np.std(data_substrate_np, axis=0, ddof=1))
else:
self.datas.append(np.array([]).reshape(0,len(time)) if len(time)>0 else np.array([]))
self.datasp.append(np.zeros(len(time)))
self.datas_std.append(np.zeros(len(time)))
if product_cols:
data_product = [df[col].dropna().values for col in product_cols]
data_product = [arr[:len(time)] for arr in data_product if len(arr) >= len(time)]
if data_product:
data_product_np = np.array(data_product)
self.datap.append(data_product_np)
self.datapp.append(np.mean(data_product_np, axis=0))
self.datap_std.append(np.std(data_product_np, axis=0, ddof=1))
else:
self.datap.append(np.array([]).reshape(0,len(time)) if len(time)>0 else np.array([]))
self.datapp.append(np.zeros(len(time)))
self.datap_std.append(np.zeros(len(time)))
self.time = time
def fit_model(self):
if self.model_type == 'logistic':
self.biomass_model = self.logistic
self.biomass_diff = self.logistic_diff
elif self.model_type == 'gompertz':
self.biomass_model = self.gompertz
self.biomass_diff = self.gompertz_diff
elif self.model_type == 'moser':
self.biomass_model = self.moser
self.biomass_diff = self.moser_diff
def fit_biomass(self, time, biomass):
try:
# Asegurar que biomasa no esté vacío y tenga valores finitos
if biomass is None or len(biomass) == 0 or not np.all(np.isfinite(biomass)):
print(f"Datos de biomasa inválidos para {self.model_type}.")
return None
if self.model_type == 'logistic':
# p0 = [min(biomass) if len(biomass)>0 else 0.1, max(biomass)*1.5 if max(biomass)>0 else 1.0, 0.1]
p0_xo = biomass[0] if len(biomass)>0 else 0.1
p0_xm = max(biomass)*1.5 if len(biomass)>0 and max(biomass)>0 else 1.0
p0_um = 0.1
p0 = [p0_xo, p0_xm, p0_um]
bounds = ([0, 0, 0], [np.inf, np.inf, np.inf]) # Añadir bounds
popt, _ = curve_fit(self.logistic, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, method='trf')
self.params['biomass'] = {'xo': popt[0], 'xm': popt[1], 'um': popt[2]}
y_pred = self.logistic(time, *popt)
elif self.model_type == 'gompertz':
p0_xm = max(biomass) if len(biomass)>0 and max(biomass)>0 else 1.0
p0_um = 0.1
# Estimar lag como el tiempo donde la tasa de crecimiento es máxima
# o donde la biomasa alcanza, por ejemplo, el 10% de Xm si es más simple
p0_lag = time[np.argmax(np.gradient(biomass))] if len(biomass)>1 else time[0] if len(time)>0 else 0
p0 = [p0_xm, p0_um, p0_lag]
bounds = ([0, 0, 0], [np.inf, np.inf, max(time) if len(time)>0 else np.inf]) # lag no debe ser mayor que el tiempo total
popt, _ = curve_fit(self.gompertz, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, method='trf')
self.params['biomass'] = {'xm': popt[0], 'um': popt[1], 'lag': popt[2]}
y_pred = self.gompertz(time, *popt)
elif self.model_type == 'moser':
p0_Xm = max(biomass) if len(biomass)>0 and max(biomass)>0 else 1.0
p0_um = 0.1
p0_Ks = time[0] if len(time)>0 else 0 # Ks podría ser el inicio del crecimiento
p0 = [p0_Xm, p0_um, p0_Ks]
bounds = ([0, 0, -np.inf], [np.inf, np.inf, np.inf]) # Ks puede ser negativo
popt, _ = curve_fit(self.moser, time, biomass, p0=p0, maxfev=self.maxfev, bounds=bounds, method='trf')
self.params['biomass'] = {'Xm': popt[0], 'um': popt[1], 'Ks': popt[2]}
y_pred = self.moser(time, *popt)
else: # Default case or error
return None
# Calcular R2 y RMSE solo si y_pred es válido
if y_pred is not None and np.all(np.isfinite(y_pred)) and len(y_pred) == len(biomass):
if np.sum((biomass - np.mean(biomass)) ** 2) == 0: # Evitar división por cero si todos los y son iguales
self.r2['biomass'] = 1.0 if np.allclose(biomass, y_pred) else 0.0
else:
self.r2['biomass'] = 1 - (np.sum((biomass - y_pred) ** 2) / np.sum((biomass - np.mean(biomass)) ** 2))
self.rmse['biomass'] = np.sqrt(mean_squared_error(biomass, y_pred))
else:
self.r2['biomass'] = np.nan
self.rmse['biomass'] = np.nan
return None # Indicar fallo si y_pred no es válido
return y_pred
except RuntimeError as e: # Específicamente para errores de curve_fit
print(f"Error de convergencia en fit_biomass_{self.model_type}: {e}")
self.params['biomass'] = {}
self.r2['biomass'] = np.nan
self.rmse['biomass'] = np.nan
return None
except Exception as e:
print(f"Error general en fit_biomass_{self.model_type}: {e}")
self.params['biomass'] = {}
self.r2['biomass'] = np.nan
self.rmse['biomass'] = np.nan
return None
def fit_substrate(self, time, substrate, biomass_params):
try:
if substrate is None or len(substrate) == 0 or not np.all(np.isfinite(substrate)):
print(f"Datos de sustrato inválidos para {self.model_type}.")
return None
if not biomass_params: # Si no hay parámetros de biomasa, no se puede ajustar sustrato
print(f"No hay parámetros de biomasa para ajustar sustrato con {self.model_type}.")
return None
p0_so = substrate[0] if len(substrate)>0 else 1.0
p0 = [p0_so, 0.01, 0.01]
bounds = ([0, 0, 0], [np.inf, np.inf, np.inf]) # Asumiendo parámetros no negativos
if self.model_type == 'logistic':
current_biomass_params = [biomass_params['xo'], biomass_params['xm'], biomass_params['um']]
elif self.model_type == 'gompertz':
current_biomass_params = [biomass_params['xm'], biomass_params['um'], biomass_params['lag']]
elif self.model_type == 'moser':
current_biomass_params = [biomass_params['Xm'], biomass_params['um'], biomass_params['Ks']]
else:
return None
popt, _ = curve_fit(
lambda t, so, p, q: self.substrate(t, so, p, q, current_biomass_params),
time, substrate, p0=p0, maxfev=self.maxfev, bounds=bounds, method='trf'
)
self.params['substrate'] = {'so': popt[0], 'p': popt[1], 'q': popt[2]}
y_pred = self.substrate(time, *popt, current_biomass_params)
if y_pred is not None and np.all(np.isfinite(y_pred)) and len(y_pred) == len(substrate):
if np.sum((substrate - np.mean(substrate)) ** 2) == 0:
self.r2['substrate'] = 1.0 if np.allclose(substrate, y_pred) else 0.0
else:
self.r2['substrate'] = 1 - (np.sum((substrate - y_pred) ** 2) / np.sum((substrate - np.mean(substrate)) ** 2))
self.rmse['substrate'] = np.sqrt(mean_squared_error(substrate, y_pred))
else:
self.r2['substrate'] = np.nan
self.rmse['substrate'] = np.nan
return None
return y_pred
except RuntimeError as e:
print(f"Error de convergencia en fit_substrate_{self.model_type}: {e}")
self.params['substrate'] = {}
self.r2['substrate'] = np.nan
self.rmse['substrate'] = np.nan
return None
except Exception as e:
print(f"Error general en fit_substrate_{self.model_type}: {e}")
self.params['substrate'] = {}
self.r2['substrate'] = np.nan
self.rmse['substrate'] = np.nan
return None
def fit_product(self, time, product, biomass_params):
try:
if product is None or len(product) == 0 or not np.all(np.isfinite(product)):
print(f"Datos de producto inválidos para {self.model_type}.")
return None
if not biomass_params:
print(f"No hay parámetros de biomasa para ajustar producto con {self.model_type}.")
return None
p0_po = product[0] if len(product)>0 else 0.0
p0 = [p0_po, 0.01, 0.01]
bounds = ([0, 0, 0], [np.inf, np.inf, np.inf]) # Asumiendo parámetros no negativos
if self.model_type == 'logistic':
current_biomass_params = [biomass_params['xo'], biomass_params['xm'], biomass_params['um']]
elif self.model_type == 'gompertz':
current_biomass_params = [biomass_params['xm'], biomass_params['um'], biomass_params['lag']]
elif self.model_type == 'moser':
current_biomass_params = [biomass_params['Xm'], biomass_params['um'], biomass_params['Ks']]
else:
return None
popt, _ = curve_fit(
lambda t, po, alpha, beta: self.product(t, po, alpha, beta, current_biomass_params),
time, product, p0=p0, maxfev=self.maxfev, bounds=bounds, method='trf'
)
self.params['product'] = {'po': popt[0], 'alpha': popt[1], 'beta': popt[2]}
y_pred = self.product(time, *popt, current_biomass_params)
if y_pred is not None and np.all(np.isfinite(y_pred)) and len(y_pred) == len(product):
if np.sum((product - np.mean(product)) ** 2) == 0:
self.r2['product'] = 1.0 if np.allclose(product, y_pred) else 0.0
else:
self.r2['product'] = 1 - (np.sum((product - y_pred) ** 2) / np.sum((product - np.mean(product)) ** 2))
self.rmse['product'] = np.sqrt(mean_squared_error(product, y_pred))
else:
self.r2['product'] = np.nan
self.rmse['product'] = np.nan
return None
return y_pred
except RuntimeError as e:
print(f"Error de convergencia en fit_product_{self.model_type}: {e}")
self.params['product'] = {}
self.r2['product'] = np.nan
self.rmse['product'] = np.nan
return None
except Exception as e:
print(f"Error general en fit_product_{self.model_type}: {e}")
self.params['product'] = {}
self.r2['product'] = np.nan
self.rmse['product'] = np.nan
return None
def generate_fine_time_grid(self, time):
if len(time) == 0: return np.array([])
time_fine = np.linspace(time.min(), time.max(), 500)
return time_fine
def system(self, y, t, biomass_params_tuple, substrate_params_tuple, product_params_tuple, model_type):
X, S, P = y
# Asegurar que los parámetros no sean None o vacíos
if not biomass_params_tuple: biomass_params_tuple = (0,0,0) # Default
if not substrate_params_tuple: substrate_params_tuple = (0,0,0)
if not product_params_tuple: product_params_tuple = (0,0,0)
if model_type == 'logistic':
# xo, xm, um = biomass_params_tuple # xo no se usa en la diff
dXdt = self.logistic_diff(X, t, biomass_params_tuple)
elif model_type == 'gompertz':
# xm, um, lag = biomass_params_tuple
dXdt = self.gompertz_diff(X, t, biomass_params_tuple)
elif model_type == 'moser':
# Xm, um, Ks = biomass_params_tuple
dXdt = self.moser_diff(X, t, biomass_params_tuple)
else:
dXdt = 0.0
so, p, q = substrate_params_tuple
po, alpha, beta = product_params_tuple
# Evitar valores negativos no físicos para S y P si es necesario
# Esto es una simplificación, modelos más complejos podrían manejar esto de otra forma
dSdt = -p * dXdt - q * X
dPdt = alpha * dXdt + beta * X
# if S + dSdt * (t_step if 't_step' in locals() else 0.01) < 0: dSdt = -S / (t_step if 't_step' in locals() else 0.01) # Evita S negativo
# if P + dPdt * (t_step if 't_step' in locals() else 0.01) < 0: dPdt = -P / (t_step if 't_step' in locals() else 0.01) # Evita P negativo (raro)
return [dXdt, dSdt, dPdt]
def get_initial_conditions(self, time, biomass, substrate, product):
X0, S0, P0 = 0,0,0 # Defaults
if 'biomass' in self.params and self.params['biomass']:
if self.model_type == 'logistic':
X0 = self.params['biomass'].get('xo', biomass[0] if len(biomass)>0 else 0)
elif self.model_type == 'gompertz':
xm = self.params['biomass'].get('xm', 0)
um = self.params['biomass'].get('um', 0)
lag = self.params['biomass'].get('lag', 0)
# Calcular X0 para Gompertz en t=0 (o el primer punto de tiempo)
t_initial = time[0] if len(time)>0 else 0
X0 = xm * np.exp(-np.exp((um * np.e / xm)*(lag - t_initial)+1)) if xm > 0 else (biomass[0] if len(biomass)>0 else 0)
elif self.model_type == 'moser':
Xm = self.params['biomass'].get('Xm', 0)
um = self.params['biomass'].get('um', 0)
Ks = self.params['biomass'].get('Ks', 0)
t_initial = time[0] if len(time)>0 else 0
X0 = Xm*(1 - np.exp(-um*(t_initial - Ks))) if Xm > 0 else (biomass[0] if len(biomass)>0 else 0)
elif len(biomass) > 0:
X0 = biomass[0]
if 'substrate' in self.params and self.params['substrate']:
S0 = self.params['substrate'].get('so', substrate[0] if len(substrate)>0 else 0)
elif len(substrate) > 0:
S0 = substrate[0]
if 'product' in self.params and self.params['product']:
P0 = self.params['product'].get('po', product[0] if len(product)>0 else 0)
elif len(product) > 0:
P0 = product[0]
# Asegurar que las condiciones iniciales no sean NaN
X0 = X0 if np.isfinite(X0) else (biomass[0] if len(biomass)>0 and np.isfinite(biomass[0]) else 0)
S0 = S0 if np.isfinite(S0) else (substrate[0] if len(substrate)>0 and np.isfinite(substrate[0]) else 0)
P0 = P0 if np.isfinite(P0) else (product[0] if len(product)>0 and np.isfinite(product[0]) else 0)
return [X0, S0, P0]
def solve_differential_equations(self, time, biomass, substrate, product):
if 'biomass' not in self.params or not self.params['biomass']:
print("No hay parámetros de biomasa, no se pueden resolver las EDO.")
return None, None, None, time
if len(time) == 0:
print("Tiempo vacío, no se pueden resolver EDO.")
return None, None, None, time
biomass_p = self.params['biomass']
if self.model_type == 'logistic':
biomass_params_tuple = (biomass_p.get('xo',0), biomass_p.get('xm',1), biomass_p.get('um',0.1))
elif self.model_type == 'gompertz':
biomass_params_tuple = (biomass_p.get('xm',1), biomass_p.get('um',0.1), biomass_p.get('lag',0))
elif self.model_type == 'moser':
biomass_params_tuple = (biomass_p.get('Xm',1), biomass_p.get('um',0.1), biomass_p.get('Ks',0))
else:
biomass_params_tuple = (0,0,0) # Default
substrate_p = self.params.get('substrate', {})
substrate_params_tuple = (substrate_p.get('so',0), substrate_p.get('p',0), substrate_p.get('q',0))
product_p = self.params.get('product', {})
product_params_tuple = (product_p.get('po',0), product_p.get('alpha',0), product_p.get('beta',0))
initial_conditions = self.get_initial_conditions(time, biomass, substrate, product)
time_fine = self.generate_fine_time_grid(time)
if len(time_fine) == 0: # Si time_fine está vacío (porque time estaba vacío)
return None, None, None, time
try:
sol = odeint(self.system, initial_conditions, time_fine,
args=(biomass_params_tuple, substrate_params_tuple, product_params_tuple, self.model_type),
tcrit=time) # Añadir tcrit para mejorar la precisión en los puntos de datos originales
except Exception as e:
print(f"Error al resolver EDOs: {e}")
return None, None, None, time_fine
X = sol[:, 0]
S = sol[:, 1]
P = sol[:, 2]
# Opcional: asegurar que S y P no sean negativos si no tiene sentido físico
S = np.maximum(S, 0)
# P = np.maximum(P, 0) # Producto usualmente no necesita esto
return X, S, P, time_fine
def plot_results(self, time, biomass, substrate, product,
y_pred_biomass, y_pred_substrate, y_pred_product,
biomass_std=None, substrate_std=None, product_std=None,
experiment_name='', legend_position='best', params_position='upper right',
show_legend=True, show_params=True,
style='whitegrid',
line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
use_differential=False,
# AGREGAR ESTOS NUEVOS PARÁMETROS:
x_label='Tiempo', y_label_biomass='Biomasa',
y_label_substrate='Sustrato', y_label_product='Producto'):
if y_pred_biomass is None and not use_differential: # Si no hay ajuste y no se usan EDOs, no graficar
print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type} y no se usan EDOs. Omitiendo figura.")
return None
if len(time) == 0:
print(f"No hay datos de tiempo para graficar para {experiment_name}. Omitiendo figura.")
return None
sns.set_style(style)
time_to_plot = time # Por defecto
if use_differential and 'biomass' in self.params and self.params['biomass']:
# Asegurarse de que los datos originales no estén vacíos
if len(biomass)>0 and len(substrate)>0 and len(product)>0:
X_ode, S_ode, P_ode, time_fine_ode = self.solve_differential_equations(time, biomass, substrate, product)
if X_ode is not None: # Si la solución de EDO es exitosa
y_pred_biomass, y_pred_substrate, y_pred_product = X_ode, S_ode, P_ode
time_to_plot = time_fine_ode
else: # Si falla la EDO, usar los ajustes de curve_fit si existen
time_to_plot = self.generate_fine_time_grid(time)
if y_pred_biomass is not None: # Re-evaluar el modelo ajustado en la malla fina
biomass_p = self.params.get('biomass', {})
if biomass_p:
y_pred_biomass = self.biomass_model(time_to_plot, *biomass_p.values())
if y_pred_substrate is not None and 'biomass' in self.params and self.params['biomass']:
substrate_p = self.params.get('substrate', {})
if substrate_p:
y_pred_substrate = self.substrate(time_to_plot, *substrate_p.values(), list(self.params['biomass'].values()))
if y_pred_product is not None and 'biomass' in self.params and self.params['biomass']:
product_p = self.params.get('product', {})
if product_p:
y_pred_product = self.product(time_to_plot, *product_p.values(), list(self.params['biomass'].values()))
else: # Si los datos originales están vacíos, no se puede usar EDO
print(f"Datos originales vacíos para {experiment_name}, no se pueden usar EDOs.")
use_differential = False # Forzar a no usar EDO
time_to_plot = self.generate_fine_time_grid(time) # Usar malla fina para ajustes si existen
# Re-evaluar modelos ajustados en la malla fina
if y_pred_biomass is not None:
biomass_p = self.params.get('biomass', {})
if biomass_p: y_pred_biomass = self.biomass_model(time_to_plot, *biomass_p.values())
if y_pred_substrate is not None and 'biomass' in self.params and self.params['biomass']:
substrate_p = self.params.get('substrate', {})
if substrate_p: y_pred_substrate = self.substrate(time_to_plot, *substrate_p.values(), list(self.params['biomass'].values()))
if y_pred_product is not None and 'biomass' in self.params and self.params['biomass']:
product_p = self.params.get('product', {})
if product_p: y_pred_product = self.product(time_to_plot, *product_p.values(), list(self.params['biomass'].values()))
elif y_pred_biomass is not None: # No EDO, pero hay ajuste, usar malla fina
time_to_plot = self.generate_fine_time_grid(time)
biomass_p = self.params.get('biomass', {})
if biomass_p: y_pred_biomass = self.biomass_model(time_to_plot, *biomass_p.values())
if y_pred_substrate is not None and 'biomass' in self.params and self.params['biomass']:
substrate_p = self.params.get('substrate', {})
if substrate_p: y_pred_substrate = self.substrate(time_to_plot, *substrate_p.values(), list(self.params['biomass'].values()))
if y_pred_product is not None and 'biomass' in self.params and self.params['biomass']:
product_p = self.params.get('product', {})
if product_p: y_pred_product = self.product(time_to_plot, *product_p.values(), list(self.params['biomass'].values()))
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 15))
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()})', fontsize=16)
plots = [
(ax1, biomass, y_pred_biomass, biomass_std, y_label_biomass, 'Modelo', self.params.get('biomass', {}),
self.r2.get('biomass', np.nan), self.rmse.get('biomass', np.nan)),
(ax2, substrate, y_pred_substrate, substrate_std, y_label_substrate, 'Modelo', self.params.get('substrate', {}),
self.r2.get('substrate', np.nan), self.rmse.get('substrate', np.nan)),
(ax3, product, y_pred_product, product_std, y_label_product, 'Modelo', self.params.get('product', {}),
self.r2.get('product', np.nan), self.rmse.get('product', np.nan))
]
for idx, (ax, data, y_pred, data_std, ylabel, model_name, params_dict, r2, rmse) in enumerate(plots):
# Solo graficar datos experimentales si existen y son válidos
if data is not None and len(data) > 0 and np.all(np.isfinite(data)):
if data_std is not None and len(data_std) == len(data) and np.all(np.isfinite(data_std)):
ax.errorbar(time, data, yerr=data_std, fmt=marker_style, color=point_color,
label='Datos experimentales', capsize=5, elinewidth=1, markeredgewidth=1)
else:
ax.plot(time, data, marker=marker_style, linestyle='', color=point_color,
label='Datos experimentales')
# Solo graficar predicciones si existen y son válidas
if y_pred is not None and len(y_pred) > 0 and np.all(np.isfinite(y_pred)) and len(time_to_plot) == len(y_pred):
ax.plot(time_to_plot, y_pred, linestyle=line_style, color=line_color, label=model_name)
ax.set_xlabel(x_label)
ax.set_ylabel(ylabel)
if show_legend:
ax.legend(loc=legend_position)
ax.set_title(f'{ylabel}')
if show_params and params_dict and all(isinstance(v, (int, float)) and np.isfinite(v) for v in params_dict.values()):
param_text = '\n'.join([f"{k} = {v:.3g}" for k, v in params_dict.items()]) # Usar .3g para mejor formato
text = f"{param_text}\nR² = {r2:.3f}\nRMSE = {rmse:.3f}"
if params_position == 'outside right':
bbox_props = dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.7, edgecolor='gray')
ax.annotate(text, xy=(1.02, 0.5), xycoords='axes fraction',
xytext=(10,0), textcoords='offset points', # Pequeño offset
verticalalignment='center', bbox=bbox_props, fontsize=9)
else:
text_x, text_y, ha, va = 0.05, 0.95, 'left', 'top' # Default upper left
if params_position == 'upper right': text_x, ha = 0.95, 'right'
elif params_position == 'lower left': text_y, va = 0.05, 'bottom'
elif params_position == 'lower right': text_x, text_y, ha, va = 0.95, 0.05, 'right', 'bottom'
ax.text(text_x, text_y, text, transform=ax.transAxes,
verticalalignment=va, horizontalalignment=ha,
bbox={'boxstyle': 'round,pad=0.3', 'facecolor':'white', 'alpha':0.7, 'edgecolor':'gray'}, fontsize=9)
ax.grid(True, linestyle=':', alpha=0.7)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=150) # Aumentar DPI para mejor calidad
buf.seek(0)
image = Image.open(buf).convert("RGB")
plt.close(fig)
return image
def plot_combined_results(self, time, biomass, substrate, product,
y_pred_biomass, y_pred_substrate, y_pred_product,
biomass_std=None, substrate_std=None, product_std=None,
experiment_name='', legend_position='best', params_position='upper right',
show_legend=True, show_params=True,
style='whitegrid',
line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o',
use_differential=False,
# AGREGAR ESTOS NUEVOS PARÁMETROS:
x_label='Tiempo', y_label_biomass='Biomasa',
y_label_substrate='Sustrato', y_label_product='Producto'):
if y_pred_biomass is None and not use_differential:
print(f"No se pudo ajustar biomasa para {experiment_name} con {self.model_type} (combinado). Omitiendo figura.")
return None
if len(time) == 0:
print(f"No hay datos de tiempo para graficar (combinado) para {experiment_name}. Omitiendo figura.")
return None
sns.set_style(style)
time_to_plot = time # Por defecto
if use_differential and 'biomass' in self.params and self.params['biomass']:
if len(biomass)>0 and len(substrate)>0 and len(product)>0:
X_ode, S_ode, P_ode, time_fine_ode = self.solve_differential_equations(time, biomass, substrate, product)
if X_ode is not None:
y_pred_biomass, y_pred_substrate, y_pred_product = X_ode, S_ode, P_ode
time_to_plot = time_fine_ode
else: # Fallback a ajustes si EDO falla
time_to_plot = self.generate_fine_time_grid(time)
if y_pred_biomass is not None:
biomass_p = self.params.get('biomass', {})
if biomass_p: y_pred_biomass = self.biomass_model(time_to_plot, *biomass_p.values())
if y_pred_substrate is not None and 'biomass' in self.params and self.params['biomass']:
substrate_p = self.params.get('substrate', {})
if substrate_p: y_pred_substrate = self.substrate(time_to_plot, *substrate_p.values(), list(self.params['biomass'].values()))
if y_pred_product is not None and 'biomass' in self.params and self.params['biomass']:
product_p = self.params.get('product', {})
if product_p: y_pred_product = self.product(time_to_plot, *product_p.values(), list(self.params['biomass'].values()))
else:
print(f"Datos originales vacíos para {experiment_name} (combinado), no se pueden usar EDOs.")
use_differential = False
time_to_plot = self.generate_fine_time_grid(time)
if y_pred_biomass is not None:
biomass_p = self.params.get('biomass', {})
if biomass_p: y_pred_biomass = self.biomass_model(time_to_plot, *biomass_p.values())
if y_pred_substrate is not None and 'biomass' in self.params and self.params['biomass']:
substrate_p = self.params.get('substrate', {})
if substrate_p: y_pred_substrate = self.substrate(time_to_plot, *substrate_p.values(), list(self.params['biomass'].values()))
if y_pred_product is not None and 'biomass' in self.params and self.params['biomass']:
product_p = self.params.get('product', {})
if product_p: y_pred_product = self.product(time_to_plot, *product_p.values(), list(self.params['biomass'].values()))
elif y_pred_biomass is not None: # No EDO, pero hay ajuste, usar malla fina
time_to_plot = self.generate_fine_time_grid(time)
biomass_p = self.params.get('biomass', {})
if biomass_p: y_pred_biomass = self.biomass_model(time_to_plot, *biomass_p.values())
if y_pred_substrate is not None and 'biomass' in self.params and self.params['biomass']:
substrate_p = self.params.get('substrate', {})
if substrate_p: y_pred_substrate = self.substrate(time_to_plot, *substrate_p.values(), list(self.params['biomass'].values()))
if y_pred_product is not None and 'biomass' in self.params and self.params['biomass']:
product_p = self.params.get('product', {})
if product_p: y_pred_product = self.product(time_to_plot, *product_p.values(), list(self.params['biomass'].values()))
fig, ax1 = plt.subplots(figsize=(12, 7)) # Un poco más ancho para acomodar texto
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()})', fontsize=16)
colors = {'Biomasa': 'blue', 'Sustrato': 'green', 'Producto': 'red'}
# Usar los colores de línea y punto definidos por el usuario si es posible,
# pero necesitamos 3 colores distintos.
# Por ahora, mantendremos los colores fijos para la gráfica combinada para claridad.
ax1.set_xlabel(x_label)
ax1.set_ylabel(y_label_biomass, color=colors['Biomasa'])
if biomass is not None and len(biomass)>0 and np.all(np.isfinite(biomass)):
if biomass_std is not None and len(biomass_std)==len(biomass) and np.all(np.isfinite(biomass_std)):
ax1.errorbar(time, biomass, yerr=biomass_std, fmt=marker_style, color=colors['Biomasa'],
label=f'{y_label_biomass} (Datos)', capsize=5, elinewidth=1, markeredgewidth=1)
else:
ax1.plot(time, biomass, marker=marker_style, linestyle='', color=colors['Biomasa'],
label=f'{y_label_biomass} (Datos)')
if y_pred_biomass is not None and len(y_pred_biomass)>0 and np.all(np.isfinite(y_pred_biomass)) and len(time_to_plot)==len(y_pred_biomass):
ax1.plot(time_to_plot, y_pred_biomass, linestyle=line_style, color=colors['Biomasa'],
label=f'{y_label_biomass} (Modelo)')
ax1.tick_params(axis='y', labelcolor=colors['Biomasa'])
ax1.grid(True, linestyle=':', alpha=0.7, axis='y') # Grid solo para el eje y primario
ax2 = ax1.twinx()
ax2.set_ylabel(y_label_substrate, color=colors['Sustrato'])
if substrate is not None and len(substrate)>0 and np.all(np.isfinite(substrate)):
if substrate_std is not None and len(substrate_std)==len(substrate) and np.all(np.isfinite(substrate_std)):
ax2.errorbar(time, substrate, yerr=substrate_std, fmt=marker_style, markerfacecolor='none', markeredgecolor=colors['Sustrato'], ecolor=colors['Sustrato'],
label=f'{y_label_substrate} (Datos)', capsize=5, elinewidth=1, markeredgewidth=1)
else:
ax2.plot(time, substrate, marker=marker_style, markerfacecolor='none', markeredgecolor=colors['Sustrato'], linestyle='', color=colors['Sustrato'],
label=f'{y_label_substrate} (Datos)')
if y_pred_substrate is not None and len(y_pred_substrate)>0 and np.all(np.isfinite(y_pred_substrate)) and len(time_to_plot)==len(y_pred_substrate):
ax2.plot(time_to_plot, y_pred_substrate, linestyle=line_style, color=colors['Sustrato'],
label=f'{y_label_substrate} (Modelo)')
ax2.tick_params(axis='y', labelcolor=colors['Sustrato'])
ax3 = ax1.twinx()
ax3.spines["right"].set_position(("axes", 1.15)) # Ajustar posición para evitar superposición
# ax3.set_frame_on(True) # Ya está por defecto
# ax3.patch.set_visible(False) # No es necesario si el frame está on
# for sp in ax3.spines.values():
# sp.set_visible(True) # Ya está por defecto
ax3.set_ylabel(y_label_product, color=colors['Producto'])
if product is not None and len(product)>0 and np.all(np.isfinite(product)):
if product_std is not None and len(product_std)==len(product) and np.all(np.isfinite(product_std)):
ax3.errorbar(time, product, yerr=product_std, fmt=marker_style, markerfacecolor='none', markeredgecolor=colors['Producto'], ecolor=colors['Producto'],
label=f'{y_label_product} (Datos)', capsize=5, elinewidth=1, markeredgewidth=1)
else:
ax3.plot(time, product, marker=marker_style, markerfacecolor='none', markeredgecolor=colors['Producto'], linestyle='', color=colors['Producto'],
label=f'{y_label_product} (Datos)')
if y_pred_product is not None and len(y_pred_product)>0 and np.all(np.isfinite(y_pred_product)) and len(time_to_plot)==len(y_pred_product):
ax3.plot(time_to_plot, y_pred_product, linestyle=line_style, color=colors['Producto'],
label=f'{y_label_product} (Modelo)')
ax3.tick_params(axis='y', labelcolor=colors['Producto'])
lines_labels = [ax.get_legend_handles_labels() for ax in [ax1, ax2, ax3] if ax.has_data()]
if lines_labels:
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
if show_legend and lines: # Solo mostrar leyenda si hay algo que mostrar
ax1.legend(lines, labels, loc=legend_position, fontsize=9)
if show_params:
texts_to_join = []
if 'biomass' in self.params and self.params['biomass'] and all(isinstance(v, (int, float)) and np.isfinite(v) for v in self.params['biomass'].values()):
param_text_biomass = '\n'.join([f"{k} = {v:.3g}" for k, v in self.params['biomass'].items()])
texts_to_join.append(f"{y_label_biomass}:\n{param_text_biomass}\nR² = {self.r2.get('biomass', np.nan):.3f}\nRMSE = {self.rmse.get('biomass', np.nan):.3f}")
if 'substrate' in self.params and self.params['substrate'] and all(isinstance(v, (int, float)) and np.isfinite(v) for v in self.params['substrate'].values()):
param_text_substrate = '\n'.join([f"{k} = {v:.3g}" for k, v in self.params['substrate'].items()])
texts_to_join.append(f"{y_label_substrate}:\n{param_text_substrate}\nR² = {self.r2.get('substrate', np.nan):.3f}\nRMSE = {self.rmse.get('substrate', np.nan):.3f}")
if 'product' in self.params and self.params['product'] and all(isinstance(v, (int, float)) and np.isfinite(v) for v in self.params['product'].values()):
param_text_product = '\n'.join([f"{k} = {v:.3g}" for k, v in self.params['product'].items()])
texts_to_join.append(f"{y_label_product}:\n{param_text_product}\nR² = {self.r2.get('product', np.nan):.3f}\nRMSE = {self.rmse.get('product', np.nan):.3f}")
total_text = "\n\n".join(texts_to_join)
if total_text: # Solo mostrar si hay texto
if params_position == 'outside right':
bbox_props = dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.7, edgecolor='gray')
# Usar ax3 para anotar fuera, ya que es el más a la derecha
ax3.annotate(total_text, xy=(1.20, 0.5), xycoords='axes fraction', # Ajustar xy para que no se solape con el eje
xytext=(10,0), textcoords='offset points',
verticalalignment='center', bbox=bbox_props, fontsize=8) # Reducir fontsize
else:
text_x, text_y, ha, va = 0.02, 0.98, 'left', 'top' # Default upper left, un poco más adentro
if params_position == 'upper right': text_x, ha = 0.98, 'right'
elif params_position == 'lower left': text_y, va = 0.02, 'bottom'
elif params_position == 'lower right': text_x, text_y, ha, va = 0.98, 0.02, 'right', 'bottom'
ax1.text(text_x, text_y, total_text, transform=ax1.transAxes,
verticalalignment=va, horizontalalignment=ha,
bbox={'boxstyle':'round,pad=0.3', 'facecolor':'white', 'alpha':0.7, 'edgecolor':'gray'}, fontsize=8) # Reducir fontsize
plt.tight_layout(rect=[0, 0.03, 0.85 if params_position == 'outside right' and show_params else 1, 0.95]) # Ajustar right para outside params
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=150)
buf.seek(0)
image = Image.open(buf).convert("RGB")
plt.close(fig)
return image
def process_all_data(file, legend_position, params_position, model_types, experiment_names, lower_bounds, upper_bounds,
mode='independent', style='whitegrid', line_color='#0000FF', point_color='#000000',
line_style='-', marker_style='o', show_legend=True, show_params=True, use_differential=False,
maxfev_val=50000,
# AGREGAR ESTOS NUEVOS PARÁMETROS:
x_label='Tiempo', y_label_biomass='Biomasa',
y_label_substrate='Sustrato', y_label_product='Producto'):
if file is None:
print("No se ha subido ningún archivo.")
return [], pd.DataFrame()
try:
xls = pd.ExcelFile(file.name)
except Exception as e:
print(f"Error al leer el archivo Excel: {e}")
return [], pd.DataFrame()
sheet_names = xls.sheet_names
figures = []
comparison_data = []
experiment_counter = 0
for sheet_name in sheet_names:
try:
df = pd.read_excel(file.name, sheet_name=sheet_name, header=[0, 1])
# Limpiar nombres de columnas (quitar espacios extra, etc.)
df.columns = pd.MultiIndex.from_tuples([(str(c1).strip(), str(c2).strip()) for c1, c2 in df.columns])
except Exception as e:
print(f"Error al leer la hoja '{sheet_name}': {e}")
continue
# Validar que las columnas necesarias existan antes de procesar
required_cols_level1 = ['Tiempo', 'Biomasa', 'Sustrato', 'Producto']
actual_cols_level1 = df.columns.get_level_values(1).unique()
if not all(rc in actual_cols_level1 for rc in required_cols_level1):
print(f"Advertencia: La hoja '{sheet_name}' no contiene todas las columnas requeridas (Tiempo, Biomasa, Sustrato, Producto) en el nivel 1 del encabezado. Saltando esta hoja.")
continue
model_dummy_for_preprocessing = BioprocessModel() # Instancia solo para procesar datos de la hoja actual
try:
model_dummy_for_preprocessing.process_data(df)
except ValueError as e:
print(f"Error al procesar datos de la hoja '{sheet_name}': {e}. Saltando esta hoja.")
continue
time_global_sheet = model_dummy_for_preprocessing.time # Tiempo base de la hoja
if not time_global_sheet.size: # Si no hay datos de tiempo, saltar hoja
print(f"No se encontraron datos de tiempo válidos en la hoja '{sheet_name}'. Saltando.")
continue
if mode == 'independent':
# Iterar sobre los experimentos definidos por el primer nivel de las columnas
# Asegurarse de que los nombres de los experimentos (nivel 0) sean únicos y válidos
unique_experiments_in_sheet = df.columns.levels[0]
for exp_col_name in unique_experiments_in_sheet:
# Extraer datos para este experimento específico
try:
# Filtrar el DataFrame para este experimento
exp_df = df[exp_col_name]
time_exp = exp_df['Tiempo'].dropna().values
biomass = exp_df['Biomasa'].dropna().values
substrate = exp_df['Sustrato'].dropna().values
product = exp_df['Producto'].dropna().values
# Asegurar que todos los arrays tengan la misma longitud que time_exp
min_len = len(time_exp)
biomass = biomass[:min_len]
substrate = substrate[:min_len]
product = product[:min_len]
if not (len(time_exp) > 0 and len(biomass) > 0 and len(substrate) > 0 and len(product) > 0):
print(f"Datos insuficientes para el experimento '{exp_col_name}' en la hoja '{sheet_name}'. Saltando.")
continue
except KeyError as e:
print(f"Faltan columnas (Tiempo, Biomasa, Sustrato o Producto) para el experimento '{exp_col_name}' en la hoja '{sheet_name}': {e}. Saltando este experimento.")
continue
except Exception as e: # Captura general para otros errores de extracción
print(f"Error al extraer datos para el experimento '{exp_col_name}' en la hoja '{sheet_name}': {e}. Saltando este experimento.")
continue
biomass_std = None # Para 'independent', no hay std a menos que se calcule de réplicas dentro de este "experimento"
substrate_std = None
product_std = None
current_experiment_name_label = (experiment_names[experiment_counter] if experiment_counter < len(experiment_names) and experiment_names[experiment_counter]
else f"{sheet_name} - {exp_col_name}")
for model_type in model_types:
model = BioprocessModel(model_type=model_type, maxfev=maxfev_val)
model.fit_model() # Configura self.biomass_model y self.biomass_diff
y_pred_biomass = model.fit_biomass(time_exp, biomass)
if y_pred_biomass is None or not model.params.get('biomass'):
print(f"Fallo el ajuste de biomasa para {current_experiment_name_label}, modelo {model_type}.")
y_pred_substrate = None
y_pred_product = None
else:
y_pred_substrate = model.fit_substrate(time_exp, substrate, model.params['biomass'])
y_pred_product = model.fit_product(time_exp, product, model.params['biomass'])
comparison_data.append({
'Experimento': current_experiment_name_label,
'Modelo': model_type.capitalize(),
'R² Biomasa': model.r2.get('biomass', np.nan),
'RMSE Biomasa': model.rmse.get('biomass', np.nan),
'R² Sustrato': model.r2.get('substrate', np.nan),
'RMSE Sustrato': model.rmse.get('substrate', np.nan),
'R² Producto': model.r2.get('product', np.nan),
'RMSE Producto': model.rmse.get('product', np.nan)
})
# Siempre intentar graficar, plot_results manejará y_pred_x None
fig = model.plot_results(time_exp, biomass, substrate, product,
y_pred_biomass, y_pred_substrate, y_pred_product,
biomass_std, substrate_std, product_std,
current_experiment_name_label,
legend_position, params_position,
show_legend, show_params,
style,
line_color, point_color, line_style, marker_style,
use_differential,
x_label, y_label_biomass, y_label_substrate, y_label_product)
if fig is not None:
figures.append(fig)
experiment_counter += 1
elif mode in ['average', 'combinado']: # Promedio de todos los experimentos en la hoja
try:
# Usar los datos preprocesados por model_dummy_for_preprocessing
time_exp = model_dummy_for_preprocessing.time
biomass = model_dummy_for_preprocessing.dataxp[-1] if model_dummy_for_preprocessing.dataxp else np.array([])
substrate = model_dummy_for_preprocessing.datasp[-1] if model_dummy_for_preprocessing.datasp else np.array([])
product = model_dummy_for_preprocessing.datapp[-1] if model_dummy_for_preprocessing.datapp else np.array([])
if not (time_exp.size > 0 and biomass.size > 0 and substrate.size > 0 and product.size > 0):
print(f"Datos promedio insuficientes en la hoja '{sheet_name}'. Saltando.")
continue # Saltar al siguiente sheet_name
except IndexError: # Si dataxp, etc., están vacíos
print(f"No se pudieron obtener datos promedio de la hoja '{sheet_name}'. Saltando esta hoja.")
continue
biomass_std = model_dummy_for_preprocessing.datax_std[-1] if model_dummy_for_preprocessing.datax_std and len(model_dummy_for_preprocessing.datax_std[-1]) == len(biomass) else None
substrate_std = model_dummy_for_preprocessing.datas_std[-1] if model_dummy_for_preprocessing.datas_std and len(model_dummy_for_preprocessing.datas_std[-1]) == len(substrate) else None
product_std = model_dummy_for_preprocessing.datap_std[-1] if model_dummy_for_preprocessing.datap_std and len(model_dummy_for_preprocessing.datap_std[-1]) == len(product) else None
current_experiment_name_label = (experiment_names[experiment_counter] if experiment_counter < len(experiment_names) and experiment_names[experiment_counter]
else f"{sheet_name} - Promedio")
for model_type in model_types:
model = BioprocessModel(model_type=model_type, maxfev=maxfev_val)
model.fit_model()
y_pred_biomass = model.fit_biomass(time_exp, biomass)
if y_pred_biomass is None or not model.params.get('biomass'):
print(f"Fallo el ajuste de biomasa para {current_experiment_name_label}, modelo {model_type}.")
y_pred_substrate = None
y_pred_product = None
else:
y_pred_substrate = model.fit_substrate(time_exp, substrate, model.params['biomass'])
y_pred_product = model.fit_product(time_exp, product, model.params['biomass'])
comparison_data.append({
'Experimento': current_experiment_name_label,
'Modelo': model_type.capitalize(),
'R² Biomasa': model.r2.get('biomass', np.nan),
'RMSE Biomasa': model.rmse.get('biomass', np.nan),
'R² Sustrato': model.r2.get('substrate', np.nan),
'RMSE Sustrato': model.rmse.get('substrate', np.nan),
'R² Producto': model.r2.get('product', np.nan),
'RMSE Producto': model.rmse.get('product', np.nan)
})
PlottingFunction = model.plot_combined_results if mode == 'combinado' else model.plot_results
fig = PlottingFunction(time_exp, biomass, substrate, product,
y_pred_biomass, y_pred_substrate, y_pred_product,
biomass_std, substrate_std, product_std,
current_experiment_name_label,
legend_position, params_position,
show_legend, show_params,
style,
line_color, point_color, line_style, marker_style,
use_differential,
x_label, y_label_biomass, y_label_substrate, y_label_product)
if fig is not None:
figures.append(fig)
experiment_counter += 1
comparison_df = pd.DataFrame(comparison_data)
if not comparison_df.empty:
# Convertir a numérico antes de ordenar, errores a NaN para que no rompa el sort
for col in ['R² Biomasa', 'RMSE Biomasa', 'R² Sustrato', 'RMSE Sustrato', 'R² Producto', 'RMSE Producto']:
comparison_df[col] = pd.to_numeric(comparison_df[col], errors='coerce')
comparison_df_sorted = comparison_df.sort_values(
by=['Experimento', 'R² Biomasa', 'R² Sustrato', 'R² Producto', 'RMSE Biomasa', 'RMSE Sustrato', 'RMSE Producto'],
ascending=[True, False, False, False, True, True, True] # Ordenar por experimento, luego por R2 (desc) y RMSE (asc)
).reset_index(drop=True)
else:
comparison_df_sorted = pd.DataFrame(columns=[ # Asegurar que el DF vacío tenga las columnas correctas
'Experimento', 'Modelo', 'R² Biomasa', 'RMSE Biomasa',
'R² Sustrato', 'RMSE Sustrato', 'R² Producto', 'RMSE Producto'
])
return figures, comparison_df_sorted
def create_interface():
with gr.Blocks(theme=gr.themes.Soft()) as demo: # Usar un tema suave
gr.Markdown("# Modelos Cinéticos de Bioprocesos")
gr.Markdown(r"""
Ajuste y visualización de modelos cinéticos (Logístico, Gompertz, Moser) para crecimiento microbiano,
consumo de sustrato y formación de producto (Luedeking-Piret).
**Instrucciones:**
1. Cargue un archivo Excel. El formato esperado es:
- Cada hoja representa un conjunto de datos o condición experimental.
- La primera fila del Excel debe ser el nombre del tratamiento/experimento (ej: Control, Trat1, Trat2...).
- La segunda fila debe ser el tipo de dato (Tiempo, Biomasa, Sustrato, Producto).
- Las columnas subsiguientes son las réplicas o mediciones.
- Ejemplo:
| Experimento A | Experimento A | Experimento B | Experimento B |
|---------------|---------------|---------------|---------------|
| Tiempo | Biomasa | Tiempo | Biomasa |
| 0 | 0.1 | 0 | 0.12 |
| 1 | 0.5 | 1 | 0.6 |
| ... | ... | ... | ... |
2. Configure los parámetros de visualización y modelado.
3. Haga clic en "Simular".
""")
with gr.Tabs():
with gr.TabItem("Carga y Configuración Principal"):
file_input = gr.File(label="Subir archivo Excel (.xlsx)", file_types=['.xlsx'])
with gr.Row():
model_types = gr.CheckboxGroup(
choices=["logistic", "gompertz", "moser"],
label="Tipo(s) de Modelo de Crecimiento",
value=["logistic"],
info="Seleccione uno o más modelos para ajustar."
)
mode = gr.Radio(
["independent", "average", "combinado"],
label="Modo de Análisis de Datos",
value="independent",
info=(
"- Independent: Analiza cada columna de 'Experimento' (nivel 0 del encabezado) por separado.\n"
"- Average: Promedia todas las réplicas dentro de una hoja y ajusta un modelo a los promedios.\n"
"- Combinado: Similar a 'Average', pero grafica Biomasa, Sustrato y Producto en un solo gráfico con múltiples ejes Y."
)
)
experiment_names = gr.Textbox(
label="Nombres de los Tratamientos/Experimentos (opcional, uno por línea)",
placeholder="Tratamiento Control\nTratamiento con Inductor\n...",
lines=3,
info="Si se deja vacío, se usarán los nombres de las hojas/columnas del Excel."
)
maxfev_input = gr.Number(label="maxfev (Máx. iteraciones para ajuste)", value=50000, minimum=1000, step=1000)
use_differential = gr.Checkbox(label="Resolver y graficar usando Ecuaciones Diferenciales (EDOs)", value=False,
info="Si se marca, las curvas ajustadas se generarán resolviendo las EDOs del sistema. Si no, se usarán las ecuaciones algebraicas ajustadas.")
with gr.TabItem("Configuración de Gráficos"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("#### Apariencia General")
style_dropdown = gr.Dropdown(choices=['whitegrid', 'darkgrid', 'white', 'dark', 'ticks'], label="Estilo de gráfico (Seaborn)", value='whitegrid')
show_legend = gr.Checkbox(label="Mostrar Leyenda", value=True)
legend_position = gr.Radio(
choices=["best", "upper left", "upper right", "lower left", "lower right"],
label="Posición de la leyenda", value="best"
)
with gr.Column(scale=1):
gr.Markdown("#### Parámetros en Gráfico")
show_params = gr.Checkbox(label="Mostrar Parámetros y Métricas (R², RMSE)", value=True)
params_positions = ["upper right", "upper left", "lower left", "lower right", "outside right"]
params_position = gr.Radio(
choices=params_positions, label="Posición de los parámetros", value="upper right"
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("#### Colores y Estilos de Línea")
line_color_picker = gr.ColorPicker(label="Color de la línea del modelo", value='#0000FF')
point_color_picker = gr.ColorPicker(label="Color de los puntos (datos)", value='#000000')
with gr.Column(scale=1):
gr.Markdown("#### Estilos de Marcador y Línea")
line_style_options = ['-', '--', '-.', ':']
line_style_dropdown = gr.Dropdown(choices=line_style_options, label="Estilo de línea del modelo", value='-')
marker_style_options = ['o', 's', '^', 'v', 'D', 'x', '+', '*']
marker_style_dropdown = gr.Dropdown(choices=marker_style_options, label="Estilo de marcador (datos)", value='o')
# Controles para nombres de ejes
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("#### Etiquetas de Ejes")
x_axis_label = gr.Textbox(
label="Etiqueta del eje X", value="Tiempo (h)",
placeholder="Ejemplo: Tiempo (h), Days, Hours"
)
y_axis_biomass = gr.Textbox(
label="Etiqueta del eje Y - Biomasa", value="Biomasa (g/L)",
placeholder="Ejemplo: Biomasa (g/L), Cell Density"
)
with gr.Column(scale=1):
gr.Markdown("#### (Continuación Etiquetas Y)") # Espaciador
y_axis_substrate = gr.Textbox(
label="Etiqueta del eje Y - Sustrato", value="Sustrato (g/L)",
placeholder="Ejemplo: Sustrato (g/L), Glucose"
)
y_axis_product = gr.Textbox(
label="Etiqueta del eje Y - Producto", value="Producto (g/L)",
placeholder="Ejemplo: Producto (g/L), Ethanol"
)
# Bounds (opcional, podría ser una característica avanzada para otra pestaña)
# with gr.TabItem("Configuración Avanzada (Bounds)"):
# gr.Markdown("Opcional: Definir límites para los parámetros del modelo durante el ajuste. Usar con precaución.")
# with gr.Row():
# lower_bounds = gr.Textbox(
# label="Límites Inferiores (uno por línea, formato: p1,p2,p3)",
# placeholder="Ej: 0,0,0 (para logístico xo,xm,um)\n0,0,0 (para sustrato so,p,q)\n...",
# lines=3, info="Dejar vacío para no usar límites inferiores."
# )
# upper_bounds = gr.Textbox(
# label="Límites Superiores (uno por línea, formato: p1,p2,p3)",
# placeholder="Ej: inf,inf,inf\ninf,inf,inf\n...",
# lines=3, info="Usar 'inf' para infinito. Dejar vacío para no usar límites superiores."
# )
# Por ahora, los bounds no se usarán explícitamente desde la UI para simplificar.
# Se pueden añadir internamente en fit_biomass, etc. si es necesario.
lower_bounds = gr.Textbox(value="", visible=False) # Ocultos por ahora
upper_bounds = gr.Textbox(value="", visible=False) # Ocultos por ahora
simulate_btn = gr.Button("Simular y Graficar", variant="primary")
gr.Markdown("---")
gr.Markdown("## Resultados")
with gr.Tabs():
with gr.TabItem("Gráficos"):
output_gallery = gr.Gallery(label="Figuras Generadas", columns=[2,1], height='auto', object_fit="contain")
with gr.TabItem("Tabla Comparativa"):
output_table = gr.Dataframe(
label="Tabla Comparativa de Modelos y Métricas de Ajuste",
headers=["Experimento", "Modelo", "R² Biomasa", "RMSE Biomasa",
"R² Sustrato", "RMSE Sustrato", "R² Producto", "RMSE Producto"],
interactive=False,
wrap=True,
height=400
)
export_btn = gr.Button("Exportar Tabla a Excel")
file_output_excel = gr.File(label="Descargar Tabla Excel", file_count="single")
state_df = gr.State(pd.DataFrame()) # Para guardar el dataframe de la tabla para exportar
def process_and_plot_wrapper(file, legend_pos, params_pos, model_ts, analysis_mode, exp_names_str,
# lower_b_str, upper_b_str, # Omitidos por ahora
plot_style, line_c, point_c, line_s, marker_s,
show_leg, show_par, use_diff_eq, maxfev,
x_lab, y_lab_bio, y_lab_sub, y_lab_prod): # Nuevos parámetros de etiquetas
if file is None:
gr.Warning("Por favor, cargue un archivo Excel.")
return [], pd.DataFrame(), pd.DataFrame() # Devuelve valores vacíos para todas las salidas
experiment_names_list = [name.strip() for name in exp_names_str.strip().split('\n') if name.strip()]
# Bounds no se usan desde la UI por ahora
lower_bounds_list = []
upper_bounds_list = []
# Llamada a la función principal de procesamiento
figures, comparison_df = process_all_data(
file, legend_pos, params_pos, model_ts, experiment_names_list,
lower_bounds_list, upper_bounds_list, # Pasando listas vacías
mode=analysis_mode, style=plot_style,
line_color=line_c, point_color=point_c, line_style=line_s, marker_style=marker_s,
show_legend=show_leg, show_params=show_par, use_differential=use_diff_eq,
maxfev_val=int(maxfev),
# Pasando las nuevas etiquetas
x_label=x_lab, y_label_biomass=y_lab_bio,
y_label_substrate=y_lab_sub, y_label_product=y_lab_prod
)
if not figures and comparison_df.empty:
gr.Info("No se generaron figuras ni datos. Revise la consola para mensajes de error o advertencias sobre el formato del archivo.")
return figures, comparison_df, comparison_df # El tercer output es para state_df
simulate_btn.click(
fn=process_and_plot_wrapper,
inputs=[file_input, legend_position, params_position, model_types, mode, experiment_names,
# lower_bounds, upper_bounds, # Omitidos
style_dropdown, line_color_picker, point_color_picker, line_style_dropdown, marker_style_dropdown,
show_legend, show_params, use_differential, maxfev_input,
# AGREGAR ESTOS INPUTS:
x_axis_label, y_axis_biomass, y_axis_substrate, y_axis_product],
outputs=[output_gallery, output_table, state_df]
)
def export_excel_fn(df_to_export):
if df_to_export is None or df_to_export.empty:
gr.Info("No hay datos en la tabla para exportar.")
return None
try:
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
df_to_export.to_excel(tmp.name, index=False)
return tmp.name
except Exception as e:
gr.Error(f"Error al exportar a Excel: {e}")
return None
export_btn.click(
fn=export_excel_fn,
inputs=state_df,
outputs=file_output_excel
)
gr.Markdown("---")
gr.Markdown("Desarrollado con Gradio y Python. Modelo base y mejoras por la comunidad.")
return demo
if __name__ == '__main__':
# Para ejecutar localmente si no estás en un notebook
# import os
# os.environ['GRADIO_TEMP_DIR'] = os.path.join(os.getcwd(), "gradio_tmp") # Opcional: definir dir temporal
demo = create_interface()
demo.launch(share=True, debug=True) # share=True para enlace público, debug=True para ver errores en consola