C2MV commited on
Commit
1f21647
·
verified ·
1 Parent(s): 3086146

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -41
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 # Critical: if biomass fit fails, no plot.
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
- # Fallback to curve_fit results on fine grid if ODE failed
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) # Use original y_pred_biomass_fit for this
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: # Biomass fit itself failed, so predictions are NaN
523
  y_pred_b,y_pred_s,y_pred_p = (np.full_like(time_curves,np.nan) for _ in range(3))
524
- else: # Not using ODE, ensure curve_fit results are plotted on fine grid
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: # Biomass fit failed
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
- # FIXED: Compute formatted strings separately
568
- r2_str = f"{r2_val:.3f}" if np.isfinite(r2_val) else 'N/A'
569
- rmse_str = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else 'N/A'
570
- txt = f"{p_txt}\nR²={r2_str}\nRMSE={rmse_str}"
 
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: # No ODE
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
- # FIXED: Compute formatted strings separately
677
- r2_str = f"{r2_val:.3f}" if np.isfinite(r2_val) else 'N/A'
678
- rmse_str = f"{rmse_val:.3f}" if np.isfinite(rmse_val) else 'N/A'
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
- # Extract Time for this specific experiment group
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): # Time itself has replicates (unusual)
754
  time_exp = time_data_for_exp.iloc[:,0].dropna().astype(float).values
755
- else: # Time is a single series
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): # Has replicates (e.g., R1, R2 columns under 'Biomasa')
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 # Single numeric series
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([]) # Ensure it's an 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) # This now gets filtered positive biomass
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
- # This explicit check for average mode helps clarify why fitting might be skipped for the whole sheet average.
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 # Messages already cover issues
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... (etc., same as before)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
934
  """)
935
  gr.Markdown(r"""
936
- ## Modelos Matemáticos para Bioprocesos ... (etc., same as before)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) # Increased lines for messages
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({})