#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