Update app.py
Browse files
app.py
CHANGED
@@ -493,9 +493,8 @@ class BioprocessModel:
|
|
493 |
X_ode_success = False
|
494 |
|
495 |
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']):
|
496 |
-
# This case means biomass fitting itself failed. Plotting is not possible.
|
497 |
print(f"Ajuste de biomasa falló para {experiment_name}, modelo {self.model_type}. No se generará gráfico.")
|
498 |
-
return None
|
499 |
|
500 |
can_use_ode = use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']
|
501 |
|
@@ -509,19 +508,18 @@ class BioprocessModel:
|
|
509 |
X_ode_success = True
|
510 |
else:
|
511 |
print(f"Solución EDO falló para {experiment_name}, {self.model_type}. Usando resultados de ajuste directo si existen.")
|
512 |
-
|
513 |
-
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
|
514 |
b_params=list(self.params['biomass'].values())
|
515 |
-
y_pred_b=self.biomass_model(time_curves,*b_params)
|
516 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
517 |
s_params=list(self.params['substrate'].values()); y_pred_s=self.substrate(time_curves,*s_params,b_params)
|
518 |
else: y_pred_s=np.full_like(time_curves,np.nan)
|
519 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
520 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
521 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
522 |
-
else:
|
523 |
y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
524 |
-
else:
|
525 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
526 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
527 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
@@ -530,7 +528,7 @@ class BioprocessModel:
|
|
530 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
531 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
532 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
533 |
-
else:
|
534 |
y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
535 |
|
536 |
|
@@ -543,14 +541,12 @@ class BioprocessModel:
|
|
543 |
(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))]
|
544 |
|
545 |
for i,(ax,data,y_pred_curve,std,ylab,leg_lab,p_dict,r2_val,rmse_val) in enumerate(plot_cfg):
|
546 |
-
# Plot experimental data
|
547 |
if data is not None and len(data)>0 and not np.all(np.isnan(data)):
|
548 |
if show_error_bars and std is not None and len(std)==len(data) and not np.all(np.isnan(std)):
|
549 |
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)
|
550 |
else: ax.plot(time,data,marker=marker_style,linestyle='',color=point_color,label='Datos experimentales',markersize=5)
|
551 |
else: ax.text(0.5,0.5,'No hay datos experimentales.',transform=ax.transAxes,ha='center',va='center',color='gray', fontsize=9)
|
552 |
|
553 |
-
# Plot model curve
|
554 |
if y_pred_curve is not None and len(y_pred_curve)>0 and not np.all(np.isnan(y_pred_curve)):
|
555 |
ax.plot(time_curves,y_pred_curve,linestyle=line_style,color=line_color,label=leg_lab)
|
556 |
elif i == 0 and y_pred_biomass_fit is None:
|
@@ -564,10 +560,11 @@ class BioprocessModel:
|
|
564 |
if show_legend: ax.legend(loc=legend_position)
|
565 |
if show_params and p_dict and any(np.isfinite(v) for v in p_dict.values()):
|
566 |
p_txt='\n'.join([f"{k}={v:.3g}" if np.isfinite(v) else f"{k}=N/A" for k,v in p_dict.items()])
|
567 |
-
#
|
568 |
-
r2_str = f"{r2_val:.3f}" if np.isfinite(r2_val) else
|
569 |
-
rmse_str = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else
|
570 |
-
txt
|
|
|
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)
|
@@ -605,7 +602,6 @@ class BioprocessModel:
|
|
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)
|
607 |
|
608 |
-
# Logic for ODE/curve_fit selection (similar to plot_results)
|
609 |
if can_use_ode:
|
610 |
X_ode_res,S_ode_res,P_ode_res,time_fine_ode = self.solve_differential_equations(time,biomass,substrate,product)
|
611 |
if X_ode_res is not None:
|
@@ -619,7 +615,7 @@ class BioprocessModel:
|
|
619 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'): p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
620 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
621 |
else: y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
622 |
-
else:
|
623 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
624 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
625 |
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)
|
@@ -673,10 +669,11 @@ class BioprocessModel:
|
|
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 |
-
#
|
677 |
-
r2_str = f"{r2_val:.3f}" if np.isfinite(r2_val) else
|
678 |
-
rmse_str = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else
|
679 |
all_param_text.append(f"{cat_label}:\n" + "\n".join(p_list) + f"\n R²={r2_str}\n RMSE={rmse_str}")
|
|
|
680 |
total_text = "\n\n".join(all_param_text)
|
681 |
if total_text:
|
682 |
if params_position=='outside right':
|
@@ -744,35 +741,32 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
744 |
|
745 |
exp_df_slice_multi = df[exp_group_name]
|
746 |
try:
|
747 |
-
|
748 |
-
if 'Tiempo' not in exp_df_slice_multi.columns.get_level_values(0): # Check if 'Tiempo' is a primary key in the slice
|
749 |
all_plot_messages.append(f"No se encontró 'Tiempo' en el grupo '{exp_group_name}' de la hoja '{sheet_name}'.")
|
750 |
continue
|
751 |
|
752 |
time_data_for_exp = exp_df_slice_multi['Tiempo']
|
753 |
-
if isinstance(time_data_for_exp, pd.DataFrame):
|
754 |
time_exp = time_data_for_exp.iloc[:,0].dropna().astype(float).values
|
755 |
-
else:
|
756 |
time_exp = time_data_for_exp.dropna().astype(float).values
|
757 |
|
758 |
def get_comp_data_independent(component_name_str):
|
759 |
if component_name_str in exp_df_slice_multi.columns.get_level_values(0):
|
760 |
comp_data = exp_df_slice_multi[component_name_str]
|
761 |
-
if isinstance(comp_data,pd.DataFrame):
|
762 |
-
# Ensure all replicate columns are numeric before mean/std
|
763 |
numeric_cols = [col for col in comp_data.columns if pd.api.types.is_numeric_dtype(comp_data[col])]
|
764 |
if not numeric_cols: return np.array([]), None
|
765 |
comp_data_numeric = comp_data[numeric_cols]
|
766 |
return comp_data_numeric.mean(axis=1).dropna().astype(float).values, comp_data_numeric.std(axis=1,ddof=1).dropna().astype(float).values
|
767 |
elif pd.api.types.is_numeric_dtype(comp_data):
|
768 |
-
return comp_data.dropna().astype(float).values,None
|
769 |
return np.array([]),None
|
770 |
|
771 |
biomass_exp,biomass_std_exp=get_comp_data_independent('Biomasa')
|
772 |
substrate_exp,substrate_std_exp=get_comp_data_independent('Sustrato')
|
773 |
product_exp,product_std_exp=get_comp_data_independent('Producto')
|
774 |
|
775 |
-
# Align all data to the shortest length after NaNs and with time_exp
|
776 |
min_len=len(time_exp)
|
777 |
if len(biomass_exp)>0:min_len=min(min_len,len(biomass_exp))
|
778 |
if len(substrate_exp)>0:min_len=min(min_len,len(substrate_exp))
|
@@ -780,7 +774,7 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
780 |
|
781 |
time_exp=time_exp[:min_len]
|
782 |
if len(biomass_exp)>0:biomass_exp=biomass_exp[:min_len]
|
783 |
-
else: biomass_exp = np.array([])
|
784 |
if biomass_std_exp is not None and len(biomass_std_exp)>0:biomass_std_exp=biomass_std_exp[:min_len]
|
785 |
|
786 |
if len(substrate_exp)>0:substrate_exp=substrate_exp[:min_len]
|
@@ -802,13 +796,11 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
802 |
for model_type_iter in model_types_selected:
|
803 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
804 |
model_instance.fit_model()
|
805 |
-
y_pred_biomass = model_instance.fit_biomass(time_exp, biomass_exp)
|
806 |
y_pred_substrate, y_pred_product = None, None
|
807 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
808 |
if len(substrate_exp)>0: y_pred_substrate = model_instance.fit_substrate(time_exp, substrate_exp, model_instance.params['biomass'])
|
809 |
if len(product_exp)>0: y_pred_product = model_instance.fit_product(time_exp, product_exp, model_instance.params['biomass'])
|
810 |
-
# else: # Message for failed biomass fit is now inside fit_biomass or due to insufficient data
|
811 |
-
# all_plot_messages.append(f"Ajuste de biomasa falló o datos insuficientes para {current_experiment_name}, modelo {model_type_iter}.")
|
812 |
|
813 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
814 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
@@ -841,9 +833,7 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
841 |
for mt_ in model_types_selected: comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
842 |
continue
|
843 |
|
844 |
-
|
845 |
-
# The fit_biomass function itself will also perform checks on the data it receives.
|
846 |
-
if biomass_avg[0] <= 1e-9: # Check if the very first point of the averaged biomass is problematic
|
847 |
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.")
|
848 |
for mt_ in model_types_selected:
|
849 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
@@ -858,8 +848,6 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
858 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
859 |
if len(substrate_avg)>0: y_pred_substrate = model_instance.fit_substrate(time_avg, substrate_avg, model_instance.params['biomass'])
|
860 |
if len(product_avg)>0: y_pred_product = model_instance.fit_product(time_avg, product_avg, model_instance.params['biomass'])
|
861 |
-
# else: # Message now handled by fit_biomass or insufficient data checks
|
862 |
-
# all_plot_messages.append(f"Ajuste biomasa promedio falló o datos insuficientes: {current_experiment_name}, {model_type_iter}.")
|
863 |
|
864 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
865 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
@@ -886,7 +874,7 @@ def process_all_data(file, legend_position, params_position, model_types_selecte
|
|
886 |
if all_plot_messages: final_message += " Mensajes:\n" + "\n".join(list(set(all_plot_messages)))
|
887 |
if not figures_with_names and not comparison_df_sorted.empty: final_message += "\nNo se generaron gráficos, pero hay datos en la tabla."
|
888 |
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)."
|
889 |
-
elif not figures_with_names and comparison_df_sorted.empty and all_plot_messages : pass
|
890 |
|
891 |
return figures_with_names, comparison_df_sorted, final_message, all_parameters_collected
|
892 |
|
@@ -930,10 +918,64 @@ def create_interface():
|
|
930 |
gr.Markdown("# Modelos Cinéticos de Bioprocesos")
|
931 |
with gr.Tab("Teoría y Uso"):
|
932 |
gr.Markdown(r"""
|
933 |
-
Análisis y visualización de datos de bioprocesos
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
934 |
""")
|
935 |
gr.Markdown(r"""
|
936 |
-
## Modelos Matemáticos para Bioprocesos
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
937 |
""")
|
938 |
with gr.Tab("Simulación"):
|
939 |
with gr.Row():
|
@@ -972,14 +1014,13 @@ def create_interface():
|
|
972 |
simulate_btn = gr.Button("Simular y Graficar", variant="primary")
|
973 |
|
974 |
with gr.Tab("Resultados"):
|
975 |
-
status_message_ui = gr.Textbox(label="Estado del Procesamiento", interactive=False, lines=3, max_lines=10)
|
976 |
output_gallery_ui = gr.Gallery(label="Resultados Gráficos", columns=[2,1], height=600, object_fit="contain", preview=True)
|
977 |
output_table_ui = gr.Dataframe(
|
978 |
label="Tabla Comparativa de Modelos",
|
979 |
headers=["Experimento","Modelo","R² Biomasa","RMSE Biomasa","R² Sustrato","RMSE Sustrato","R² Producto","RMSE Producto"],
|
980 |
interactive=False,
|
981 |
wrap=True
|
982 |
-
# Removed height=400
|
983 |
)
|
984 |
state_df_ui = gr.State(pd.DataFrame())
|
985 |
state_params_ui = gr.State({})
|
|
|
493 |
X_ode_success = False
|
494 |
|
495 |
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']):
|
|
|
496 |
print(f"Ajuste de biomasa falló para {experiment_name}, modelo {self.model_type}. No se generará gráfico.")
|
497 |
+
return None
|
498 |
|
499 |
can_use_ode = use_differential and self.biomass_diff is not None and 'biomass' in self.params and self.params['biomass']
|
500 |
|
|
|
508 |
X_ode_success = True
|
509 |
else:
|
510 |
print(f"Solución EDO falló para {experiment_name}, {self.model_type}. Usando resultados de ajuste directo si existen.")
|
511 |
+
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
|
|
512 |
b_params=list(self.params['biomass'].values())
|
513 |
+
y_pred_b=self.biomass_model(time_curves,*b_params)
|
514 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
515 |
s_params=list(self.params['substrate'].values()); y_pred_s=self.substrate(time_curves,*s_params,b_params)
|
516 |
else: y_pred_s=np.full_like(time_curves,np.nan)
|
517 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
518 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
519 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
520 |
+
else:
|
521 |
y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
522 |
+
else:
|
523 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
524 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
525 |
if y_pred_substrate_fit is not None and 'substrate' in self.params and self.params.get('substrate'):
|
|
|
528 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'):
|
529 |
p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
530 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
531 |
+
else:
|
532 |
y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
533 |
|
534 |
|
|
|
541 |
(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))]
|
542 |
|
543 |
for i,(ax,data,y_pred_curve,std,ylab,leg_lab,p_dict,r2_val,rmse_val) in enumerate(plot_cfg):
|
|
|
544 |
if data is not None and len(data)>0 and not np.all(np.isnan(data)):
|
545 |
if show_error_bars and std is not None and len(std)==len(data) and not np.all(np.isnan(std)):
|
546 |
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)
|
547 |
else: ax.plot(time,data,marker=marker_style,linestyle='',color=point_color,label='Datos experimentales',markersize=5)
|
548 |
else: ax.text(0.5,0.5,'No hay datos experimentales.',transform=ax.transAxes,ha='center',va='center',color='gray', fontsize=9)
|
549 |
|
|
|
550 |
if y_pred_curve is not None and len(y_pred_curve)>0 and not np.all(np.isnan(y_pred_curve)):
|
551 |
ax.plot(time_curves,y_pred_curve,linestyle=line_style,color=line_color,label=leg_lab)
|
552 |
elif i == 0 and y_pred_biomass_fit is None:
|
|
|
560 |
if show_legend: ax.legend(loc=legend_position)
|
561 |
if show_params and p_dict and any(np.isfinite(v) for v in p_dict.values()):
|
562 |
p_txt='\n'.join([f"{k}={v:.3g}" if np.isfinite(v) else f"{k}=N/A" for k,v in p_dict.items()])
|
563 |
+
# *** CORRECTED LINES START ***
|
564 |
+
r2_str = f"{r2_val:.3f}" if np.isfinite(r2_val) else "N/A"
|
565 |
+
rmse_str = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else "N/A"
|
566 |
+
txt=f"{p_txt}\nR²={r2_str}\nRMSE={rmse_str}"
|
567 |
+
# *** CORRECTED LINES END ***
|
568 |
if params_position=='outside right':
|
569 |
fig.subplots_adjust(right=0.70)
|
570 |
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)
|
|
|
602 |
if axis_labels is None: axis_labels = {'x_label':'Tiempo','biomass_label':'Biomasa','substrate_label':'Sustrato','product_label':'Producto'}
|
603 |
sns.set_style(style)
|
604 |
|
|
|
605 |
if can_use_ode:
|
606 |
X_ode_res,S_ode_res,P_ode_res,time_fine_ode = self.solve_differential_equations(time,biomass,substrate,product)
|
607 |
if X_ode_res is not None:
|
|
|
615 |
if y_pred_product_fit is not None and 'product' in self.params and self.params.get('product'): p_params=list(self.params['product'].values()); y_pred_p=self.product(time_curves,*p_params,b_params)
|
616 |
else: y_pred_p=np.full_like(time_curves,np.nan)
|
617 |
else: y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
|
618 |
+
else:
|
619 |
if y_pred_biomass_fit is not None and self.biomass_model and 'biomass' in self.params and self.params['biomass']:
|
620 |
b_params=list(self.params['biomass'].values()); y_pred_b=self.biomass_model(time_curves,*b_params)
|
621 |
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)
|
|
|
669 |
(axis_labels['product_label'], self.params.get('product',{}), self.r2.get('product',np.nan), self.rmse.get('product',np.nan))]:
|
670 |
if p_dict and any(np.isfinite(v) for v in p_dict.values()):
|
671 |
p_list = [f" {k}={v:.3g}" if np.isfinite(v) else f" {k}=N/A" for k,v in p_dict.items()]
|
672 |
+
# *** CORRECTED LINES START ***
|
673 |
+
r2_str = f"{r2_val:.3f}" if np.isfinite(r2_val) else "N/A"
|
674 |
+
rmse_str = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else "N/A"
|
675 |
all_param_text.append(f"{cat_label}:\n" + "\n".join(p_list) + f"\n R²={r2_str}\n RMSE={rmse_str}")
|
676 |
+
# *** CORRECTED LINES END ***
|
677 |
total_text = "\n\n".join(all_param_text)
|
678 |
if total_text:
|
679 |
if params_position=='outside right':
|
|
|
741 |
|
742 |
exp_df_slice_multi = df[exp_group_name]
|
743 |
try:
|
744 |
+
if 'Tiempo' not in exp_df_slice_multi.columns.get_level_values(0):
|
|
|
745 |
all_plot_messages.append(f"No se encontró 'Tiempo' en el grupo '{exp_group_name}' de la hoja '{sheet_name}'.")
|
746 |
continue
|
747 |
|
748 |
time_data_for_exp = exp_df_slice_multi['Tiempo']
|
749 |
+
if isinstance(time_data_for_exp, pd.DataFrame):
|
750 |
time_exp = time_data_for_exp.iloc[:,0].dropna().astype(float).values
|
751 |
+
else:
|
752 |
time_exp = time_data_for_exp.dropna().astype(float).values
|
753 |
|
754 |
def get_comp_data_independent(component_name_str):
|
755 |
if component_name_str in exp_df_slice_multi.columns.get_level_values(0):
|
756 |
comp_data = exp_df_slice_multi[component_name_str]
|
757 |
+
if isinstance(comp_data,pd.DataFrame):
|
|
|
758 |
numeric_cols = [col for col in comp_data.columns if pd.api.types.is_numeric_dtype(comp_data[col])]
|
759 |
if not numeric_cols: return np.array([]), None
|
760 |
comp_data_numeric = comp_data[numeric_cols]
|
761 |
return comp_data_numeric.mean(axis=1).dropna().astype(float).values, comp_data_numeric.std(axis=1,ddof=1).dropna().astype(float).values
|
762 |
elif pd.api.types.is_numeric_dtype(comp_data):
|
763 |
+
return comp_data.dropna().astype(float).values,None
|
764 |
return np.array([]),None
|
765 |
|
766 |
biomass_exp,biomass_std_exp=get_comp_data_independent('Biomasa')
|
767 |
substrate_exp,substrate_std_exp=get_comp_data_independent('Sustrato')
|
768 |
product_exp,product_std_exp=get_comp_data_independent('Producto')
|
769 |
|
|
|
770 |
min_len=len(time_exp)
|
771 |
if len(biomass_exp)>0:min_len=min(min_len,len(biomass_exp))
|
772 |
if len(substrate_exp)>0:min_len=min(min_len,len(substrate_exp))
|
|
|
774 |
|
775 |
time_exp=time_exp[:min_len]
|
776 |
if len(biomass_exp)>0:biomass_exp=biomass_exp[:min_len]
|
777 |
+
else: biomass_exp = np.array([])
|
778 |
if biomass_std_exp is not None and len(biomass_std_exp)>0:biomass_std_exp=biomass_std_exp[:min_len]
|
779 |
|
780 |
if len(substrate_exp)>0:substrate_exp=substrate_exp[:min_len]
|
|
|
796 |
for model_type_iter in model_types_selected:
|
797 |
model_instance = BioprocessModel(model_type=model_type_iter, maxfev=maxfev_val)
|
798 |
model_instance.fit_model()
|
799 |
+
y_pred_biomass = model_instance.fit_biomass(time_exp, biomass_exp)
|
800 |
y_pred_substrate, y_pred_product = None, None
|
801 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
802 |
if len(substrate_exp)>0: y_pred_substrate = model_instance.fit_substrate(time_exp, substrate_exp, model_instance.params['biomass'])
|
803 |
if len(product_exp)>0: y_pred_product = model_instance.fit_product(time_exp, product_exp, model_instance.params['biomass'])
|
|
|
|
|
804 |
|
805 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
806 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
|
|
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 biomass_avg[0] <= 1e-9:
|
|
|
|
|
837 |
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.")
|
838 |
for mt_ in model_types_selected:
|
839 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':mt_.capitalize(),'R² Biomasa':np.nan,'RMSE Biomasa':np.nan})
|
|
|
848 |
if y_pred_biomass is not None and model_instance.params.get('biomass'):
|
849 |
if len(substrate_avg)>0: y_pred_substrate = model_instance.fit_substrate(time_avg, substrate_avg, model_instance.params['biomass'])
|
850 |
if len(product_avg)>0: y_pred_product = model_instance.fit_product(time_avg, product_avg, model_instance.params['biomass'])
|
|
|
|
|
851 |
|
852 |
all_parameters_collected.setdefault(current_experiment_name, {})[model_type_iter] = model_instance.params
|
853 |
comparison_data.append({'Experimento':current_experiment_name,'Modelo':model_type_iter.capitalize(),
|
|
|
874 |
if all_plot_messages: final_message += " Mensajes:\n" + "\n".join(list(set(all_plot_messages)))
|
875 |
if not figures_with_names and not comparison_df_sorted.empty: final_message += "\nNo se generaron gráficos, pero hay datos en la tabla."
|
876 |
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)."
|
877 |
+
elif not figures_with_names and comparison_df_sorted.empty and all_plot_messages : pass
|
878 |
|
879 |
return figures_with_names, comparison_df_sorted, final_message, all_parameters_collected
|
880 |
|
|
|
918 |
gr.Markdown("# Modelos Cinéticos de Bioprocesos")
|
919 |
with gr.Tab("Teoría y Uso"):
|
920 |
gr.Markdown(r"""
|
921 |
+
Análisis y visualización de datos de bioprocesos. Esta herramienta permite ajustar diferentes modelos cinéticos (Logistic, Gompertz, Moser, Baranyi)
|
922 |
+
a datos experimentales de crecimiento microbiano, consumo de sustrato y producción de metabolitos.
|
923 |
+
**Instrucciones:**
|
924 |
+
1. Prepara tus datos en un archivo Excel (.xlsx). Cada hoja puede representar un experimento o condición diferente.
|
925 |
+
2. El formato del Excel debe tener un encabezado de dos niveles:
|
926 |
+
* Nivel 0: Nombre del Experimento/Grupo (ej: 'Control', 'Exp1_AltaTemp').
|
927 |
+
* Nivel 1: Tipo de Dato ('Tiempo', 'Biomasa', 'Sustrato', 'Producto').
|
928 |
+
* Sub-columnas (Nivel 2 implícito o explícito si hay réplicas): 'R1', 'R2', 'Promedio', etc. para los datos.
|
929 |
+
3. Sube el archivo Excel.
|
930 |
+
4. Selecciona el(los) modelo(s) a ajustar.
|
931 |
+
5. Configura las opciones de simulación y gráficos según sea necesario.
|
932 |
+
6. Haz clic en "Simular y Graficar".
|
933 |
+
7. Revisa los resultados en la pestaña "Resultados".
|
934 |
+
**Modos de Análisis:**
|
935 |
+
* `independent`: Analiza cada grupo de columnas (Nivel 0 del encabezado) en cada hoja como un experimento independiente.
|
936 |
+
* `average`: Promedia todas las réplicas de Biomasa, Sustrato y Producto por hoja y ajusta un único modelo a estos promedios.
|
937 |
+
Se usa el primer 'Tiempo' encontrado o un 'Tiempo' promedio si está estructurado así.
|
938 |
+
* `combinado`: Similar a `average`, pero presenta los tres componentes (Biomasa, Sustrato, Producto) en un único gráfico con múltiples ejes Y.
|
939 |
+
**Salidas:**
|
940 |
+
* Galería de imágenes con los gráficos de ajuste.
|
941 |
+
* Tabla comparativa con R² y RMSE para cada modelo y componente.
|
942 |
+
* Opciones para exportar la tabla (Excel, CSV), los parámetros del modelo (Excel) y las imágenes (ZIP).
|
943 |
""")
|
944 |
gr.Markdown(r"""
|
945 |
+
## Modelos Matemáticos para Bioprocesos
|
946 |
+
|
947 |
+
### 1. Modelo Logístico (Verhulst)
|
948 |
+
Describe el crecimiento de la biomasa ($X$) limitado por la capacidad de carga ($X_m$).
|
949 |
+
$$ \frac{dX}{dt} = \mu_m X \left(1 - \frac{X}{X_m}\right) $$
|
950 |
+
Solución integral:
|
951 |
+
$$ X(t) = \frac{X_0 X_m e^{\mu_m t}}{X_m - X_0 + X_0 e^{\mu_m t}} $$
|
952 |
+
Parámetros: $X_0$ (biomasa inicial), $X_m$ (biomasa máxima), $\mu_m$ (tasa de crecimiento específica máxima).
|
953 |
+
|
954 |
+
### 2. Modelo de Gompertz Modificado
|
955 |
+
Modelo sigmoidal usado frecuentemente para describir crecimiento con una fase de latencia ($\lambda$).
|
956 |
+
$$ X(t) = X_m \exp\left(-\exp\left(\frac{\mu_m e}{X_m}(\lambda - t) + 1\right)\right) $$
|
957 |
+
Parámetros: $X_m$ (biomasa máxima), $\mu_m$ (tasa de crecimiento específica máxima), $\lambda$ (fase de latencia).
|
958 |
+
Nota: La biomasa inicial $X_0$ es $X(t=0)$.
|
959 |
+
|
960 |
+
### 3. Modelo de Moser
|
961 |
+
Modelo que relaciona la tasa de crecimiento con la concentración de sustrato limitante. La forma integrada aquí presentada es una simplificación que no modela explícitamente $S$.
|
962 |
+
$$ \frac{dX}{dt} = \mu_m \left(1 - \frac{X}{X_m}\right) X \quad (\text{forma simplificada para } X(t)) $$
|
963 |
+
La ecuación usada para el ajuste de $X(t)$ es:
|
964 |
+
$$ X(t) = X_m (1 - \exp(-\mu_m (t - K_s))) $$
|
965 |
+
Donde $K_s$ actúa como un parámetro de "tiempo de inicio" o "lag".
|
966 |
+
Parámetros: $X_m$ (biomasa máxima), $\mu_m$ (tasa de crecimiento), $K_s$ (constante de afinidad/lag).
|
967 |
+
|
968 |
+
### 4. Modelo de Baranyi-Roberts
|
969 |
+
Modelo más complejo que incluye ajuste fisiológico de los microorganismos.
|
970 |
+
$$ X(t) = X_m \frac{\exp(\mu_m A(t))}{\exp(\mu_m A(t)) + \frac{X_m}{X_0} - 1} $$
|
971 |
+
Donde $A(t) = t + \frac{1}{\mu_m} \ln\left(e^{-\mu_m t} + e^{-\mu_m \lambda} - e^{-\mu_m (t+\lambda)}\right)$.
|
972 |
+
Parámetros: $X_0$ (biomasa inicial), $X_m$ (biomasa máxima), $\mu_m$ (tasa de crecimiento específica máxima), $\lambda$ (fase de latencia).
|
973 |
+
|
974 |
+
### Modelos de Consumo de Sustrato y Formación de Producto (Luedeking-Piret)
|
975 |
+
Para sustrato ($S$) y producto ($P$):
|
976 |
+
$$ \frac{dS}{dt} = -Y_{X/S} \frac{dX}{dt} - m_S X \quad \implies S(t) = S_0 - p(X(t)-X_0) - q \int_0^t X(\tau)d\tau $$
|
977 |
+
$$ \frac{dP}{dt} = Y_{P/X} \frac{dX}{dt} + m_P X \quad \implies P(t) = P_0 + \alpha(X(t)-X_0) + \beta \int_0^t X(\tau)d\tau $$
|
978 |
+
Parámetros: $S_0, P_0$ (concentraciones iniciales), $p, Y_{X/S}$ (rendimiento biomasa/sustrato), $q, m_S$ (mantenimiento para sustrato), $\alpha, Y_{P/X}$ (rendimiento producto/biomasa), $\beta, m_P$ (mantenimiento para producto).
|
979 |
""")
|
980 |
with gr.Tab("Simulación"):
|
981 |
with gr.Row():
|
|
|
1014 |
simulate_btn = gr.Button("Simular y Graficar", variant="primary")
|
1015 |
|
1016 |
with gr.Tab("Resultados"):
|
1017 |
+
status_message_ui = gr.Textbox(label="Estado del Procesamiento", interactive=False, lines=3, max_lines=10)
|
1018 |
output_gallery_ui = gr.Gallery(label="Resultados Gráficos", columns=[2,1], height=600, object_fit="contain", preview=True)
|
1019 |
output_table_ui = gr.Dataframe(
|
1020 |
label="Tabla Comparativa de Modelos",
|
1021 |
headers=["Experimento","Modelo","R² Biomasa","RMSE Biomasa","R² Sustrato","RMSE Sustrato","R² Producto","RMSE Producto"],
|
1022 |
interactive=False,
|
1023 |
wrap=True
|
|
|
1024 |
)
|
1025 |
state_df_ui = gr.State(pd.DataFrame())
|
1026 |
state_params_ui = gr.State({})
|