Update app.py
Browse files
app.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1 |
import os
|
2 |
-
#
|
3 |
-
# If you must do it in the script, ensure it's what you intend.
|
4 |
-
os.system("pip install --upgrade gradio") # Commenting out for typical execution, assuming Gradio is installed.
|
5 |
|
6 |
from pydantic import BaseModel
|
7 |
import numpy as np
|
@@ -27,76 +25,87 @@ class BioprocessModel:
|
|
27 |
self.params = {}
|
28 |
self.r2 = {}
|
29 |
self.rmse = {}
|
30 |
-
self.datax = []
|
31 |
self.datas = []
|
32 |
self.datap = []
|
33 |
-
self.dataxp = []
|
34 |
self.datasp = []
|
35 |
self.datapp = []
|
36 |
-
self.datax_std = []
|
37 |
self.datas_std = []
|
38 |
self.datap_std = []
|
39 |
self.biomass_model = None
|
40 |
self.biomass_diff = None
|
41 |
self.model_type = model_type
|
42 |
self.maxfev = maxfev
|
43 |
-
self.time = None
|
44 |
|
45 |
@staticmethod
|
46 |
def logistic(time, xo, xm, um):
|
47 |
-
if xm
|
|
|
|
|
48 |
return np.full_like(time, np.nan)
|
49 |
term_exp = np.exp(um * time)
|
50 |
denominator = (xm - xo + xo * term_exp)
|
51 |
-
denominator = np.where(denominator == 0, 1e-9, denominator)
|
52 |
return (xo * xm * term_exp) / denominator
|
53 |
|
|
|
54 |
@staticmethod
|
55 |
def gompertz(time, xm, um, lag):
|
56 |
-
if xm <= 0 or um <=0 :
|
57 |
return np.full_like(time, np.nan)
|
58 |
exp_term = (um * np.e / xm) * (lag - time) + 1
|
59 |
-
exp_term_clipped = np.clip(exp_term, -np.inf, 700)
|
60 |
return xm * np.exp(-np.exp(exp_term_clipped))
|
61 |
|
62 |
@staticmethod
|
63 |
def moser(time, Xm, um, Ks):
|
64 |
if Xm <=0 or um <=0: return np.full_like(time, np.nan)
|
|
|
65 |
return Xm * (1 - np.exp(-um * (time - Ks)))
|
66 |
|
67 |
@staticmethod
|
68 |
def baranyi(time, X0, Xm, um, lag):
|
69 |
-
if X0 <= 0 or Xm <= X0 or um <= 0 or lag < 0:
|
70 |
return np.full_like(time, np.nan)
|
71 |
-
|
72 |
log_arg_A = np.exp(-um * time) + np.exp(-um * lag) - np.exp(-um * (time + lag))
|
73 |
-
log_arg_A = np.where(log_arg_A <= 1e-9, 1e-9, log_arg_A)
|
74 |
A_t = time + (1 / um) * np.log(log_arg_A)
|
|
|
75 |
exp_um_At = np.exp(um * A_t)
|
76 |
-
exp_um_At_clipped = np.clip(exp_um_At, -np.inf, 700)
|
|
|
77 |
numerator = Xm * exp_um_At_clipped
|
78 |
denominator = (Xm / X0 - 1) + exp_um_At_clipped
|
79 |
-
denominator = np.where(denominator == 0, 1e-9, denominator)
|
|
|
80 |
return numerator / denominator
|
81 |
|
82 |
@staticmethod
|
83 |
def logistic_diff(X, t, params):
|
|
|
|
|
84 |
_, xm, um = params
|
85 |
if xm == 0: return 0
|
86 |
return um * X * (1 - X / xm)
|
87 |
|
88 |
@staticmethod
|
89 |
def gompertz_diff(X, t, params):
|
|
|
90 |
xm, um, lag = params
|
91 |
-
if xm == 0 or X <= 1e-9 : return 0 # Avoid
|
92 |
k_val = um * np.e / xm
|
93 |
u_val = k_val * (lag - t) + 1
|
94 |
-
u_val_clipped = np.clip(u_val, -np.inf, 700)
|
95 |
return X * k_val * np.exp(u_val_clipped)
|
96 |
|
97 |
@staticmethod
|
98 |
def moser_diff(X, t, params):
|
99 |
-
Xm, um,
|
|
|
100 |
return um * (Xm - X)
|
101 |
|
102 |
def substrate(self, time, so, p, q, biomass_params_list):
|
@@ -104,16 +113,29 @@ class BioprocessModel:
|
|
104 |
return np.full_like(time, np.nan)
|
105 |
X_t = self.biomass_model(time, *biomass_params_list)
|
106 |
if np.any(np.isnan(X_t)): return np.full_like(time, np.nan)
|
|
|
107 |
integral_X = np.zeros_like(X_t)
|
108 |
if len(time) > 1:
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
if self.model_type in ['logistic', 'baranyi']: X0_calc = biomass_params_list[0]
|
113 |
-
elif self.model_type == 'gompertz': X0_calc = self.gompertz(0, *biomass_params_list)
|
114 |
-
elif self.model_type == 'moser': X0_calc = self.moser(0, *biomass_params_list)
|
115 |
-
else: X0_calc = X_t[0] if len(X_t)>0 else 0
|
116 |
-
|
|
|
117 |
return so - p * (X_t - X0) - q * integral_X
|
118 |
|
119 |
def product(self, time, po, alpha, beta, biomass_params_list):
|
@@ -121,88 +143,116 @@ class BioprocessModel:
|
|
121 |
return np.full_like(time, np.nan)
|
122 |
X_t = self.biomass_model(time, *biomass_params_list)
|
123 |
if np.any(np.isnan(X_t)): return np.full_like(time, np.nan)
|
|
|
124 |
integral_X = np.zeros_like(X_t)
|
125 |
if len(time) > 1:
|
126 |
-
|
127 |
-
|
128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
if self.model_type in ['logistic', 'baranyi']: X0_calc = biomass_params_list[0]
|
130 |
elif self.model_type == 'gompertz': X0_calc = self.gompertz(0, *biomass_params_list)
|
131 |
elif self.model_type == 'moser': X0_calc = self.moser(0, *biomass_params_list)
|
132 |
-
else: X0_calc = X_t[0] if len(X_t)>0 else 0
|
133 |
-
|
|
|
134 |
return po + alpha * (X_t - X0) + beta * integral_X
|
135 |
|
136 |
def process_data(self, df):
|
|
|
137 |
biomass_cols = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Biomasa']
|
138 |
substrate_cols = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Sustrato']
|
139 |
product_cols = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Producto']
|
140 |
-
|
141 |
time_col_candidates = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Tiempo']
|
|
|
142 |
if not time_col_candidates:
|
143 |
-
raise ValueError("La columna ('*', 'Tiempo') no se encuentra en el DataFrame.")
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
#
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
if self.model_type == 'logistic':
|
207 |
self.biomass_model = self.logistic; self.biomass_diff = self.logistic_diff
|
208 |
elif self.model_type == 'gompertz':
|
@@ -210,71 +260,113 @@ class BioprocessModel:
|
|
210 |
elif self.model_type == 'moser':
|
211 |
self.biomass_model = self.moser; self.biomass_diff = self.moser_diff
|
212 |
elif self.model_type == 'baranyi':
|
213 |
-
self.biomass_model = self.baranyi; self.biomass_diff = None
|
214 |
else: raise ValueError(f"Modelo de biomasa desconocido: {self.model_type}")
|
215 |
|
216 |
def fit_biomass(self, time, biomass):
|
217 |
time = np.asarray(time, dtype=float); biomass = np.asarray(biomass, dtype=float)
|
|
|
|
|
|
|
|
|
|
|
218 |
if len(time) != len(biomass):
|
|
|
219 |
min_len = min(len(time), len(biomass)); time = time[:min_len]; biomass = biomass[:min_len]
|
220 |
-
if min_len <
|
221 |
-
|
|
|
|
|
|
|
222 |
time = time[valid_indices]; biomass = biomass[valid_indices]
|
|
|
223 |
num_params = 4 if self.model_type == 'baranyi' else 3
|
224 |
if len(time) < num_params:
|
225 |
-
print(f"No hay suficientes datos válidos ({len(time)}) para {self.model_type} ({num_params} params).")
|
226 |
self.r2['biomass'] = np.nan; self.rmse['biomass'] = np.nan; return None
|
227 |
-
|
228 |
-
|
|
|
229 |
self.r2['biomass'] = np.nan; self.rmse['biomass'] = np.nan; return None
|
|
|
|
|
|
|
|
|
|
|
230 |
try:
|
231 |
popt, y_pred = None, None
|
|
|
|
|
|
|
232 |
if self.model_type == 'logistic':
|
233 |
-
xo_g, xm_g, um_g = biomass[0]
|
234 |
-
if xm_g <= xo_g: xm_g = xo_g
|
235 |
-
p0=[xo_g, xm_g, um_g]
|
236 |
-
|
237 |
-
|
|
|
|
|
|
|
238 |
self.params['biomass']={'Xo':popt[0],'Xm':popt[1],'um':popt[2]}; y_pred=self.logistic(time,*popt)
|
|
|
239 |
elif self.model_type == 'gompertz':
|
240 |
-
xm_g,um_g=max(biomass)
|
241 |
-
|
|
|
242 |
lag_g=time[lag_idx[0]] if len(lag_idx)>0 and time[lag_idx[0]]>=0 else (time[0] if len(time)>0 and time[0]>=0 else 0)
|
243 |
-
p0=[xm_g,um_g,lag_g]
|
244 |
-
|
|
|
245 |
self.params['biomass']={'Xm':popt[0],'um':popt[1],'lag':popt[2]}; y_pred=self.gompertz(time,*popt)
|
|
|
246 |
elif self.model_type == 'moser':
|
247 |
-
Xm_g,um_g,Ks_g=max(biomass)
|
248 |
-
p0=[Xm_g,um_g,Ks_g]
|
249 |
-
|
|
|
250 |
self.params['biomass']={'Xm':popt[0],'um':popt[1],'Ks':popt[2]}; y_pred=self.moser(time,*popt)
|
|
|
251 |
elif self.model_type == 'baranyi':
|
252 |
-
X0_g,Xm_g,um_g=biomass[0]
|
253 |
-
if Xm_g<=X0_g: Xm_g=X0_g
|
254 |
-
lag_idx_b=np.where(biomass >
|
255 |
lag_g_b=time[lag_idx_b[0]] if len(lag_idx_b)>0 and time[lag_idx_b[0]]>=0 else (time[0] if len(time)>0 and time[0]>=0 else 0)
|
256 |
-
p0=[X0_g,Xm_g,um_g,lag_g_b]
|
257 |
-
|
258 |
-
|
|
|
|
|
|
|
259 |
self.params['biomass']={'X0':popt[0],'Xm':popt[1],'um':popt[2],'lag':popt[3]}; y_pred=self.baranyi(time,*popt)
|
260 |
-
else:
|
|
|
|
|
|
|
261 |
if y_pred is None or np.any(np.isnan(y_pred)) or np.any(np.isinf(y_pred)):
|
|
|
262 |
self.r2['biomass']=np.nan; self.rmse['biomass']=np.nan; self.params['biomass']={}; return None
|
|
|
263 |
ss_res=np.sum((biomass-y_pred)**2); ss_tot=np.sum((biomass-np.mean(biomass))**2)
|
264 |
-
self.r2['biomass']=
|
|
|
265 |
self.rmse['biomass']=np.sqrt(mean_squared_error(biomass,y_pred)); return y_pred
|
266 |
-
except RuntimeError as e: print(f"RuntimeError fit_biomass {self.model_type}: {e}"); self.params['biomass']={}; self.r2['biomass']=np.nan; self.rmse['biomass']=np.nan; return None
|
267 |
-
except Exception as e: print(f"Error fit_biomass {self.model_type}: {e}"); self.params['biomass']={}; self.r2['biomass']=np.nan; self.rmse['biomass']=np.nan; return None
|
268 |
|
269 |
-
|
270 |
-
|
|
|
|
|
|
|
|
|
|
|
271 |
time=np.asarray(time,dtype=float); substrate=np.asarray(substrate,dtype=float)
|
272 |
valid_idx=~np.isnan(time)&~np.isnan(substrate); time=time[valid_idx]; substrate=substrate[valid_idx]
|
273 |
-
if len(time)<3: self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; return None
|
|
|
274 |
try:
|
275 |
biomass_vals=list(biomass_params_dict.values())
|
276 |
-
p0=[substrate[0] if len(substrate)>0 else 1.0,0.1,0.01];
|
277 |
-
|
|
|
278 |
self.params['substrate']={'so':popt[0],'p':popt[1],'q':popt[2]}; y_pred=self.substrate(time,*popt,biomass_vals)
|
279 |
if np.any(np.isnan(y_pred)) or np.any(np.isinf(y_pred)): self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; self.params['substrate']={}; return None
|
280 |
ss_res=np.sum((substrate-y_pred)**2); ss_tot=np.sum((substrate-np.mean(substrate))**2)
|
@@ -283,15 +375,19 @@ class BioprocessModel:
|
|
283 |
except RuntimeError as e: print(f"RuntimeError fit_substrate {self.model_type}: {e}"); self.params['substrate']={}; self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; return None
|
284 |
except Exception as e: print(f"Error fit_substrate {self.model_type}: {e}"); self.params['substrate']={}; self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; return None
|
285 |
|
286 |
-
def fit_product(self, time, product, biomass_params_dict):
|
287 |
-
if not biomass_params_dict or not self.params.get('biomass'):
|
|
|
|
|
288 |
time=np.asarray(time,dtype=float); product=np.asarray(product,dtype=float)
|
289 |
valid_idx=~np.isnan(time)&~np.isnan(product); time=time[valid_idx]; product=product[valid_idx]
|
290 |
-
if len(time)<3: self.r2['product']=np.nan; self.rmse['product']=np.nan; return None
|
|
|
291 |
try:
|
292 |
biomass_vals=list(biomass_params_dict.values())
|
293 |
-
p0=[product[0] if len(product)>0 else 0.0,0.1,0.01];
|
294 |
-
|
|
|
295 |
self.params['product']={'po':popt[0],'alpha':popt[1],'beta':popt[2]}; y_pred=self.product(time,*popt,biomass_vals)
|
296 |
if np.any(np.isnan(y_pred)) or np.any(np.isinf(y_pred)): self.r2['product']=np.nan; self.rmse['product']=np.nan; self.params['product']={}; return None
|
297 |
ss_res=np.sum((product-y_pred)**2); ss_tot=np.sum((product-np.mean(product))**2)
|
@@ -300,65 +396,92 @@ class BioprocessModel:
|
|
300 |
except RuntimeError as e: print(f"RuntimeError fit_product {self.model_type}: {e}"); self.params['product']={}; self.r2['product']=np.nan; self.rmse['product']=np.nan; return None
|
301 |
except Exception as e: print(f"Error fit_product {self.model_type}: {e}"); self.params['product']={}; self.r2['product']=np.nan; self.rmse['product']=np.nan; return None
|
302 |
|
303 |
-
def generate_fine_time_grid(self,
|
304 |
-
if
|
305 |
-
t_min,t_max=np.min(
|
306 |
-
return np.array([t_min]) if t_min==t_max else np.linspace(t_min,t_max,
|
307 |
|
308 |
def system(self, y, t, biomass_params_ode, substrate_params, product_params, model_type_ode):
|
309 |
-
X,S,P=y; X=max(X,0); dXdt=0.0
|
|
|
310 |
if model_type_ode=='logistic': dXdt=self.logistic_diff(X,t,biomass_params_ode)
|
311 |
elif model_type_ode=='gompertz': dXdt=self.gompertz_diff(X,t,biomass_params_ode)
|
312 |
elif model_type_ode=='moser': dXdt=self.moser_diff(X,t,biomass_params_ode)
|
313 |
-
else: dXdt=0.0
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
if 'biomass' in self.params and self.params['biomass']:
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
|
335 |
def solve_differential_equations(self, time, biomass, substrate, product):
|
336 |
-
if self.biomass_diff is None: return None,None,None,time
|
337 |
-
if 'biomass' not in self.params or not self.params['biomass']: return None,None,None,time
|
338 |
-
if time is None or len(time)==0: return None,None,None,np.array([])
|
339 |
|
340 |
-
biomass_p_ode=[]
|
341 |
-
if self.model_type=='logistic': biomass_p_ode=[self.params['biomass']['Xo'],self.params['biomass']['Xm'],self.params['biomass']['um']]
|
342 |
elif self.model_type=='gompertz': biomass_p_ode=[self.params['biomass']['Xm'],self.params['biomass']['um'],self.params['biomass']['lag']]
|
343 |
elif self.model_type=='moser': biomass_p_ode=[self.params['biomass']['Xm'],self.params['biomass']['um'],self.params['biomass']['Ks']]
|
344 |
-
else: return None,None,None,time
|
345 |
|
346 |
substrate_p=[self.params.get('substrate',{}).get('so',0), self.params.get('substrate',{}).get('p',0), self.params.get('substrate',{}).get('q',0)]
|
347 |
product_p=[self.params.get('product',{}).get('po',0), self.params.get('product',{}).get('alpha',0), self.params.get('product',{}).get('beta',0)]
|
348 |
|
349 |
-
init_cond=self.get_initial_conditions(time,biomass,substrate,product)
|
350 |
time_f=self.generate_fine_time_grid(time)
|
351 |
-
if len(time_f)==0: return None,None,None,time
|
352 |
-
|
353 |
-
hmax_val=(time_f[-1]-time_f[0])/
|
354 |
try:
|
355 |
sol=odeint(self.system,init_cond,time_f,args=(biomass_p_ode,substrate_p,product_p,self.model_type),rtol=1e-6,atol=1e-6,hmax=hmax_val)
|
356 |
except Exception as e_ode:
|
357 |
-
print(f"Error
|
358 |
try: sol=odeint(self.system,init_cond,time_f,args=(biomass_p_ode,substrate_p,product_p,self.model_type),rtol=1e-6,atol=1e-6,method='lsoda',hmax=hmax_val)
|
359 |
-
except Exception as e_lsoda: print(f"Error
|
360 |
-
|
361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
362 |
def plot_results(self, time, biomass, substrate, product,
|
363 |
y_pred_biomass_fit, y_pred_substrate_fit, y_pred_product_fit,
|
364 |
biomass_std=None, substrate_std=None, product_std=None,
|
@@ -370,11 +493,12 @@ class BioprocessModel:
|
|
370 |
|
371 |
y_pred_b,y_pred_s,y_pred_p = y_pred_biomass_fit,y_pred_substrate_fit,y_pred_product_fit
|
372 |
time_curves = self.generate_fine_time_grid(time)
|
373 |
-
X_ode_success = False
|
374 |
|
375 |
if y_pred_biomass_fit is None and not (use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']):
|
376 |
-
|
377 |
-
|
|
|
378 |
|
379 |
can_use_ode = use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']
|
380 |
|
@@ -386,19 +510,21 @@ class BioprocessModel:
|
|
386 |
if X_ode_res is not None:
|
387 |
y_pred_b,y_pred_s,y_pred_p,time_curves = X_ode_res,S_ode_res,P_ode_res,time_fine_ode
|
388 |
X_ode_success = True
|
389 |
-
else:
|
390 |
-
print(f"
|
391 |
-
|
|
|
392 |
b_params=list(self.params['biomass'].values())
|
393 |
-
y_pred_b=self.biomass_model(time_curves,*b_params)
|
394 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
395 |
s_params=list(self.params['substrate'].values()); y_pred_s=self.substrate(time_curves,*s_params,b_params)
|
396 |
else: y_pred_s=np.full_like(time_curves,np.nan)
|
397 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
398 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
399 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
400 |
-
else:
|
401 |
-
|
|
|
402 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
403 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
404 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
@@ -407,39 +533,43 @@ class BioprocessModel:
|
|
407 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
408 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
409 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
410 |
-
else:
|
|
|
|
|
411 |
|
412 |
fig,(ax1,ax2,ax3)=plt.subplots(3,1,figsize=(10,15),sharex=True)
|
413 |
-
title_suffix = " - EDO" if X_ode_success else (" - Ajuste Directo" if not use_differential or self.biomass_diff is None else " - EDO (falló, usando ajuste)")
|
414 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()}){title_suffix}',fontsize=16)
|
415 |
|
416 |
plot_cfg=[(ax1,biomass,y_pred_b,biomass_std,axis_labels['biomass_label'],'Modelo Biomasa',self.params.get('biomass',{}),self.r2.get('biomass',np.nan),self.rmse.get('biomass',np.nan)),
|
417 |
(ax2,substrate,y_pred_s,substrate_std,axis_labels['substrate_label'],'Modelo Sustrato',self.params.get('substrate',{}),self.r2.get('substrate',np.nan),self.rmse.get('substrate',np.nan)),
|
418 |
(ax3,product,y_pred_p,product_std,axis_labels['product_label'],'Modelo Producto',self.params.get('product',{}),self.r2.get('product',np.nan),self.rmse.get('product',np.nan))]
|
419 |
|
420 |
-
for i,(ax,data,y_pred_curve,std,ylab,leg_lab,p_dict,
|
|
|
421 |
if data is not None and len(data)>0 and not np.all(np.isnan(data)):
|
422 |
if show_error_bars and std is not None and len(std)==len(data) and not np.all(np.isnan(std)):
|
423 |
ax.errorbar(time,data,yerr=std,fmt=marker_style,color=point_color,label='Datos experimentales',capsize=error_cap_size,elinewidth=error_line_width,markeredgewidth=1,markersize=5)
|
424 |
else: ax.plot(time,data,marker=marker_style,linestyle='',color=point_color,label='Datos experimentales',markersize=5)
|
425 |
-
else: ax.text(0.5,0.5,'No hay datos experimentales.',transform=ax.transAxes,ha='center',va='center',color='gray')
|
426 |
|
|
|
427 |
if y_pred_curve is not None and len(y_pred_curve)>0 and not np.all(np.isnan(y_pred_curve)):
|
428 |
ax.plot(time_curves,y_pred_curve,linestyle=line_style,color=line_color,label=leg_lab)
|
429 |
-
elif i == 0 and y_pred_biomass_fit is None:
|
430 |
ax.text(0.5, 0.6, 'Modelo de biomasa no ajustado.', transform=ax.transAxes, ha='center', va='center', color='red', fontsize=9)
|
431 |
-
elif i > 0 and (y_pred_biomass_fit is None or not self.params.get('biomass')):
|
432 |
ax.text(0.5, 0.4, 'No ajustado (depende de biomasa).', transform=ax.transAxes, ha='center', va='center', color='orange', fontsize=9)
|
433 |
-
elif y_pred_curve is None or np.all(np.isnan(y_pred_curve)):
|
434 |
ax.text(0.5, 0.4, 'Modelo no ajustado o resultado inválido.', transform=ax.transAxes, ha='center', va='center', color='orange', fontsize=9)
|
435 |
|
436 |
ax.set_ylabel(ylab); ax.set_title(ylab)
|
437 |
if show_legend: ax.legend(loc=legend_position)
|
438 |
if show_params and p_dict and any(np.isfinite(v) for v in p_dict.values()):
|
439 |
p_txt='\n'.join([f"{k}={v:.3g}" if np.isfinite(v) else f"{k}=N/A" for k,v in p_dict.items()])
|
440 |
-
txt=f"{p_txt}\nR²={
|
441 |
if params_position=='outside right':
|
442 |
-
fig.subplots_adjust(right=0.70)
|
443 |
ax.annotate(txt,xy=(1.05,0.5),xycoords='axes fraction',xytext=(10,0),textcoords='offset points',va='center',ha='left',bbox={'boxstyle':'round,pad=0.3','facecolor':'wheat','alpha':0.7}, fontsize=8)
|
444 |
else:
|
445 |
tx,ha=(0.95,'right') if 'right' in params_position else (0.05,'left')
|
@@ -449,7 +579,7 @@ class BioprocessModel:
|
|
449 |
|
450 |
ax3.set_xlabel(axis_labels['x_label'])
|
451 |
plt.tight_layout(rect=[0,0.03,1,0.95]);
|
452 |
-
if params_position == 'outside right': fig.subplots_adjust(right=0.70)
|
453 |
|
454 |
buf=io.BytesIO(); fig.savefig(buf,format='png',bbox_inches='tight'); buf.seek(0)
|
455 |
image=Image.open(buf).convert("RGB"); plt.close(fig); return image
|
@@ -459,7 +589,7 @@ class BioprocessModel:
|
|
459 |
biomass_std=None, substrate_std=None, product_std=None,
|
460 |
experiment_name='', legend_position='best', params_position='upper right',
|
461 |
show_legend=True, show_params=True, style='whitegrid',
|
462 |
-
line_color='#0072B2', point_color='#D55E00', line_style='-', marker_style='o',
|
463 |
use_differential=False, axis_labels=None,
|
464 |
show_error_bars=True, error_cap_size=3, error_line_width=1):
|
465 |
|
@@ -467,7 +597,10 @@ class BioprocessModel:
|
|
467 |
time_curves = self.generate_fine_time_grid(time)
|
468 |
X_ode_success = False
|
469 |
|
470 |
-
if y_pred_biomass_fit is None and not (use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']):
|
|
|
|
|
|
|
471 |
can_use_ode = use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']
|
472 |
if axis_labels is None: axis_labels = {'x_label':'Tiempo','biomass_label':'Biomasa','substrate_label':'Sustrato','product_label':'Producto'}
|
473 |
sns.set_style(style)
|
@@ -478,7 +611,7 @@ class BioprocessModel:
|
|
478 |
if X_ode_res is not None:
|
479 |
y_pred_b,y_pred_s,y_pred_p,time_curves = X_ode_res,S_ode_res,P_ode_res,time_fine_ode
|
480 |
X_ode_success = True
|
481 |
-
else:
|
482 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
483 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
484 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'): s_params=list(self.params['substrate'].values()); y_pred_s=self.substrate(time_curves,*s_params,b_params)
|
@@ -496,7 +629,7 @@ class BioprocessModel:
|
|
496 |
else: y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
497 |
|
498 |
fig,ax1=plt.subplots(figsize=(12,7))
|
499 |
-
title_suffix = " - EDO" if X_ode_success else (" - Ajuste Directo" if not use_differential or self.biomass_diff is None else " - EDO (falló, usando ajuste)")
|
500 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()}){title_suffix}',fontsize=16)
|
501 |
|
502 |
colors = sns.color_palette("tab10", 3)
|
@@ -534,17 +667,17 @@ class BioprocessModel:
|
|
534 |
|
535 |
if show_params:
|
536 |
all_param_text = []
|
537 |
-
for cat_label, p_dict,
|
538 |
(axis_labels['biomass_label'], self.params.get('biomass',{}), self.r2.get('biomass',np.nan), self.rmse.get('biomass',np.nan)),
|
539 |
(axis_labels['substrate_label'], self.params.get('substrate',{}), self.r2.get('substrate',np.nan), self.rmse.get('substrate',np.nan)),
|
540 |
(axis_labels['product_label'], self.params.get('product',{}), self.r2.get('product',np.nan), self.rmse.get('product',np.nan))]:
|
541 |
if p_dict and any(np.isfinite(v) for v in p_dict.values()):
|
542 |
p_list = [f" {k}={v:.3g}" if np.isfinite(v) else f" {k}=N/A" for k,v in p_dict.items()]
|
543 |
-
all_param_text.append(f"{cat_label}:\n" + "\n".join(p_list) + f"\n R²={
|
544 |
total_text = "\n\n".join(all_param_text)
|
545 |
if total_text:
|
546 |
if params_position=='outside right':
|
547 |
-
fig.subplots_adjust(right=0.65)
|
548 |
fig.text(0.67,0.5,total_text,transform=fig.transFigure,va='center',ha='left',bbox=dict(boxstyle='round,pad=0.3',facecolor='wheat',alpha=0.7),fontsize=7)
|
549 |
else:
|
550 |
tx,ha=(0.95,'right') if 'right' in params_position else (0.05,'left')
|
@@ -557,9 +690,10 @@ class BioprocessModel:
|
|
557 |
buf=io.BytesIO(); fig.savefig(buf,format='png',bbox_inches='tight'); buf.seek(0)
|
558 |
image=Image.open(buf).convert("RGB"); plt.close(fig); return image
|
559 |
|
|
|
560 |
def sanitize_filename(name, max_length=100):
|
561 |
-
name = str(name)
|
562 |
-
name = re.sub(r'[^\w\s
|
563 |
name = re.sub(r'[-\s]+', '_', name)
|
564 |
return name[:max_length]
|
565 |
|
@@ -586,7 +720,7 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
586 |
for sheet_name_idx, sheet_name in enumerate(sheet_names):
|
587 |
current_sheet_name_base = (experiment_names_list[sheet_name_idx]
|
588 |
if sheet_name_idx < len(experiment_names_list) and experiment_names_list[sheet_name_idx]
|
589 |
-
else f"Hoja_{sanitize_filename(sheet_name, 15)}")
|
590 |
try:
|
591 |
df = pd.read_excel(xls, sheet_name=sheet_name, header=[0, 1])
|
592 |
if df.empty: all_plot_messages.append(f"Hoja '{sheet_name}' vacía."); continue
|
@@ -602,70 +736,76 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
602 |
if mode == 'independent':
|
603 |
unique_exp_groups = df.columns.get_level_values(0).unique()
|
604 |
for exp_group_idx, exp_group_name in enumerate(unique_exp_groups):
|
605 |
-
# Sanitize exp_group_name for use in experiment identifier
|
606 |
sanitized_exp_group_name = sanitize_filename(exp_group_name, 20)
|
607 |
current_experiment_name = f"{current_sheet_name_base}_{sanitized_exp_group_name}"
|
608 |
|
609 |
exp_df_slice_multi = df[exp_group_name]
|
610 |
try:
|
611 |
-
|
612 |
-
|
613 |
-
|
614 |
-
|
615 |
-
all_plot_messages.append(f"No se encontró 'Tiempo' en {current_experiment_name}"); continue
|
616 |
|
617 |
-
|
618 |
-
if isinstance(
|
619 |
-
|
620 |
-
else:
|
621 |
-
|
622 |
-
|
623 |
-
|
624 |
-
|
625 |
-
|
626 |
-
|
627 |
-
|
628 |
-
|
629 |
-
|
|
|
|
|
|
|
|
|
630 |
return np.array([]),None
|
631 |
|
632 |
biomass_exp,biomass_std_exp=get_comp_data_independent('Biomasa')
|
633 |
substrate_exp,substrate_std_exp=get_comp_data_independent('Sustrato')
|
634 |
product_exp,product_std_exp=get_comp_data_independent('Producto')
|
635 |
|
|
|
636 |
min_len=len(time_exp)
|
637 |
if len(biomass_exp)>0:min_len=min(min_len,len(biomass_exp))
|
638 |
-
else: all_plot_messages.append(f"Sin datos de biomasa para {current_experiment_name}."); # continue or allow processing without biomass
|
639 |
if len(substrate_exp)>0:min_len=min(min_len,len(substrate_exp))
|
640 |
if len(product_exp)>0:min_len=min(min_len,len(product_exp))
|
641 |
|
642 |
time_exp=time_exp[:min_len]
|
643 |
if len(biomass_exp)>0:biomass_exp=biomass_exp[:min_len]
|
|
|
644 |
if biomass_std_exp is not None and len(biomass_std_exp)>0:biomass_std_exp=biomass_std_exp[:min_len]
|
|
|
645 |
if len(substrate_exp)>0:substrate_exp=substrate_exp[:min_len]
|
|
|
646 |
if substrate_std_exp is not None and len(substrate_std_exp)>0:substrate_std_exp=substrate_std_exp[:min_len]
|
|
|
647 |
if len(product_exp)>0:product_exp=product_exp[:min_len]
|
|
|
648 |
if product_std_exp is not None and len(product_std_exp)>0:product_std_exp=product_std_exp[:min_len]
|
649 |
|
650 |
-
|
651 |
if len(time_exp)==0: all_plot_messages.append(f"Sin datos de tiempo para {current_experiment_name}."); continue
|
652 |
-
if len(biomass_exp)==0:
|
653 |
all_plot_messages.append(f"Sin datos de biomasa para {current_experiment_name}, no se puede ajustar el modelo de biomasa.")
|
654 |
for mt_ in model_types_selected: comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
655 |
continue
|
656 |
-
|
657 |
except KeyError as e_key: all_plot_messages.append(f"Falta columna {e_key} en '{current_experiment_name}'."); continue
|
658 |
except Exception as e_data: all_plot_messages.append(f"Error procesando datos para '{current_experiment_name}': {e_data}."); continue
|
659 |
|
660 |
for model_type_iter in model_types_selected:
|
661 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
662 |
model_instance.fit_model()
|
663 |
-
y_pred_biomass = model_instance.fit_biomass(time_exp, biomass_exp)
|
664 |
y_pred_substrate, y_pred_product = None, None
|
665 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
666 |
if len(substrate_exp)>0: y_pred_substrate = model_instance.fit_substrate(time_exp, substrate_exp, model_instance.params['biomass'])
|
667 |
if len(product_exp)>0: y_pred_product = model_instance.fit_product(time_exp, product_exp, model_instance.params['biomass'])
|
668 |
-
else:
|
|
|
669 |
|
670 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
671 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
@@ -688,11 +828,24 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
688 |
substrate_std_avg = model_dummy_for_sheet.datas_std[-1] if model_dummy_for_sheet.datas_std and model_dummy_for_sheet.datas_std[-1] is not None and len(model_dummy_for_sheet.datas_std[-1]) == len(substrate_avg) else None
|
689 |
product_std_avg = model_dummy_for_sheet.datap_std[-1] if model_dummy_for_sheet.datap_std and model_dummy_for_sheet.datap_std[-1] is not None and len(model_dummy_for_sheet.datap_std[-1]) == len(product_avg) else None
|
690 |
|
691 |
-
if time_avg is None or len(time_avg)==0:
|
|
|
|
|
|
|
|
|
692 |
if len(biomass_avg)==0:
|
693 |
-
all_plot_messages.append(f"Sin datos de biomasa promedio para '{current_sheet_name_base}'.")
|
694 |
for mt_ in model_types_selected: comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
695 |
-
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
696 |
|
697 |
for model_type_iter in model_types_selected:
|
698 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
@@ -702,7 +855,8 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
702 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
703 |
if len(substrate_avg)>0: y_pred_substrate = model_instance.fit_substrate(time_avg, substrate_avg, model_instance.params['biomass'])
|
704 |
if len(product_avg)>0: y_pred_product = model_instance.fit_product(time_avg, product_avg, model_instance.params['biomass'])
|
705 |
-
else:
|
|
|
706 |
|
707 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
708 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
@@ -721,17 +875,21 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
721 |
cols_to_sort = ['R² Biomasa', 'R² Sustrato', 'R² Producto', 'RMSE Biomasa', 'RMSE Sustrato', 'RMSE Producto']
|
722 |
existing_cols_to_sort = [col for col in cols_to_sort if col in comparison_df.columns]
|
723 |
ascending_map = {'R²': False, 'RMSE': True}
|
724 |
-
sort_ascending = [True, True] + [ascending_map[col.split(' ')[0]] for col in existing_cols_to_sort]
|
725 |
comparison_df_sorted = comparison_df.sort_values(by=['Experimento','Modelo']+existing_cols_to_sort,ascending=sort_ascending).reset_index(drop=True)
|
726 |
else: comparison_df_sorted = pd.DataFrame(columns=['Experimento','Modelo','R² Biomasa','RMSE Biomasa','R² Sustrato','RMSE Sustrato','R² Producto','RMSE Producto'])
|
727 |
|
728 |
final_message = "Procesamiento completado."
|
729 |
-
if all_plot_messages: final_message += " Mensajes:\n" + "\n".join(list(set(all_plot_messages)))
|
730 |
if not figures_with_names and not comparison_df_sorted.empty: final_message += "\nNo se generaron gráficos, pero hay datos en la tabla."
|
731 |
-
elif not figures_with_names and comparison_df_sorted.empty: final_message += "\nNo se generaron gráficos ni datos para la tabla."
|
|
|
732 |
|
733 |
return figures_with_names, comparison_df_sorted, final_message, all_parameters_collected
|
734 |
|
|
|
|
|
|
|
735 |
|
736 |
MODEL_CHOICES = [("Logistic (3-parám)","logistic"),("Gompertz (3-parám)","gompertz"),("Moser (3-parám)","moser"),("Baranyi (4-parám)","baranyi")]
|
737 |
|
@@ -744,73 +902,40 @@ def create_zip_of_images(figures_with_names_list, base_zip_filename="plots"):
|
|
744 |
for item_idx, item in enumerate(figures_with_names_list):
|
745 |
img_pil = item['image']
|
746 |
img_name_suggestion = item['name']
|
747 |
-
|
748 |
-
# Sanitize and ensure uniqueness for filename within zip
|
749 |
base_name, ext = os.path.splitext(img_name_suggestion)
|
750 |
-
if not ext: ext = ".png"
|
751 |
-
|
752 |
-
sanitized_base = sanitize_filename(base_name, max_length=80) # Shorter max for base before adding index
|
753 |
img_name_in_zip = f"{sanitized_base}{ext}"
|
754 |
-
|
755 |
-
# Ensure unique name within zip, in case sanitization leads to clashes
|
756 |
count = 1
|
757 |
original_sanitized_base = sanitized_base
|
758 |
while img_name_in_zip in zf.namelist():
|
759 |
img_name_in_zip = f"{original_sanitized_base}_{count}{ext}"
|
760 |
count += 1
|
761 |
-
if count > len(figures_with_names_list) + 5:
|
762 |
-
img_name_in_zip = f"{original_sanitized_base}_{item_idx}_{count}{ext}"
|
763 |
break
|
764 |
-
|
765 |
-
|
766 |
img_byte_arr = io.BytesIO()
|
767 |
img_pil.save(img_byte_arr, format='PNG')
|
768 |
img_byte_arr.seek(0)
|
769 |
zf.writestr(img_name_in_zip, img_byte_arr.getvalue())
|
770 |
-
|
771 |
zip_buffer.seek(0)
|
772 |
-
|
773 |
try:
|
774 |
-
# Gradio's gr.File component handles the temporary file lifecycle if given bytes or a BytesIO object directly.
|
775 |
-
# However, to ensure it has a .zip extension for download, creating a named temp file is safer.
|
776 |
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip_file:
|
777 |
tmp_zip_file.write(zip_buffer.getvalue())
|
778 |
return tmp_zip_file.name, "ZIP con imágenes generado exitosamente."
|
779 |
except Exception as e:
|
780 |
return None, f"Error creando archivo ZIP temporal: {str(e)}"
|
781 |
|
782 |
-
|
783 |
def create_interface():
|
784 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
785 |
gr.Markdown("# Modelos Cinéticos de Bioprocesos")
|
786 |
-
|
787 |
with gr.Tab("Teoría y Uso"):
|
788 |
gr.Markdown(r"""
|
789 |
-
Análisis y visualización de datos de bioprocesos
|
790 |
-
y el modelo de Luedeking-Piret para el consumo de sustrato y la formación de producto.
|
791 |
-
Nuevos modelos como Baranyi (4 parámetros) han sido añadidos.
|
792 |
-
|
793 |
-
**Instrucciones de Uso:**
|
794 |
-
1. **Subir archivo Excel (.xlsx):**
|
795 |
-
El archivo debe tener una estructura de MultiIndex en las columnas:
|
796 |
-
- Nivel 0: Nombre del experimento/tratamiento (ej: "Control", "Tratamiento A").
|
797 |
-
- Nivel 1: Tipo de dato ("Tiempo", "Biomasa", "Sustrato", "Producto").
|
798 |
-
- Nivel 2 (Opcional): Identificador de réplica (ej: "R1", "R2"). Si se omite, se asume una sola serie.
|
799 |
-
2. **Seleccionar Modelo(s) de Biomasa.**
|
800 |
-
3. **Elegir Modo de Análisis.**
|
801 |
-
4. **Configurar Opciones.**
|
802 |
-
5. **Ejecutar:** Haz clic en "Simular y Graficar".
|
803 |
-
6. **Resultados:** Visualiza los gráficos y la tabla. Exporta la tabla (Excel/CSV), los parámetros del modelo (Excel) o todas las imágenes generadas (ZIP).
|
804 |
""")
|
805 |
gr.Markdown(r"""
|
806 |
-
## Modelos Matemáticos para Bioprocesos
|
807 |
-
**1. Modelo Logístico (3p):** $ X(t) = \frac{X_0 X_m e^{\mu_m t}}{X_m - X_0 + X_0 e^{\mu_m t}} $
|
808 |
-
**2. Modelo Gompertz (3p):** $ X(t) = X_m \exp\left(-\exp\left(\frac{\mu_m e}{X_m}(\lambda-t)+1\right)\right) $
|
809 |
-
**3. Modelo de Moser (simplificado, 3p):** $ X(t)=X_m(1-e^{-\mu_m(t-K_s)}) $
|
810 |
-
**4. Modelo de Baranyi (4p):** $ \ln X(t) = \ln X_0 + \mu_m A(t) - \ln\left(1 + \frac{e^{\mu_m A(t)}-1}{X_m/X_0}\right) $
|
811 |
-
**Luedeking-Piret (Sustrato y Producto):** $ \frac{dS}{dt} = -p \frac{dX}{dt} - q X \quad ; \quad \frac{dP}{dt} = \alpha \frac{dX}{dt} + \beta X $
|
812 |
""")
|
813 |
-
|
814 |
with gr.Tab("Simulación"):
|
815 |
with gr.Row():
|
816 |
file_input = gr.File(label="Subir archivo Excel (.xlsx)", file_types=['.xlsx'])
|
@@ -845,29 +970,26 @@ def create_interface():
|
|
845 |
with gr.Accordion("Configuración Avanzada de Ajuste (No implementado aún)", open=False):
|
846 |
lower_bounds_str_ui = gr.Textbox(label="Lower Bounds (JSON, no usado)", lines=3)
|
847 |
upper_bounds_str_ui = gr.Textbox(label="Upper Bounds (JSON, no usado)", lines=3)
|
848 |
-
|
849 |
simulate_btn = gr.Button("Simular y Graficar", variant="primary")
|
850 |
|
851 |
with gr.Tab("Resultados"):
|
852 |
-
status_message_ui = gr.Textbox(label="Estado del Procesamiento", interactive=False, lines=
|
853 |
output_gallery_ui = gr.Gallery(label="Resultados Gráficos", columns=[2,1], height=600, object_fit="contain", preview=True)
|
854 |
-
output_table_ui = gr.Dataframe(
|
855 |
label="Tabla Comparativa de Modelos",
|
856 |
headers=["Experimento","Modelo","R² Biomasa","RMSE Biomasa","R² Sustrato","RMSE Sustrato","R² Producto","RMSE Producto"],
|
857 |
interactive=False,
|
858 |
wrap=True
|
|
|
859 |
)
|
860 |
-
|
861 |
state_df_ui = gr.State(pd.DataFrame())
|
862 |
state_params_ui = gr.State({})
|
863 |
state_figures_ui = gr.State([])
|
864 |
-
|
865 |
with gr.Row():
|
866 |
export_excel_btn = gr.Button("Exportar Tabla a Excel")
|
867 |
export_csv_btn = gr.Button("Exportar Tabla a CSV")
|
868 |
export_params_btn = gr.Button("Exportar Parámetros a Excel")
|
869 |
export_images_zip_btn = gr.Button("Descargar Imágenes (ZIP)")
|
870 |
-
|
871 |
download_file_output_ui = gr.File(label="Descargar Tabla/Parámetros", interactive=False)
|
872 |
download_zip_images_ui = gr.File(label="Descargar ZIP de Imágenes", interactive=False)
|
873 |
|
@@ -880,16 +1002,13 @@ def create_interface():
|
|
880 |
if file is None: return [], pd.DataFrame(), "Error: Sube un archivo Excel.", pd.DataFrame(), {}, []
|
881 |
axis_labels = {'x_label':x_label or 'Tiempo','biomass_label':biomass_label or 'Biomasa','substrate_label':substrate_label or 'Sustrato','product_label':product_label or 'Producto'}
|
882 |
if not models_sel: return [], pd.DataFrame(), "Error: Selecciona un modelo.", pd.DataFrame(), {}, []
|
883 |
-
|
884 |
figures_with_names, comparison_df, message, collected_params = process_all_data(
|
885 |
file, legend_pos, params_pos, models_sel, exp_names, low_bounds_str, up_bounds_str,
|
886 |
analysis_mode, plot_style, line_col, point_col, line_sty, marker_sty,
|
887 |
show_leg, show_par, use_diff, int(maxfev), axis_labels,
|
888 |
-
show_error_bars_arg, error_cap_size_arg, error_line_width_arg
|
889 |
-
)
|
890 |
pil_images_for_gallery = [item['image'] for item in figures_with_names] if figures_with_names else []
|
891 |
return pil_images_for_gallery, comparison_df, message, comparison_df, collected_params, figures_with_names
|
892 |
-
|
893 |
simulate_btn.click(
|
894 |
fn=run_simulation_interface,
|
895 |
inputs=[file_input, legend_position_ui, params_position_ui, model_types_selected_ui, mode, experiment_names_str_ui,
|
@@ -897,15 +1016,14 @@ def create_interface():
|
|
897 |
line_style_dropdown_ui, marker_style_dropdown_ui, show_legend_ui, show_params_ui, use_differential_ui,
|
898 |
maxfev_input_ui, x_axis_label_input_ui, biomass_axis_label_input_ui, substrate_axis_label_input_ui,
|
899 |
product_axis_label_input_ui, show_error_bars_ui, error_cap_size_ui, error_line_width_ui],
|
900 |
-
outputs=[output_gallery_ui, output_table_ui, status_message_ui, state_df_ui, state_params_ui, state_figures_ui]
|
901 |
-
)
|
902 |
|
903 |
def export_df_to_file(df_to_export, file_format="excel"):
|
904 |
if df_to_export is None or df_to_export.empty:
|
905 |
with tempfile.NamedTemporaryFile(suffix=".txt",delete=False,mode="w",encoding="utf-8") as tmp: tmp.write("No hay datos en la tabla para exportar."); return tmp.name,"No hay datos para exportar."
|
906 |
try:
|
907 |
suffix=".xlsx" if file_format=="excel" else ".csv"
|
908 |
-
with tempfile.NamedTemporaryFile(suffix=suffix,delete=False,mode='w+b' if file_format=="excel" else 'w', encoding=None if file_format=="excel" else "utf-8-sig") as tmp_f:
|
909 |
if file_format=="excel": df_to_export.to_excel(tmp_f.name,index=False)
|
910 |
else: df_to_export.to_csv(tmp_f.name,index=False)
|
911 |
return tmp_f.name,f"Tabla exportada a {suffix[1:]} exitosamente."
|
@@ -923,10 +1041,10 @@ def create_interface():
|
|
923 |
for exp_name,models_data in params_state_dict.items():
|
924 |
for model_type_name,all_params_for_model_type in models_data.items():
|
925 |
for param_category,category_params in all_params_for_model_type.items():
|
926 |
-
if category_params and isinstance(category_params,dict) and category_params:
|
927 |
df_params=pd.DataFrame({'Parámetro':list(category_params.keys()),'Valor':list(category_params.values())})
|
928 |
s_exp=sanitize_filename(exp_name,15); s_mod=sanitize_filename(model_type_name,10); s_cat=sanitize_filename(param_category,4)
|
929 |
-
sheet_name=f"{s_exp}_{s_mod}_{s_cat}"[:31]
|
930 |
orig_sn,c=sheet_name,1
|
931 |
while sheet_name in writer.sheets: sheet_name=f"{orig_sn[:28]}_{c}"[:31]; c+=1;
|
932 |
df_params.to_excel(writer,sheet_name=sheet_name,index=False)
|
@@ -950,7 +1068,5 @@ def create_interface():
|
|
950 |
return demo
|
951 |
|
952 |
if __name__ == '__main__':
|
953 |
-
# Ensure Gradio is installed, or run: pip install gradio pandas matplotlib seaborn scikit-learn openpyxl Pillow
|
954 |
-
# The os.system call for pip install is generally not recommended within scripts for production/distribution.
|
955 |
demo_instance = create_interface()
|
956 |
-
demo_instance.launch(share=False, debug=True)
|
|
|
1 |
import os
|
2 |
+
# os.system("pip install --upgrade gradio") # Best to manage dependencies externally
|
|
|
|
|
3 |
|
4 |
from pydantic import BaseModel
|
5 |
import numpy as np
|
|
|
25 |
self.params = {}
|
26 |
self.r2 = {}
|
27 |
self.rmse = {}
|
28 |
+
self.datax = [] # Stores list of 2D numpy arrays (replicates per experiment/sheet)
|
29 |
self.datas = []
|
30 |
self.datap = []
|
31 |
+
self.dataxp = [] # Stores list of 1D numpy arrays (averaged data per experiment/sheet)
|
32 |
self.datasp = []
|
33 |
self.datapp = []
|
34 |
+
self.datax_std = [] # Stores list of 1D numpy arrays (std dev of data per experiment/sheet)
|
35 |
self.datas_std = []
|
36 |
self.datap_std = []
|
37 |
self.biomass_model = None
|
38 |
self.biomass_diff = None
|
39 |
self.model_type = model_type
|
40 |
self.maxfev = maxfev
|
41 |
+
self.time = None # Stores the final time vector for the current processing context (sheet)
|
42 |
|
43 |
@staticmethod
|
44 |
def logistic(time, xo, xm, um):
|
45 |
+
if xm <= 0 or xo <= 0 or um <=0 or xm <= xo : # Added xm <= xo check
|
46 |
+
# xm must be > xo for growth. If xm=xo, implies X(t)=xo, which means um=0.
|
47 |
+
# If um > 0 and xm=xo, the denominator can be zero if xo*exp(um*t) term dominates.
|
48 |
return np.full_like(time, np.nan)
|
49 |
term_exp = np.exp(um * time)
|
50 |
denominator = (xm - xo + xo * term_exp)
|
51 |
+
denominator = np.where(denominator == 0, 1e-9, denominator) # Avoid division by zero
|
52 |
return (xo * xm * term_exp) / denominator
|
53 |
|
54 |
+
|
55 |
@staticmethod
|
56 |
def gompertz(time, xm, um, lag):
|
57 |
+
if xm <= 0 or um <=0 : # lag can be 0 or positive
|
58 |
return np.full_like(time, np.nan)
|
59 |
exp_term = (um * np.e / xm) * (lag - time) + 1
|
60 |
+
exp_term_clipped = np.clip(exp_term, -np.inf, 700) # Avoid overflow in np.exp(np.exp())
|
61 |
return xm * np.exp(-np.exp(exp_term_clipped))
|
62 |
|
63 |
@staticmethod
|
64 |
def moser(time, Xm, um, Ks):
|
65 |
if Xm <=0 or um <=0: return np.full_like(time, np.nan)
|
66 |
+
# Ks can be negative, allowing for an initial lag or even growth before t=0 if extrapolated
|
67 |
return Xm * (1 - np.exp(-um * (time - Ks)))
|
68 |
|
69 |
@staticmethod
|
70 |
def baranyi(time, X0, Xm, um, lag):
|
71 |
+
if X0 <= 0 or Xm <= X0 or um <= 0 or lag < 0: # lag should be non-negative, Xm > X0
|
72 |
return np.full_like(time, np.nan)
|
73 |
+
|
74 |
log_arg_A = np.exp(-um * time) + np.exp(-um * lag) - np.exp(-um * (time + lag))
|
75 |
+
log_arg_A = np.where(log_arg_A <= 1e-9, 1e-9, log_arg_A) # Prevent log(0 or negative)
|
76 |
A_t = time + (1 / um) * np.log(log_arg_A)
|
77 |
+
|
78 |
exp_um_At = np.exp(um * A_t)
|
79 |
+
exp_um_At_clipped = np.clip(exp_um_At, -np.inf, 700) # Clamp large values
|
80 |
+
|
81 |
numerator = Xm * exp_um_At_clipped
|
82 |
denominator = (Xm / X0 - 1) + exp_um_At_clipped
|
83 |
+
denominator = np.where(denominator == 0, 1e-9, denominator) # Avoid division by zero
|
84 |
+
|
85 |
return numerator / denominator
|
86 |
|
87 |
@staticmethod
|
88 |
def logistic_diff(X, t, params):
|
89 |
+
# params for logistic_diff: [xo_initial_condition, xm, um]
|
90 |
+
# xo is not used in diff eq itself, but xm and um are parameters of the DE.
|
91 |
_, xm, um = params
|
92 |
if xm == 0: return 0
|
93 |
return um * X * (1 - X / xm)
|
94 |
|
95 |
@staticmethod
|
96 |
def gompertz_diff(X, t, params):
|
97 |
+
# params for gompertz_diff: [xm, um, lag]
|
98 |
xm, um, lag = params
|
99 |
+
if xm == 0 or X <= 1e-9 : return 0 # Avoid log(0) or division by zero if Xm or X is zero
|
100 |
k_val = um * np.e / xm
|
101 |
u_val = k_val * (lag - t) + 1
|
102 |
+
u_val_clipped = np.clip(u_val, -np.inf, 700) # Avoid overflow in exp
|
103 |
return X * k_val * np.exp(u_val_clipped)
|
104 |
|
105 |
@staticmethod
|
106 |
def moser_diff(X, t, params):
|
107 |
+
# params for moser_diff: [Xm, um, Ks]
|
108 |
+
Xm, um, _ = params # Ks is not directly in this simplified dX/dt
|
109 |
return um * (Xm - X)
|
110 |
|
111 |
def substrate(self, time, so, p, q, biomass_params_list):
|
|
|
113 |
return np.full_like(time, np.nan)
|
114 |
X_t = self.biomass_model(time, *biomass_params_list)
|
115 |
if np.any(np.isnan(X_t)): return np.full_like(time, np.nan)
|
116 |
+
|
117 |
integral_X = np.zeros_like(X_t)
|
118 |
if len(time) > 1:
|
119 |
+
# Use trapz for better integral approximation if scipy is available, else simple cumsum
|
120 |
+
try:
|
121 |
+
from scipy.integrate import cumulative_trapezoid
|
122 |
+
if len(time) > 1 and len(X_t) > 1:
|
123 |
+
integral_X[1:] = cumulative_trapezoid(X_t, time, initial=0)
|
124 |
+
else: # Fallback for single point or if lengths mismatch unexpectedly
|
125 |
+
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
126 |
+
integral_X = np.cumsum(X_t * dt)
|
127 |
+
except ImportError:
|
128 |
+
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
129 |
+
integral_X = np.cumsum(X_t * dt)
|
130 |
+
|
131 |
+
|
132 |
+
X0_calc = 0.0
|
133 |
if self.model_type in ['logistic', 'baranyi']: X0_calc = biomass_params_list[0]
|
134 |
+
elif self.model_type == 'gompertz': X0_calc = self.gompertz(0, *biomass_params_list) # X(t=0)
|
135 |
+
elif self.model_type == 'moser': X0_calc = self.moser(0, *biomass_params_list) # X(t=0)
|
136 |
+
else: X0_calc = X_t[0] if len(X_t)>0 else 0.0
|
137 |
+
|
138 |
+
X0 = X0_calc if not np.isnan(X0_calc) else (biomass_params_list[0] if biomass_params_list and len(biomass_params_list)>0 else 0.0)
|
139 |
return so - p * (X_t - X0) - q * integral_X
|
140 |
|
141 |
def product(self, time, po, alpha, beta, biomass_params_list):
|
|
|
143 |
return np.full_like(time, np.nan)
|
144 |
X_t = self.biomass_model(time, *biomass_params_list)
|
145 |
if np.any(np.isnan(X_t)): return np.full_like(time, np.nan)
|
146 |
+
|
147 |
integral_X = np.zeros_like(X_t)
|
148 |
if len(time) > 1:
|
149 |
+
try:
|
150 |
+
from scipy.integrate import cumulative_trapezoid
|
151 |
+
if len(time) > 1 and len(X_t) > 1:
|
152 |
+
integral_X[1:] = cumulative_trapezoid(X_t, time, initial=0)
|
153 |
+
else:
|
154 |
+
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
155 |
+
integral_X = np.cumsum(X_t * dt)
|
156 |
+
except ImportError:
|
157 |
+
dt = np.diff(time, prepend=time[0] - (time[1]-time[0] if len(time)>1 else 1))
|
158 |
+
integral_X = np.cumsum(X_t * dt)
|
159 |
+
|
160 |
+
X0_calc = 0.0
|
161 |
if self.model_type in ['logistic', 'baranyi']: X0_calc = biomass_params_list[0]
|
162 |
elif self.model_type == 'gompertz': X0_calc = self.gompertz(0, *biomass_params_list)
|
163 |
elif self.model_type == 'moser': X0_calc = self.moser(0, *biomass_params_list)
|
164 |
+
else: X0_calc = X_t[0] if len(X_t)>0 else 0.0
|
165 |
+
|
166 |
+
X0 = X0_calc if not np.isnan(X0_calc) else (biomass_params_list[0] if biomass_params_list and len(biomass_params_list)>0 else 0.0)
|
167 |
return po + alpha * (X_t - X0) + beta * integral_X
|
168 |
|
169 |
def process_data(self, df):
|
170 |
+
# Identify columns for Time, Biomass, Substrate, Product based on the second level of MultiIndex
|
171 |
biomass_cols = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Biomasa']
|
172 |
substrate_cols = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Sustrato']
|
173 |
product_cols = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Producto']
|
|
|
174 |
time_col_candidates = [col for col in df.columns if isinstance(col, tuple) and len(col) > 1 and col[1] == 'Tiempo']
|
175 |
+
|
176 |
if not time_col_candidates:
|
177 |
+
raise ValueError("La columna ('*', 'Tiempo', '*') no se encuentra en el DataFrame.")
|
178 |
+
|
179 |
+
# Use the time data from the first identified 'Tiempo' column as the reference time axis for the sheet
|
180 |
+
# This assumes that for 'average' or 'combinado' mode, all data on the sheet can be aligned to this time axis.
|
181 |
+
ref_time_col_tuple = time_col_candidates[0]
|
182 |
+
time_data_series_or_df = df[ref_time_col_tuple]
|
183 |
+
|
184 |
+
if isinstance(time_data_series_or_df, pd.DataFrame): # If 'Tiempo' itself has sub-columns (e.g., replicates of time, unusual)
|
185 |
+
time = time_data_series_or_df.iloc[:,0].dropna().values # Take the first sub-column
|
186 |
+
else: # Standard case: 'Tiempo' is a single series
|
187 |
+
time = time_data_series_or_df.dropna().values
|
188 |
+
|
189 |
+
if len(time) == 0:
|
190 |
+
raise ValueError("El vector de tiempo de referencia está vacío después de procesar NaNs.")
|
191 |
+
|
192 |
+
def process_component(component_cols, data_list_all_reps, data_list_avg, data_list_std, ref_time_vector):
|
193 |
+
if not component_cols: # No columns for this component
|
194 |
+
data_list_all_reps.append(np.array([])); data_list_avg.append(np.array([])); data_list_std.append(np.array([]))
|
195 |
+
return
|
196 |
+
|
197 |
+
all_component_series = []
|
198 |
+
for col_tuple in component_cols:
|
199 |
+
series = df[col_tuple].dropna()
|
200 |
+
if not series.empty:
|
201 |
+
# Interpolate each series to the reference time vector
|
202 |
+
# This handles cases where individual replicates have different time points or lengths
|
203 |
+
# We need to ensure that the series' index is compatible with ref_time_vector for interpolation
|
204 |
+
# Assuming series.index are time points corresponding to series.values
|
205 |
+
|
206 |
+
# For simplicity, if direct time mapping isn't available per replicate,
|
207 |
+
# we'll rely on aligning by length after dropna, as done previously.
|
208 |
+
# A more robust approach would involve having time columns for each replicate.
|
209 |
+
# Current structure: ('ExpA', 'Biomass', 'R1'), ('ExpA', 'Time', 'R1_Time') - this is not assumed by current code.
|
210 |
+
# It assumes ('ExpA', 'Time', 'SomeTimeName') is common or first one is taken.
|
211 |
+
all_component_series.append(series.values)
|
212 |
+
|
213 |
+
if not all_component_series: # All series were empty after dropna
|
214 |
+
data_list_all_reps.append(np.array([])); data_list_avg.append(np.array([])); data_list_std.append(np.array([]))
|
215 |
+
return
|
216 |
+
|
217 |
+
# Align all collected series to the minimum length among them
|
218 |
+
min_len = min(len(s) for s in all_component_series)
|
219 |
+
aligned_series_np = np.array([s[:min_len] for s in all_component_series])
|
220 |
+
|
221 |
+
data_list_all_reps.append(aligned_series_np) # Store all (aligned) replicates
|
222 |
+
data_list_avg.append(np.mean(aligned_series_np, axis=0))
|
223 |
+
data_list_std.append(np.std(aligned_series_np, axis=0, ddof=1 if aligned_series_np.shape[0] > 1 else 0))
|
224 |
+
|
225 |
+
process_component(biomass_cols, self.datax, self.dataxp, self.datax_std, time)
|
226 |
+
process_component(substrate_cols, self.datas, self.datasp, self.datas_std, time)
|
227 |
+
process_component(product_cols, self.datap, self.datapp, self.datap_std, time)
|
228 |
+
|
229 |
+
# Final alignment of averaged data (xp, sp, pp) with the reference time vector
|
230 |
+
# All averaged component data should now be conformable in length due to the alignment within process_component.
|
231 |
+
# Now, ensure they are all truncated to the length of the shortest *averaged* component series or the ref_time.
|
232 |
+
|
233 |
+
current_min_len = len(time)
|
234 |
+
if self.dataxp and len(self.dataxp[-1]) > 0: current_min_len = min(current_min_len, len(self.dataxp[-1]))
|
235 |
+
else: self.dataxp[-1] = np.array([]) # Ensure it's an empty array if no data
|
236 |
+
|
237 |
+
if self.datasp and len(self.datasp[-1]) > 0: current_min_len = min(current_min_len, len(self.datasp[-1]))
|
238 |
+
else: self.datasp[-1] = np.array([])
|
239 |
+
|
240 |
+
if self.datapp and len(self.datapp[-1]) > 0: current_min_len = min(current_min_len, len(self.datapp[-1]))
|
241 |
+
else: self.datapp[-1] = np.array([])
|
242 |
+
|
243 |
+
self.time = time[:current_min_len]
|
244 |
+
|
245 |
+
if len(self.dataxp[-1]) > current_min_len : self.dataxp[-1] = self.dataxp[-1][:current_min_len]
|
246 |
+
if self.datax_std and self.datax_std[-1] is not None and len(self.datax_std[-1]) > current_min_len: self.datax_std[-1] = self.datax_std[-1][:current_min_len]
|
247 |
+
|
248 |
+
if len(self.datasp[-1]) > current_min_len : self.datasp[-1] = self.datasp[-1][:current_min_len]
|
249 |
+
if self.datas_std and self.datas_std[-1] is not None and len(self.datas_std[-1]) > current_min_len: self.datas_std[-1] = self.datas_std[-1][:current_min_len]
|
250 |
+
|
251 |
+
if len(self.datapp[-1]) > current_min_len : self.datapp[-1] = self.datapp[-1][:current_min_len]
|
252 |
+
if self.datap_std and self.datap_std[-1] is not None and len(self.datap_std[-1]) > current_min_len: self.datap_std[-1] = self.datap_std[-1][:current_min_len]
|
253 |
+
|
254 |
+
|
255 |
+
def fit_model(self): # Sets self.biomass_model and self.biomass_diff
|
256 |
if self.model_type == 'logistic':
|
257 |
self.biomass_model = self.logistic; self.biomass_diff = self.logistic_diff
|
258 |
elif self.model_type == 'gompertz':
|
|
|
260 |
elif self.model_type == 'moser':
|
261 |
self.biomass_model = self.moser; self.biomass_diff = self.moser_diff
|
262 |
elif self.model_type == 'baranyi':
|
263 |
+
self.biomass_model = self.baranyi; self.biomass_diff = None # No ODE for Baranyi here
|
264 |
else: raise ValueError(f"Modelo de biomasa desconocido: {self.model_type}")
|
265 |
|
266 |
def fit_biomass(self, time, biomass):
|
267 |
time = np.asarray(time, dtype=float); biomass = np.asarray(biomass, dtype=float)
|
268 |
+
|
269 |
+
# Ensure time and biomass are 1D and have the same length
|
270 |
+
if time.ndim > 1: time = time.flatten()
|
271 |
+
if biomass.ndim > 1: biomass = biomass.flatten()
|
272 |
+
|
273 |
if len(time) != len(biomass):
|
274 |
+
print(f"Warning: Tiempo ({len(time)}) y biomasa ({len(biomass)}) tienen longitudes diferentes para {self.model_type}. Intentando alinear.")
|
275 |
min_len = min(len(time), len(biomass)); time = time[:min_len]; biomass = biomass[:min_len]
|
276 |
+
if min_len < (4 if self.model_type == 'baranyi' else 3) :
|
277 |
+
print(f"No hay suficientes datos después de alinear para {self.model_type}.")
|
278 |
+
self.r2['biomass'] = np.nan; self.rmse['biomass'] = np.nan; return None
|
279 |
+
|
280 |
+
valid_indices = ~np.isnan(time) & ~np.isnan(biomass) & (biomass > 0) # Ensure biomass is positive for fitting log models
|
281 |
time = time[valid_indices]; biomass = biomass[valid_indices]
|
282 |
+
|
283 |
num_params = 4 if self.model_type == 'baranyi' else 3
|
284 |
if len(time) < num_params:
|
285 |
+
print(f"No hay suficientes datos válidos y positivos ({len(time)}) para {self.model_type} ({num_params} params).")
|
286 |
self.r2['biomass'] = np.nan; self.rmse['biomass'] = np.nan; return None
|
287 |
+
|
288 |
+
if len(biomass) == 0 : # Should be caught by len(time) < num_params if num_params > 0
|
289 |
+
print(f"Datos de biomasa vacíos para {self.model_type}.")
|
290 |
self.r2['biomass'] = np.nan; self.rmse['biomass'] = np.nan; return None
|
291 |
+
|
292 |
+
# The check for biomass[0] <= 1e-9 is implicitly handled by (biomass > 0) in valid_indices
|
293 |
+
# If after that, len(time) is still too short, it's caught above.
|
294 |
+
# If biomass[0] was indeed the issue, it would be filtered out, potentially making the array too short.
|
295 |
+
|
296 |
try:
|
297 |
popt, y_pred = None, None
|
298 |
+
# Common settings for curve_fit
|
299 |
+
fit_kwargs = {'maxfev': self.maxfev, 'ftol': 1e-9, 'xtol': 1e-9, 'method': 'trf'}
|
300 |
+
|
301 |
if self.model_type == 'logistic':
|
302 |
+
xo_g, xm_g, um_g = biomass[0], max(biomass)*1.1, 0.1
|
303 |
+
if xm_g <= xo_g: xm_g = xo_g * 1.5 if xo_g > 0 else 1.0 # Ensure Xm > Xo
|
304 |
+
p0=[xo_g, xm_g, um_g]
|
305 |
+
# Bounds: Xo > 0, Xm > Xo, um > 0
|
306 |
+
bounds=([1e-9, biomass[0]+1e-9, 1e-9],[max(biomass)*2, np.inf, np.inf]) # Looser Xm upper bound
|
307 |
+
p0[0]=np.clip(p0[0],bounds[0][0],bounds[1][0])
|
308 |
+
p0[1]=np.clip(p0[1],max(bounds[0][1],p0[0]+1e-9),bounds[1][1])
|
309 |
+
popt,_=curve_fit(self.logistic,time,biomass,p0=p0,bounds=bounds,**fit_kwargs)
|
310 |
self.params['biomass']={'Xo':popt[0],'Xm':popt[1],'um':popt[2]}; y_pred=self.logistic(time,*popt)
|
311 |
+
|
312 |
elif self.model_type == 'gompertz':
|
313 |
+
xm_g,um_g=max(biomass),0.1
|
314 |
+
# Simplified lag guess: time of first significant increase or first time point
|
315 |
+
lag_idx=np.where(biomass > biomass[0] * 1.1)[0] # 10% increase from initial
|
316 |
lag_g=time[lag_idx[0]] if len(lag_idx)>0 and time[lag_idx[0]]>=0 else (time[0] if len(time)>0 and time[0]>=0 else 0)
|
317 |
+
p0=[xm_g,um_g,lag_g]
|
318 |
+
bounds=([biomass[0], 1e-9, 0],[np.inf, np.inf, max(time)*1.1 if len(time)>0 and max(time)>0 else 100])
|
319 |
+
popt,_=curve_fit(self.gompertz,time,biomass,p0=p0,bounds=bounds,**fit_kwargs)
|
320 |
self.params['biomass']={'Xm':popt[0],'um':popt[1],'lag':popt[2]}; y_pred=self.gompertz(time,*popt)
|
321 |
+
|
322 |
elif self.model_type == 'moser':
|
323 |
+
Xm_g,um_g,Ks_g=max(biomass),0.1,time[0] if len(time)>0 else 0
|
324 |
+
p0=[Xm_g,um_g,Ks_g]
|
325 |
+
bounds=([biomass[0],1e-9, -max(abs(time))*2 if len(time)>0 else -100],[np.inf,np.inf, max(abs(time))*2 if len(time)>0 else 100])
|
326 |
+
popt,_=curve_fit(self.moser,time,biomass,p0=p0,bounds=bounds,**fit_kwargs)
|
327 |
self.params['biomass']={'Xm':popt[0],'um':popt[1],'Ks':popt[2]}; y_pred=self.moser(time,*popt)
|
328 |
+
|
329 |
elif self.model_type == 'baranyi':
|
330 |
+
X0_g,Xm_g,um_g=biomass[0],max(biomass),0.1
|
331 |
+
if Xm_g<=X0_g: Xm_g=X0_g*1.5 if X0_g > 0 else 1.0
|
332 |
+
lag_idx_b=np.where(biomass > X0_g*1.1)[0]
|
333 |
lag_g_b=time[lag_idx_b[0]] if len(lag_idx_b)>0 and time[lag_idx_b[0]]>=0 else (time[0] if len(time)>0 and time[0]>=0 else 0)
|
334 |
+
p0=[X0_g,Xm_g,um_g,lag_g_b]
|
335 |
+
bounds=([1e-9, biomass[0]+1e-9, 1e-9, 0],[max(biomass)*2, np.inf, np.inf, max(time)*1.1 if len(time)>0 and max(time)>0 else 100])
|
336 |
+
p0[0]=np.clip(p0[0],bounds[0][0],bounds[1][0])
|
337 |
+
p0[1]=np.clip(p0[1],max(bounds[0][1],p0[0]+1e-9),bounds[1][1])
|
338 |
+
p0[3]=np.clip(p0[3],bounds[0][3],bounds[1][3])
|
339 |
+
popt,_=curve_fit(self.baranyi,time,biomass,p0=p0,bounds=bounds,**fit_kwargs)
|
340 |
self.params['biomass']={'X0':popt[0],'Xm':popt[1],'um':popt[2],'lag':popt[3]}; y_pred=self.baranyi(time,*popt)
|
341 |
+
else:
|
342 |
+
print(f"Modelo {self.model_type} no implementado para ajuste de biomasa.")
|
343 |
+
return None
|
344 |
+
|
345 |
if y_pred is None or np.any(np.isnan(y_pred)) or np.any(np.isinf(y_pred)):
|
346 |
+
print(f"Predicción de biomasa contiene NaN/Inf para {self.model_type} después del ajuste. Ajuste fallido.")
|
347 |
self.r2['biomass']=np.nan; self.rmse['biomass']=np.nan; self.params['biomass']={}; return None
|
348 |
+
|
349 |
ss_res=np.sum((biomass-y_pred)**2); ss_tot=np.sum((biomass-np.mean(biomass))**2)
|
350 |
+
if ss_tot == 0: self.r2['biomass'] = 1.0 if ss_res < 1e-9 else 0.0
|
351 |
+
else: self.r2['biomass'] = 1 - (ss_res / ss_tot)
|
352 |
self.rmse['biomass']=np.sqrt(mean_squared_error(biomass,y_pred)); return y_pred
|
|
|
|
|
353 |
|
354 |
+
except RuntimeError as e: print(f"RuntimeError en fit_biomass_{self.model_type}: {e}"); self.params['biomass']={}; self.r2['biomass']=np.nan; self.rmse['biomass']=np.nan; return None
|
355 |
+
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
|
356 |
+
|
357 |
+
def fit_substrate(self, time, substrate, biomass_params_dict): # Assumes biomass fit was successful
|
358 |
+
if not biomass_params_dict or not self.params.get('biomass'):
|
359 |
+
print(f"Parámetros de biomasa no disponibles para fit_substrate_{self.model_type}."); self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; return None
|
360 |
+
|
361 |
time=np.asarray(time,dtype=float); substrate=np.asarray(substrate,dtype=float)
|
362 |
valid_idx=~np.isnan(time)&~np.isnan(substrate); time=time[valid_idx]; substrate=substrate[valid_idx]
|
363 |
+
if len(time)<3: print(f"Datos insuficientes para fit_substrate_{self.model_type}."); self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; return None
|
364 |
+
|
365 |
try:
|
366 |
biomass_vals=list(biomass_params_dict.values())
|
367 |
+
p0=[substrate[0] if len(substrate)>0 else 1.0, 0.1, 0.01];
|
368 |
+
bounds=([0, -np.inf, -np.inf],[np.inf, np.inf, np.inf]) # p, q can be negative
|
369 |
+
popt,_=curve_fit(lambda t,so,p,q:self.substrate(t,so,p,q,biomass_vals),time,substrate,p0=p0,maxfev=self.maxfev,bounds=bounds,ftol=1e-9,xtol=1e-9,method='trf')
|
370 |
self.params['substrate']={'so':popt[0],'p':popt[1],'q':popt[2]}; y_pred=self.substrate(time,*popt,biomass_vals)
|
371 |
if np.any(np.isnan(y_pred)) or np.any(np.isinf(y_pred)): self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; self.params['substrate']={}; return None
|
372 |
ss_res=np.sum((substrate-y_pred)**2); ss_tot=np.sum((substrate-np.mean(substrate))**2)
|
|
|
375 |
except RuntimeError as e: print(f"RuntimeError fit_substrate {self.model_type}: {e}"); self.params['substrate']={}; self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; return None
|
376 |
except Exception as e: print(f"Error fit_substrate {self.model_type}: {e}"); self.params['substrate']={}; self.r2['substrate']=np.nan; self.rmse['substrate']=np.nan; return None
|
377 |
|
378 |
+
def fit_product(self, time, product, biomass_params_dict): # Assumes biomass fit was successful
|
379 |
+
if not biomass_params_dict or not self.params.get('biomass'):
|
380 |
+
print(f"Parámetros de biomasa no disponibles para fit_product_{self.model_type}."); self.r2['product']=np.nan; self.rmse['product']=np.nan; return None
|
381 |
+
|
382 |
time=np.asarray(time,dtype=float); product=np.asarray(product,dtype=float)
|
383 |
valid_idx=~np.isnan(time)&~np.isnan(product); time=time[valid_idx]; product=product[valid_idx]
|
384 |
+
if len(time)<3: print(f"Datos insuficientes para fit_product_{self.model_type}."); self.r2['product']=np.nan; self.rmse['product']=np.nan; return None
|
385 |
+
|
386 |
try:
|
387 |
biomass_vals=list(biomass_params_dict.values())
|
388 |
+
p0=[product[0] if len(product)>0 else 0.0, 0.1, 0.01];
|
389 |
+
bounds=([0, -np.inf, -np.inf],[np.inf, np.inf, np.inf]) # alpha, beta can be negative
|
390 |
+
popt,_=curve_fit(lambda t,po,a,b_par:self.product(t,po,a,b_par,biomass_vals),time,product,p0=p0,maxfev=self.maxfev,bounds=bounds,ftol=1e-9,xtol=1e-9,method='trf')
|
391 |
self.params['product']={'po':popt[0],'alpha':popt[1],'beta':popt[2]}; y_pred=self.product(time,*popt,biomass_vals)
|
392 |
if np.any(np.isnan(y_pred)) or np.any(np.isinf(y_pred)): self.r2['product']=np.nan; self.rmse['product']=np.nan; self.params['product']={}; return None
|
393 |
ss_res=np.sum((product-y_pred)**2); ss_tot=np.sum((product-np.mean(product))**2)
|
|
|
396 |
except RuntimeError as e: print(f"RuntimeError fit_product {self.model_type}: {e}"); self.params['product']={}; self.r2['product']=np.nan; self.rmse['product']=np.nan; return None
|
397 |
except Exception as e: print(f"Error fit_product {self.model_type}: {e}"); self.params['product']={}; self.r2['product']=np.nan; self.rmse['product']=np.nan; return None
|
398 |
|
399 |
+
def generate_fine_time_grid(self, time_vector):
|
400 |
+
if time_vector is None or len(time_vector)<2: return np.array([0.0]) if (time_vector is None or len(time_vector)==0) else np.array(time_vector)
|
401 |
+
t_min,t_max=np.min(time_vector),np.max(time_vector)
|
402 |
+
return np.array([t_min]) if t_min==t_max else np.linspace(t_min,t_max,300) # Reduced points for faster plotting
|
403 |
|
404 |
def system(self, y, t, biomass_params_ode, substrate_params, product_params, model_type_ode):
|
405 |
+
X,S,P=y; X=max(X,0); dXdt=0.0 # Ensure X is non-negative
|
406 |
+
|
407 |
if model_type_ode=='logistic': dXdt=self.logistic_diff(X,t,biomass_params_ode)
|
408 |
elif model_type_ode=='gompertz': dXdt=self.gompertz_diff(X,t,biomass_params_ode)
|
409 |
elif model_type_ode=='moser': dXdt=self.moser_diff(X,t,biomass_params_ode)
|
410 |
+
else: dXdt=0.0
|
411 |
+
|
412 |
+
p_val=substrate_params[1] if len(substrate_params)>1 else 0.0
|
413 |
+
q_val=substrate_params[2] if len(substrate_params)>2 else 0.0
|
414 |
+
dSdt=-p_val*dXdt-q_val*X
|
415 |
+
if S <= 1e-9 and dSdt < 0 : dSdt = 0 # Prevent S from becoming negative due to consumption
|
416 |
+
|
417 |
+
alpha_val=product_params[1] if len(product_params)>1 else 0.0
|
418 |
+
beta_val=product_params[2] if len(product_params)>2 else 0.0
|
419 |
+
dPdt=alpha_val*dXdt+beta_val*X
|
420 |
+
return [dXdt,dSdt,dPdt]
|
421 |
+
|
422 |
+
def get_initial_conditions(self, time, biomass, substrate, product): # For ODE solving
|
423 |
+
# Use experimental data for initial conditions if available and valid, otherwise use fitted parameters if available
|
424 |
+
X0_exp=biomass[0] if biomass is not None and len(biomass)>0 and np.isfinite(biomass[0]) else 1e-6 # Small default if no data
|
425 |
+
S0_exp=substrate[0] if substrate is not None and len(substrate)>0 and np.isfinite(substrate[0]) else 0.0
|
426 |
+
P0_exp=product[0] if product is not None and len(product)>0 and np.isfinite(product[0]) else 0.0
|
427 |
+
|
428 |
+
X0 = X0_exp # Default to experimental
|
429 |
if 'biomass' in self.params and self.params['biomass']:
|
430 |
+
# For models with direct Xo parameter
|
431 |
+
if self.model_type in ['logistic', 'baranyi']:
|
432 |
+
X0 = self.params['biomass'].get('Xo', self.params['biomass'].get('X0', X0_exp)) # Handles 'Xo' or 'X0'
|
433 |
+
# For models where X(t=0) is calculated
|
434 |
+
elif self.model_type == 'gompertz' and self.biomass_model and all(k in self.params['biomass'] for k in ['Xm','um','lag']):
|
435 |
+
X0_calc = self.biomass_model(0, self.params['biomass']['Xm'],self.params['biomass']['um'],self.params['biomass']['lag'])
|
436 |
+
if np.isfinite(X0_calc): X0 = X0_calc
|
437 |
+
elif self.model_type == 'moser' and self.biomass_model and all(k in self.params['biomass'] for k in ['Xm','um','Ks']):
|
438 |
+
X0_calc = self.biomass_model(0, self.params['biomass']['Xm'],self.params['biomass']['um'],self.params['biomass']['Ks'])
|
439 |
+
if np.isfinite(X0_calc): X0 = X0_calc
|
440 |
+
|
441 |
+
S0 = self.params.get('substrate',{}).get('so', S0_exp)
|
442 |
+
P0 = self.params.get('product',{}).get('po', P0_exp)
|
443 |
+
|
444 |
+
# Ensure initial conditions are finite and X0 is positive for ODE solver
|
445 |
+
X0 = max(X0 if np.isfinite(X0) else 1e-6, 1e-9)
|
446 |
+
S0 = S0 if np.isfinite(S0) else 0.0
|
447 |
+
P0 = P0 if np.isfinite(P0) else 0.0
|
448 |
+
return [X0, S0, P0]
|
449 |
|
450 |
def solve_differential_equations(self, time, biomass, substrate, product):
|
451 |
+
if self.biomass_diff is None: print(f"ODE no soportado para {self.model_type}."); return None,None,None,time
|
452 |
+
if 'biomass' not in self.params or not self.params['biomass']: print("Parámetros de biomasa no disponibles para EDO."); return None,None,None,time
|
453 |
+
if time is None or len(time)==0: print("Tiempo no válido para EDOs."); return None,None,None,np.array([])
|
454 |
|
455 |
+
biomass_p_ode=[] # Parameters for the dX/dt part of the system
|
456 |
+
if self.model_type=='logistic': biomass_p_ode=[self.params['biomass']['Xo'],self.params['biomass']['Xm'],self.params['biomass']['um']] # Xo is for IC, Xm, um for ODE
|
457 |
elif self.model_type=='gompertz': biomass_p_ode=[self.params['biomass']['Xm'],self.params['biomass']['um'],self.params['biomass']['lag']]
|
458 |
elif self.model_type=='moser': biomass_p_ode=[self.params['biomass']['Xm'],self.params['biomass']['um'],self.params['biomass']['Ks']]
|
459 |
+
else: print(f"Modelo {self.model_type} sin EDO definida en 'system'."); return None,None,None,time
|
460 |
|
461 |
substrate_p=[self.params.get('substrate',{}).get('so',0), self.params.get('substrate',{}).get('p',0), self.params.get('substrate',{}).get('q',0)]
|
462 |
product_p=[self.params.get('product',{}).get('po',0), self.params.get('product',{}).get('alpha',0), self.params.get('product',{}).get('beta',0)]
|
463 |
|
464 |
+
init_cond=self.get_initial_conditions(time,biomass,substrate,product) # Uses fitted Xo, So, Po if available
|
465 |
time_f=self.generate_fine_time_grid(time)
|
466 |
+
if len(time_f)==0 or len(time_f) == 1 and time_f[0]==0 : print("Malla de tiempo fina no generada."); return None,None,None,time
|
467 |
+
|
468 |
+
hmax_val=(time_f[-1]-time_f[0])/200.0 if len(time_f)>1 and time_f[-1]>time_f[0] else 0.0
|
469 |
try:
|
470 |
sol=odeint(self.system,init_cond,time_f,args=(biomass_p_ode,substrate_p,product_p,self.model_type),rtol=1e-6,atol=1e-6,hmax=hmax_val)
|
471 |
except Exception as e_ode:
|
472 |
+
print(f"Error resolviendo EDO ({self.model_type}, {self.params.get('biomass')}): {e_ode}. Intentando 'lsoda'.")
|
473 |
try: sol=odeint(self.system,init_cond,time_f,args=(biomass_p_ode,substrate_p,product_p,self.model_type),rtol=1e-6,atol=1e-6,method='lsoda',hmax=hmax_val)
|
474 |
+
except Exception as e_lsoda: print(f"Error resolviendo EDO con lsoda ({self.model_type}): {e_lsoda}"); return None,None,None,time_f
|
475 |
+
|
476 |
+
# Ensure solutions are non-negative
|
477 |
+
solX = np.maximum(sol[:,0], 0)
|
478 |
+
solS = np.maximum(sol[:,1], 0)
|
479 |
+
solP = np.maximum(sol[:,2], 0)
|
480 |
+
return solX, solS, solP, time_f
|
481 |
+
|
482 |
+
# plot_results and plot_combined_results remain largely the same as previous version,
|
483 |
+
# ensure they handle None from y_pred_biomass_fit gracefully.
|
484 |
+
# (Code for plot_results and plot_combined_results is omitted here for brevity but assumed to be from the previous corrected version)
|
485 |
def plot_results(self, time, biomass, substrate, product,
|
486 |
y_pred_biomass_fit, y_pred_substrate_fit, y_pred_product_fit,
|
487 |
biomass_std=None, substrate_std=None, product_std=None,
|
|
|
493 |
|
494 |
y_pred_b,y_pred_s,y_pred_p = y_pred_biomass_fit,y_pred_substrate_fit,y_pred_product_fit
|
495 |
time_curves = self.generate_fine_time_grid(time)
|
496 |
+
X_ode_success = False
|
497 |
|
498 |
if y_pred_biomass_fit is None and not (use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']):
|
499 |
+
# This case means biomass fitting itself failed. Plotting is not possible.
|
500 |
+
print(f"Ajuste de biomasa falló para {experiment_name}, modelo {self.model_type}. No se generará gráfico.")
|
501 |
+
return None # Critical: if biomass fit fails, no plot.
|
502 |
|
503 |
can_use_ode = use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']
|
504 |
|
|
|
510 |
if X_ode_res is not None:
|
511 |
y_pred_b,y_pred_s,y_pred_p,time_curves = X_ode_res,S_ode_res,P_ode_res,time_fine_ode
|
512 |
X_ode_success = True
|
513 |
+
else:
|
514 |
+
print(f"Solución EDO falló para {experiment_name}, {self.model_type}. Usando resultados de ajuste directo si existen.")
|
515 |
+
# Fallback to curve_fit results on fine grid if ODE failed
|
516 |
+
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']: # Check if biomass fit was successful
|
517 |
b_params=list(self.params['biomass'].values())
|
518 |
+
y_pred_b=self.biomass_model(time_curves,*b_params) # Use original y_pred_biomass_fit for this
|
519 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
520 |
s_params=list(self.params['substrate'].values()); y_pred_s=self.substrate(time_curves,*s_params,b_params)
|
521 |
else: y_pred_s=np.full_like(time_curves,np.nan)
|
522 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
523 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
524 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
525 |
+
else: # Biomass fit itself failed, so predictions are NaN
|
526 |
+
y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
527 |
+
else: # Not using ODE, ensure curve_fit results are plotted on fine grid
|
528 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
529 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
530 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
|
|
533 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
534 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
535 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
536 |
+
else: # Biomass fit failed
|
537 |
+
y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
538 |
+
|
539 |
|
540 |
fig,(ax1,ax2,ax3)=plt.subplots(3,1,figsize=(10,15),sharex=True)
|
541 |
+
title_suffix = " - EDO" if X_ode_success else (" - Ajuste Directo" if not use_differential or self.biomass_diff is None or not self.params.get('biomass') else " - EDO (falló, usando ajuste)")
|
542 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()}){title_suffix}',fontsize=16)
|
543 |
|
544 |
plot_cfg=[(ax1,biomass,y_pred_b,biomass_std,axis_labels['biomass_label'],'Modelo Biomasa',self.params.get('biomass',{}),self.r2.get('biomass',np.nan),self.rmse.get('biomass',np.nan)),
|
545 |
(ax2,substrate,y_pred_s,substrate_std,axis_labels['substrate_label'],'Modelo Sustrato',self.params.get('substrate',{}),self.r2.get('substrate',np.nan),self.rmse.get('substrate',np.nan)),
|
546 |
(ax3,product,y_pred_p,product_std,axis_labels['product_label'],'Modelo Producto',self.params.get('product',{}),self.r2.get('product',np.nan),self.rmse.get('product',np.nan))]
|
547 |
|
548 |
+
for i,(ax,data,y_pred_curve,std,ylab,leg_lab,p_dict,r2_val,rmse_val) in enumerate(plot_cfg):
|
549 |
+
# Plot experimental data
|
550 |
if data is not None and len(data)>0 and not np.all(np.isnan(data)):
|
551 |
if show_error_bars and std is not None and len(std)==len(data) and not np.all(np.isnan(std)):
|
552 |
ax.errorbar(time,data,yerr=std,fmt=marker_style,color=point_color,label='Datos experimentales',capsize=error_cap_size,elinewidth=error_line_width,markeredgewidth=1,markersize=5)
|
553 |
else: ax.plot(time,data,marker=marker_style,linestyle='',color=point_color,label='Datos experimentales',markersize=5)
|
554 |
+
else: ax.text(0.5,0.5,'No hay datos experimentales.',transform=ax.transAxes,ha='center',va='center',color='gray', fontsize=9)
|
555 |
|
556 |
+
# Plot model curve
|
557 |
if y_pred_curve is not None and len(y_pred_curve)>0 and not np.all(np.isnan(y_pred_curve)):
|
558 |
ax.plot(time_curves,y_pred_curve,linestyle=line_style,color=line_color,label=leg_lab)
|
559 |
+
elif i == 0 and y_pred_biomass_fit is None:
|
560 |
ax.text(0.5, 0.6, 'Modelo de biomasa no ajustado.', transform=ax.transAxes, ha='center', va='center', color='red', fontsize=9)
|
561 |
+
elif i > 0 and (y_pred_biomass_fit is None or not self.params.get('biomass')):
|
562 |
ax.text(0.5, 0.4, 'No ajustado (depende de biomasa).', transform=ax.transAxes, ha='center', va='center', color='orange', fontsize=9)
|
563 |
+
elif y_pred_curve is None or np.all(np.isnan(y_pred_curve)):
|
564 |
ax.text(0.5, 0.4, 'Modelo no ajustado o resultado inválido.', transform=ax.transAxes, ha='center', va='center', color='orange', fontsize=9)
|
565 |
|
566 |
ax.set_ylabel(ylab); ax.set_title(ylab)
|
567 |
if show_legend: ax.legend(loc=legend_position)
|
568 |
if show_params and p_dict and any(np.isfinite(v) for v in p_dict.values()):
|
569 |
p_txt='\n'.join([f"{k}={v:.3g}" if np.isfinite(v) else f"{k}=N/A" for k,v in p_dict.items()])
|
570 |
+
txt=f"{p_txt}\nR²={r2_val:.3f if np.isfinite(r2_val) else 'N/A'}\nRMSE={rmse_val:.3f if np.isfinite(rmse_val) else 'N/A'}"
|
571 |
if params_position=='outside right':
|
572 |
+
fig.subplots_adjust(right=0.70)
|
573 |
ax.annotate(txt,xy=(1.05,0.5),xycoords='axes fraction',xytext=(10,0),textcoords='offset points',va='center',ha='left',bbox={'boxstyle':'round,pad=0.3','facecolor':'wheat','alpha':0.7}, fontsize=8)
|
574 |
else:
|
575 |
tx,ha=(0.95,'right') if 'right' in params_position else (0.05,'left')
|
|
|
579 |
|
580 |
ax3.set_xlabel(axis_labels['x_label'])
|
581 |
plt.tight_layout(rect=[0,0.03,1,0.95]);
|
582 |
+
if params_position == 'outside right': fig.subplots_adjust(right=0.70)
|
583 |
|
584 |
buf=io.BytesIO(); fig.savefig(buf,format='png',bbox_inches='tight'); buf.seek(0)
|
585 |
image=Image.open(buf).convert("RGB"); plt.close(fig); return image
|
|
|
589 |
biomass_std=None, substrate_std=None, product_std=None,
|
590 |
experiment_name='', legend_position='best', params_position='upper right',
|
591 |
show_legend=True, show_params=True, style='whitegrid',
|
592 |
+
line_color='#0072B2', point_color='#D55E00', line_style='-', marker_style='o',
|
593 |
use_differential=False, axis_labels=None,
|
594 |
show_error_bars=True, error_cap_size=3, error_line_width=1):
|
595 |
|
|
|
597 |
time_curves = self.generate_fine_time_grid(time)
|
598 |
X_ode_success = False
|
599 |
|
600 |
+
if y_pred_biomass_fit is None and not (use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']):
|
601 |
+
print(f"Ajuste de biomasa falló para {experiment_name}, modelo {self.model_type} (combinado). No se generará gráfico.")
|
602 |
+
return None
|
603 |
+
|
604 |
can_use_ode = use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']
|
605 |
if axis_labels is None: axis_labels = {'x_label':'Tiempo','biomass_label':'Biomasa','substrate_label':'Sustrato','product_label':'Producto'}
|
606 |
sns.set_style(style)
|
|
|
611 |
if X_ode_res is not None:
|
612 |
y_pred_b,y_pred_s,y_pred_p,time_curves = X_ode_res,S_ode_res,P_ode_res,time_fine_ode
|
613 |
X_ode_success = True
|
614 |
+
else:
|
615 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
616 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
617 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'): s_params=list(self.params['substrate'].values()); y_pred_s=self.substrate(time_curves,*s_params,b_params)
|
|
|
629 |
else: y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
630 |
|
631 |
fig,ax1=plt.subplots(figsize=(12,7))
|
632 |
+
title_suffix = " - EDO" if X_ode_success else (" - Ajuste Directo" if not use_differential or self.biomass_diff is None or not self.params.get('biomass') else " - EDO (falló, usando ajuste)")
|
633 |
fig.suptitle(f'{experiment_name} ({self.model_type.capitalize()}){title_suffix}',fontsize=16)
|
634 |
|
635 |
colors = sns.color_palette("tab10", 3)
|
|
|
667 |
|
668 |
if show_params:
|
669 |
all_param_text = []
|
670 |
+
for cat_label, p_dict, r2_val, rmse_val in [
|
671 |
(axis_labels['biomass_label'], self.params.get('biomass',{}), self.r2.get('biomass',np.nan), self.rmse.get('biomass',np.nan)),
|
672 |
(axis_labels['substrate_label'], self.params.get('substrate',{}), self.r2.get('substrate',np.nan), self.rmse.get('substrate',np.nan)),
|
673 |
(axis_labels['product_label'], self.params.get('product',{}), self.r2.get('product',np.nan), self.rmse.get('product',np.nan))]:
|
674 |
if p_dict and any(np.isfinite(v) for v in p_dict.values()):
|
675 |
p_list = [f" {k}={v:.3g}" if np.isfinite(v) else f" {k}=N/A" for k,v in p_dict.items()]
|
676 |
+
all_param_text.append(f"{cat_label}:\n" + "\n".join(p_list) + f"\n R²={r2_val:.3f if np.isfinite(r2_val) else 'N/A'}\n RMSE={rmse_val:.3f if np.isfinite(rmse_val) else 'N/A'}")
|
677 |
total_text = "\n\n".join(all_param_text)
|
678 |
if total_text:
|
679 |
if params_position=='outside right':
|
680 |
+
fig.subplots_adjust(right=0.65)
|
681 |
fig.text(0.67,0.5,total_text,transform=fig.transFigure,va='center',ha='left',bbox=dict(boxstyle='round,pad=0.3',facecolor='wheat',alpha=0.7),fontsize=7)
|
682 |
else:
|
683 |
tx,ha=(0.95,'right') if 'right' in params_position else (0.05,'left')
|
|
|
690 |
buf=io.BytesIO(); fig.savefig(buf,format='png',bbox_inches='tight'); buf.seek(0)
|
691 |
image=Image.open(buf).convert("RGB"); plt.close(fig); return image
|
692 |
|
693 |
+
|
694 |
def sanitize_filename(name, max_length=100):
|
695 |
+
name = str(name)
|
696 |
+
name = re.sub(r'[^\w\s.-]', '', name).strip() # Allow dot for extension
|
697 |
name = re.sub(r'[-\s]+', '_', name)
|
698 |
return name[:max_length]
|
699 |
|
|
|
720 |
for sheet_name_idx, sheet_name in enumerate(sheet_names):
|
721 |
current_sheet_name_base = (experiment_names_list[sheet_name_idx]
|
722 |
if sheet_name_idx < len(experiment_names_list) and experiment_names_list[sheet_name_idx]
|
723 |
+
else f"Hoja_{sanitize_filename(sheet_name, 15)}")
|
724 |
try:
|
725 |
df = pd.read_excel(xls, sheet_name=sheet_name, header=[0, 1])
|
726 |
if df.empty: all_plot_messages.append(f"Hoja '{sheet_name}' vacía."); continue
|
|
|
736 |
if mode == 'independent':
|
737 |
unique_exp_groups = df.columns.get_level_values(0).unique()
|
738 |
for exp_group_idx, exp_group_name in enumerate(unique_exp_groups):
|
|
|
739 |
sanitized_exp_group_name = sanitize_filename(exp_group_name, 20)
|
740 |
current_experiment_name = f"{current_sheet_name_base}_{sanitized_exp_group_name}"
|
741 |
|
742 |
exp_df_slice_multi = df[exp_group_name]
|
743 |
try:
|
744 |
+
# Extract Time for this specific experiment group
|
745 |
+
if 'Tiempo' not in exp_df_slice_multi.columns.get_level_values(0): # Check if 'Tiempo' is a primary key in the slice
|
746 |
+
all_plot_messages.append(f"No se encontró 'Tiempo' en el grupo '{exp_group_name}' de la hoja '{sheet_name}'.")
|
747 |
+
continue
|
|
|
748 |
|
749 |
+
time_data_for_exp = exp_df_slice_multi['Tiempo']
|
750 |
+
if isinstance(time_data_for_exp, pd.DataFrame): # Time itself has replicates (unusual)
|
751 |
+
time_exp = time_data_for_exp.iloc[:,0].dropna().astype(float).values
|
752 |
+
else: # Time is a single series
|
753 |
+
time_exp = time_data_for_exp.dropna().astype(float).values
|
754 |
+
|
755 |
+
def get_comp_data_independent(component_name_str):
|
756 |
+
if component_name_str in exp_df_slice_multi.columns.get_level_values(0):
|
757 |
+
comp_data = exp_df_slice_multi[component_name_str]
|
758 |
+
if isinstance(comp_data,pd.DataFrame): # Has replicates (e.g., R1, R2 columns under 'Biomasa')
|
759 |
+
# Ensure all replicate columns are numeric before mean/std
|
760 |
+
numeric_cols = [col for col in comp_data.columns if pd.api.types.is_numeric_dtype(comp_data[col])]
|
761 |
+
if not numeric_cols: return np.array([]), None
|
762 |
+
comp_data_numeric = comp_data[numeric_cols]
|
763 |
+
return comp_data_numeric.mean(axis=1).dropna().astype(float).values, comp_data_numeric.std(axis=1,ddof=1).dropna().astype(float).values
|
764 |
+
elif pd.api.types.is_numeric_dtype(comp_data):
|
765 |
+
return comp_data.dropna().astype(float).values,None # Single numeric series
|
766 |
return np.array([]),None
|
767 |
|
768 |
biomass_exp,biomass_std_exp=get_comp_data_independent('Biomasa')
|
769 |
substrate_exp,substrate_std_exp=get_comp_data_independent('Sustrato')
|
770 |
product_exp,product_std_exp=get_comp_data_independent('Producto')
|
771 |
|
772 |
+
# Align all data to the shortest length after NaNs and with time_exp
|
773 |
min_len=len(time_exp)
|
774 |
if len(biomass_exp)>0:min_len=min(min_len,len(biomass_exp))
|
|
|
775 |
if len(substrate_exp)>0:min_len=min(min_len,len(substrate_exp))
|
776 |
if len(product_exp)>0:min_len=min(min_len,len(product_exp))
|
777 |
|
778 |
time_exp=time_exp[:min_len]
|
779 |
if len(biomass_exp)>0:biomass_exp=biomass_exp[:min_len]
|
780 |
+
else: biomass_exp = np.array([]) # Ensure it's an array
|
781 |
if biomass_std_exp is not None and len(biomass_std_exp)>0:biomass_std_exp=biomass_std_exp[:min_len]
|
782 |
+
|
783 |
if len(substrate_exp)>0:substrate_exp=substrate_exp[:min_len]
|
784 |
+
else: substrate_exp = np.array([])
|
785 |
if substrate_std_exp is not None and len(substrate_std_exp)>0:substrate_std_exp=substrate_std_exp[:min_len]
|
786 |
+
|
787 |
if len(product_exp)>0:product_exp=product_exp[:min_len]
|
788 |
+
else: product_exp = np.array([])
|
789 |
if product_std_exp is not None and len(product_std_exp)>0:product_std_exp=product_std_exp[:min_len]
|
790 |
|
|
|
791 |
if len(time_exp)==0: all_plot_messages.append(f"Sin datos de tiempo para {current_experiment_name}."); continue
|
792 |
+
if len(biomass_exp)==0:
|
793 |
all_plot_messages.append(f"Sin datos de biomasa para {current_experiment_name}, no se puede ajustar el modelo de biomasa.")
|
794 |
for mt_ in model_types_selected: comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
795 |
continue
|
|
|
796 |
except KeyError as e_key: all_plot_messages.append(f"Falta columna {e_key} en '{current_experiment_name}'."); continue
|
797 |
except Exception as e_data: all_plot_messages.append(f"Error procesando datos para '{current_experiment_name}': {e_data}."); continue
|
798 |
|
799 |
for model_type_iter in model_types_selected:
|
800 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
801 |
model_instance.fit_model()
|
802 |
+
y_pred_biomass = model_instance.fit_biomass(time_exp, biomass_exp) # This now gets filtered positive biomass
|
803 |
y_pred_substrate, y_pred_product = None, None
|
804 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
805 |
if len(substrate_exp)>0: y_pred_substrate = model_instance.fit_substrate(time_exp, substrate_exp, model_instance.params['biomass'])
|
806 |
if len(product_exp)>0: y_pred_product = model_instance.fit_product(time_exp, product_exp, model_instance.params['biomass'])
|
807 |
+
# else: # Message for failed biomass fit is now inside fit_biomass or due to insufficient data
|
808 |
+
# all_plot_messages.append(f"Ajuste de biomasa falló o datos insuficientes para {current_experiment_name}, modelo {model_type_iter}.")
|
809 |
|
810 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
811 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
|
|
828 |
substrate_std_avg = model_dummy_for_sheet.datas_std[-1] if model_dummy_for_sheet.datas_std and model_dummy_for_sheet.datas_std[-1] is not None and len(model_dummy_for_sheet.datas_std[-1]) == len(substrate_avg) else None
|
829 |
product_std_avg = model_dummy_for_sheet.datap_std[-1] if model_dummy_for_sheet.datap_std and model_dummy_for_sheet.datap_std[-1] is not None and len(model_dummy_for_sheet.datap_std[-1]) == len(product_avg) else None
|
830 |
|
831 |
+
if time_avg is None or len(time_avg)==0:
|
832 |
+
all_plot_messages.append(f"Sin datos de tiempo promedio para '{current_sheet_name_base}'. No se procesarán modelos para esta hoja.")
|
833 |
+
for mt_ in model_types_selected: comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
834 |
+
continue
|
835 |
+
|
836 |
if len(biomass_avg)==0:
|
837 |
+
all_plot_messages.append(f"Sin datos de biomasa promedio para '{current_sheet_name_base}'. No se procesarán modelos de biomasa para esta hoja.")
|
838 |
for mt_ in model_types_selected: comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
839 |
+
continue
|
840 |
+
|
841 |
+
# This explicit check for average mode helps clarify why fitting might be skipped for the whole sheet average.
|
842 |
+
# The fit_biomass function itself will also perform checks on the data it receives.
|
843 |
+
if biomass_avg[0] <= 1e-9: # Check if the very first point of the averaged biomass is problematic
|
844 |
+
all_plot_messages.append(f"Biomasa inicial promedio (valor={biomass_avg[0]:.2e}) para '{current_sheet_name_base}' es <= 1e-9. Los modelos de biomasa no se ajustarán para el promedio de esta hoja.")
|
845 |
+
for mt_ in model_types_selected:
|
846 |
+
comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
847 |
+
all_parameters_collected.setdefault(current_experiment_name, {})[mt_] = {'biomass': {}, 'substrate': {}, 'product': {}}
|
848 |
+
continue
|
849 |
|
850 |
for model_type_iter in model_types_selected:
|
851 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
|
|
855 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
856 |
if len(substrate_avg)>0: y_pred_substrate = model_instance.fit_substrate(time_avg, substrate_avg, model_instance.params['biomass'])
|
857 |
if len(product_avg)>0: y_pred_product = model_instance.fit_product(time_avg, product_avg, model_instance.params['biomass'])
|
858 |
+
# else: # Message now handled by fit_biomass or insufficient data checks
|
859 |
+
# all_plot_messages.append(f"Ajuste biomasa promedio falló o datos insuficientes: {current_experiment_name}, {model_type_iter}.")
|
860 |
|
861 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
862 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
|
|
875 |
cols_to_sort = ['R² Biomasa', 'R² Sustrato', 'R² Producto', 'RMSE Biomasa', 'RMSE Sustrato', 'RMSE Producto']
|
876 |
existing_cols_to_sort = [col for col in cols_to_sort if col in comparison_df.columns]
|
877 |
ascending_map = {'R²': False, 'RMSE': True}
|
878 |
+
sort_ascending = [True, True] + [ascending_map[col.split(' ')[0]] for col in existing_cols_to_sort if col.split(' ')[0] in ascending_map]
|
879 |
comparison_df_sorted = comparison_df.sort_values(by=['Experimento','Modelo']+existing_cols_to_sort,ascending=sort_ascending).reset_index(drop=True)
|
880 |
else: comparison_df_sorted = pd.DataFrame(columns=['Experimento','Modelo','R² Biomasa','RMSE Biomasa','R² Sustrato','RMSE Sustrato','R² Producto','RMSE Producto'])
|
881 |
|
882 |
final_message = "Procesamiento completado."
|
883 |
+
if all_plot_messages: final_message += " Mensajes:\n" + "\n".join(list(set(all_plot_messages)))
|
884 |
if not figures_with_names and not comparison_df_sorted.empty: final_message += "\nNo se generaron gráficos, pero hay datos en la tabla."
|
885 |
+
elif not figures_with_names and comparison_df_sorted.empty and not all_plot_messages : final_message += "\nNo se generaron gráficos ni datos para la tabla (posiblemente no hay datos válidos en el archivo)."
|
886 |
+
elif not figures_with_names and comparison_df_sorted.empty and all_plot_messages : pass # Messages already cover issues
|
887 |
|
888 |
return figures_with_names, comparison_df_sorted, final_message, all_parameters_collected
|
889 |
|
890 |
+
# ... (MODEL_CHOICES, create_zip_of_images, create_interface, and __main__ block remain the same as the previous corrected version)
|
891 |
+
# Ensure create_interface uses the corrected gr.Dataframe without 'height'
|
892 |
+
# The rest of the UI and helper functions (create_zip_of_images, export functions) are assumed to be correct from the prior version.
|
893 |
|
894 |
MODEL_CHOICES = [("Logistic (3-parám)","logistic"),("Gompertz (3-parám)","gompertz"),("Moser (3-parám)","moser"),("Baranyi (4-parám)","baranyi")]
|
895 |
|
|
|
902 |
for item_idx, item in enumerate(figures_with_names_list):
|
903 |
img_pil = item['image']
|
904 |
img_name_suggestion = item['name']
|
|
|
|
|
905 |
base_name, ext = os.path.splitext(img_name_suggestion)
|
906 |
+
if not ext: ext = ".png"
|
907 |
+
sanitized_base = sanitize_filename(base_name, max_length=80)
|
|
|
908 |
img_name_in_zip = f"{sanitized_base}{ext}"
|
|
|
|
|
909 |
count = 1
|
910 |
original_sanitized_base = sanitized_base
|
911 |
while img_name_in_zip in zf.namelist():
|
912 |
img_name_in_zip = f"{original_sanitized_base}_{count}{ext}"
|
913 |
count += 1
|
914 |
+
if count > len(figures_with_names_list) + 5:
|
915 |
+
img_name_in_zip = f"{original_sanitized_base}_{item_idx}_{count}{ext}"
|
916 |
break
|
|
|
|
|
917 |
img_byte_arr = io.BytesIO()
|
918 |
img_pil.save(img_byte_arr, format='PNG')
|
919 |
img_byte_arr.seek(0)
|
920 |
zf.writestr(img_name_in_zip, img_byte_arr.getvalue())
|
|
|
921 |
zip_buffer.seek(0)
|
|
|
922 |
try:
|
|
|
|
|
923 |
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip_file:
|
924 |
tmp_zip_file.write(zip_buffer.getvalue())
|
925 |
return tmp_zip_file.name, "ZIP con imágenes generado exitosamente."
|
926 |
except Exception as e:
|
927 |
return None, f"Error creando archivo ZIP temporal: {str(e)}"
|
928 |
|
|
|
929 |
def create_interface():
|
930 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
931 |
gr.Markdown("# Modelos Cinéticos de Bioprocesos")
|
|
|
932 |
with gr.Tab("Teoría y Uso"):
|
933 |
gr.Markdown(r"""
|
934 |
+
Análisis y visualización de datos de bioprocesos... (etc., same as before)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
935 |
""")
|
936 |
gr.Markdown(r"""
|
937 |
+
## Modelos Matemáticos para Bioprocesos ... (etc., same as before)
|
|
|
|
|
|
|
|
|
|
|
938 |
""")
|
|
|
939 |
with gr.Tab("Simulación"):
|
940 |
with gr.Row():
|
941 |
file_input = gr.File(label="Subir archivo Excel (.xlsx)", file_types=['.xlsx'])
|
|
|
970 |
with gr.Accordion("Configuración Avanzada de Ajuste (No implementado aún)", open=False):
|
971 |
lower_bounds_str_ui = gr.Textbox(label="Lower Bounds (JSON, no usado)", lines=3)
|
972 |
upper_bounds_str_ui = gr.Textbox(label="Upper Bounds (JSON, no usado)", lines=3)
|
|
|
973 |
simulate_btn = gr.Button("Simular y Graficar", variant="primary")
|
974 |
|
975 |
with gr.Tab("Resultados"):
|
976 |
+
status_message_ui = gr.Textbox(label="Estado del Procesamiento", interactive=False, lines=3, max_lines=10) # Increased lines for messages
|
977 |
output_gallery_ui = gr.Gallery(label="Resultados Gráficos", columns=[2,1], height=600, object_fit="contain", preview=True)
|
978 |
+
output_table_ui = gr.Dataframe(
|
979 |
label="Tabla Comparativa de Modelos",
|
980 |
headers=["Experimento","Modelo","R² Biomasa","RMSE Biomasa","R² Sustrato","RMSE Sustrato","R² Producto","RMSE Producto"],
|
981 |
interactive=False,
|
982 |
wrap=True
|
983 |
+
# Removed height=400
|
984 |
)
|
|
|
985 |
state_df_ui = gr.State(pd.DataFrame())
|
986 |
state_params_ui = gr.State({})
|
987 |
state_figures_ui = gr.State([])
|
|
|
988 |
with gr.Row():
|
989 |
export_excel_btn = gr.Button("Exportar Tabla a Excel")
|
990 |
export_csv_btn = gr.Button("Exportar Tabla a CSV")
|
991 |
export_params_btn = gr.Button("Exportar Parámetros a Excel")
|
992 |
export_images_zip_btn = gr.Button("Descargar Imágenes (ZIP)")
|
|
|
993 |
download_file_output_ui = gr.File(label="Descargar Tabla/Parámetros", interactive=False)
|
994 |
download_zip_images_ui = gr.File(label="Descargar ZIP de Imágenes", interactive=False)
|
995 |
|
|
|
1002 |
if file is None: return [], pd.DataFrame(), "Error: Sube un archivo Excel.", pd.DataFrame(), {}, []
|
1003 |
axis_labels = {'x_label':x_label or 'Tiempo','biomass_label':biomass_label or 'Biomasa','substrate_label':substrate_label or 'Sustrato','product_label':product_label or 'Producto'}
|
1004 |
if not models_sel: return [], pd.DataFrame(), "Error: Selecciona un modelo.", pd.DataFrame(), {}, []
|
|
|
1005 |
figures_with_names, comparison_df, message, collected_params = process_all_data(
|
1006 |
file, legend_pos, params_pos, models_sel, exp_names, low_bounds_str, up_bounds_str,
|
1007 |
analysis_mode, plot_style, line_col, point_col, line_sty, marker_sty,
|
1008 |
show_leg, show_par, use_diff, int(maxfev), axis_labels,
|
1009 |
+
show_error_bars_arg, error_cap_size_arg, error_line_width_arg)
|
|
|
1010 |
pil_images_for_gallery = [item['image'] for item in figures_with_names] if figures_with_names else []
|
1011 |
return pil_images_for_gallery, comparison_df, message, comparison_df, collected_params, figures_with_names
|
|
|
1012 |
simulate_btn.click(
|
1013 |
fn=run_simulation_interface,
|
1014 |
inputs=[file_input, legend_position_ui, params_position_ui, model_types_selected_ui, mode, experiment_names_str_ui,
|
|
|
1016 |
line_style_dropdown_ui, marker_style_dropdown_ui, show_legend_ui, show_params_ui, use_differential_ui,
|
1017 |
maxfev_input_ui, x_axis_label_input_ui, biomass_axis_label_input_ui, substrate_axis_label_input_ui,
|
1018 |
product_axis_label_input_ui, show_error_bars_ui, error_cap_size_ui, error_line_width_ui],
|
1019 |
+
outputs=[output_gallery_ui, output_table_ui, status_message_ui, state_df_ui, state_params_ui, state_figures_ui])
|
|
|
1020 |
|
1021 |
def export_df_to_file(df_to_export, file_format="excel"):
|
1022 |
if df_to_export is None or df_to_export.empty:
|
1023 |
with tempfile.NamedTemporaryFile(suffix=".txt",delete=False,mode="w",encoding="utf-8") as tmp: tmp.write("No hay datos en la tabla para exportar."); return tmp.name,"No hay datos para exportar."
|
1024 |
try:
|
1025 |
suffix=".xlsx" if file_format=="excel" else ".csv"
|
1026 |
+
with tempfile.NamedTemporaryFile(suffix=suffix,delete=False,mode='w+b' if file_format=="excel" else 'w', encoding=None if file_format=="excel" else "utf-8-sig") as tmp_f:
|
1027 |
if file_format=="excel": df_to_export.to_excel(tmp_f.name,index=False)
|
1028 |
else: df_to_export.to_csv(tmp_f.name,index=False)
|
1029 |
return tmp_f.name,f"Tabla exportada a {suffix[1:]} exitosamente."
|
|
|
1041 |
for exp_name,models_data in params_state_dict.items():
|
1042 |
for model_type_name,all_params_for_model_type in models_data.items():
|
1043 |
for param_category,category_params in all_params_for_model_type.items():
|
1044 |
+
if category_params and isinstance(category_params,dict) and category_params:
|
1045 |
df_params=pd.DataFrame({'Parámetro':list(category_params.keys()),'Valor':list(category_params.values())})
|
1046 |
s_exp=sanitize_filename(exp_name,15); s_mod=sanitize_filename(model_type_name,10); s_cat=sanitize_filename(param_category,4)
|
1047 |
+
sheet_name=f"{s_exp}_{s_mod}_{s_cat}"[:31]
|
1048 |
orig_sn,c=sheet_name,1
|
1049 |
while sheet_name in writer.sheets: sheet_name=f"{orig_sn[:28]}_{c}"[:31]; c+=1;
|
1050 |
df_params.to_excel(writer,sheet_name=sheet_name,index=False)
|
|
|
1068 |
return demo
|
1069 |
|
1070 |
if __name__ == '__main__':
|
|
|
|
|
1071 |
demo_instance = create_interface()
|
1072 |
+
demo_instance.launch(share=False, debug=True)
|