C2MV commited on
Commit
6f25629
·
verified ·
1 Parent(s): 7b70a20

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +382 -777
app.py CHANGED
@@ -17,13 +17,11 @@ from unittest.mock import MagicMock
17
  from dataclasses import dataclass
18
  from enum import Enum
19
  import json
20
- import base64
21
 
22
  from PIL import Image
23
  import gradio as gr
24
  import plotly.graph_objects as go
25
  from plotly.subplots import make_subplots
26
- import plotly.io as pio
27
  import numpy as np
28
  import pandas as pd
29
  import matplotlib.pyplot as plt
@@ -70,11 +68,7 @@ TRANSLATIONS = {
70
  "language": "Idioma",
71
  "theory": "Teoría y Modelos",
72
  "guide": "Guía de Uso",
73
- "api_docs": "Documentación API",
74
- "individual": "Individual",
75
- "average": "Promedio",
76
- "combined": "Combinado",
77
- "config": "Configuración"
78
  },
79
  Language.EN: {
80
  "title": "🔬 Bioprocess Kinetics Analyzer",
@@ -97,11 +91,7 @@ TRANSLATIONS = {
97
  "language": "Language",
98
  "theory": "Theory and Models",
99
  "guide": "User Guide",
100
- "api_docs": "API Documentation",
101
- "individual": "Individual",
102
- "average": "Average",
103
- "combined": "Combined",
104
- "config": "Configuration"
105
  },
106
  }
107
 
@@ -110,6 +100,9 @@ C_TIME = 'tiempo'
110
  C_BIOMASS = 'biomass'
111
  C_SUBSTRATE = 'substrate'
112
  C_PRODUCT = 'product'
 
 
 
113
  COMPONENTS = [C_BIOMASS, C_SUBSTRATE, C_PRODUCT]
114
 
115
  # --- SISTEMA DE TEMAS ---
@@ -548,7 +541,6 @@ class BioprocessFitter:
548
  self.data_time: Optional[np.ndarray] = None
549
  self.data_means: Dict[str, Optional[np.ndarray]] = {c: None for c in COMPONENTS}
550
  self.data_stds: Dict[str, Optional[np.ndarray]] = {c: None for c in COMPONENTS}
551
- self.raw_data: Dict[str, List[np.ndarray]] = {c: [] for c in COMPONENTS} # Para análisis individual
552
 
553
  def _get_biomass_at_t(self, t: np.ndarray, p: List[float]) -> np.ndarray:
554
  return self.model.model_function(t, *p)
@@ -587,24 +579,20 @@ class BioprocessFitter:
587
  self.data_time = df[time_col].dropna().to_numpy()
588
  min_len = len(self.data_time)
589
 
590
- def extract(name: str) -> Tuple[np.ndarray, np.ndarray, List[np.ndarray]]:
591
  cols = [c for c in df.columns if c[1].strip().lower() == name.lower()]
592
- if not cols: return np.array([]), np.array([]), []
593
  reps = [df[c].dropna().values[:min_len] for c in cols]
594
  reps = [r for r in reps if len(r) == min_len]
595
- if not reps: return np.array([]), np.array([]), []
596
  arr = np.array(reps)
597
  mean = np.mean(arr, axis=0)
598
  std = np.std(arr, axis=0, ddof=1) if arr.shape[0] > 1 else np.zeros_like(mean)
599
- return mean, std, reps
600
 
601
- # Extraer datos con réplicas individuales
602
- for comp, name in [(C_BIOMASS, 'Biomasa'), (C_SUBSTRATE, 'Sustrato'), (C_PRODUCT, 'Producto')]:
603
- mean, std, reps = extract(name)
604
- self.data_means[comp] = mean
605
- self.data_stds[comp] = std
606
- self.raw_data[comp] = reps
607
-
608
  except (IndexError, KeyError) as e:
609
  raise ValueError(f"Estructura de DataFrame inválida. Error: {e}")
610
 
@@ -757,90 +745,6 @@ class BioprocessFitter:
757
  if self.params[C_PRODUCT]:
758
  P = self.product(t_fine, *list(self.params[C_PRODUCT].values()), bio_p)
759
  return X, S, P
760
-
761
- def plot_individual_or_combined(self, cfg, mode):
762
- """Crea gráficos individuales o combinados con Matplotlib/Seaborn"""
763
- t_exp, t_fine = cfg['time_exp'], self._generate_fine_time_grid(cfg['time_exp'])
764
- X_m, S_m, P_m = self.get_model_curves_for_plot(t_fine, cfg.get('use_differential', False))
765
-
766
- sns.set_style(cfg.get('style', 'whitegrid'))
767
-
768
- if mode == 'average':
769
- fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 15), sharex=True)
770
- fig.suptitle(f"Análisis: {cfg.get('exp_name', '')} ({self.model.display_name})", fontsize=16)
771
- axes = [ax1, ax2, ax3]
772
- else:
773
- fig, ax1 = plt.subplots(figsize=(12, 8))
774
- fig.suptitle(f"Análisis: {cfg.get('exp_name', '')} ({self.model.display_name})", fontsize=16)
775
- ax2 = ax1.twinx()
776
- ax3 = ax1.twinx()
777
- ax3.spines["right"].set_position(("axes", 1.18))
778
- axes = [ax1, ax2, ax3]
779
-
780
- data_map = {C_BIOMASS: X_m, C_SUBSTRATE: S_m, C_PRODUCT: P_m}
781
- comb_styles = {
782
- C_BIOMASS: {'c': '#0072B2', 'mc': '#56B4E9', 'm': 'o', 'ls': '-'},
783
- C_SUBSTRATE: {'c': '#009E73', 'mc': '#34E499', 'm': 's', 'ls': '--'},
784
- C_PRODUCT: {'c': '#D55E00', 'mc': '#F0E442', 'm': '^', 'ls': '-.'}
785
- }
786
-
787
- for ax, comp in zip(axes, COMPONENTS):
788
- ylabel = cfg.get('axis_labels', {}).get(f'{comp}_label', comp.capitalize())
789
- data = cfg.get(f'{comp}_exp')
790
- std = cfg.get(f'{comp}_std')
791
- model_data = data_map.get(comp)
792
-
793
- if mode == 'combined':
794
- s = comb_styles[comp]
795
- pc, lc, ms, ls = s['c'], s['mc'], s['m'], s['ls']
796
- else:
797
- pc = cfg.get(f'{comp}_point_color')
798
- lc = cfg.get(f'{comp}_line_color')
799
- ms = cfg.get(f'{comp}_marker_style')
800
- ls = cfg.get(f'{comp}_line_style')
801
-
802
- ax_c = pc if mode == 'combined' else 'black'
803
- ax.set_ylabel(ylabel, color=ax_c)
804
- ax.tick_params(axis='y', labelcolor=ax_c)
805
-
806
- if data is not None and len(data) > 0:
807
- if cfg.get('show_error_bars') and std is not None and np.any(std > 0):
808
- ax.errorbar(t_exp, data, yerr=std, fmt=ms, color=pc,
809
- label=f'{comp.capitalize()} (Datos)',
810
- capsize=cfg.get('error_cap_size', 3),
811
- elinewidth=cfg.get('error_line_width', 1))
812
- else:
813
- ax.plot(t_exp, data, ls='', marker=ms, color=pc,
814
- label=f'{comp.capitalize()} (Datos)')
815
-
816
- if model_data is not None and len(model_data) > 0:
817
- ax.plot(t_fine, model_data, ls=ls, color=lc,
818
- label=f'{comp.capitalize()} (Modelo)')
819
-
820
- if mode == 'average' and cfg.get('show_legend', True):
821
- ax.legend(loc=cfg.get('legend_pos', 'best'))
822
-
823
- if mode == 'average' and cfg.get('show_params', True) and self.params[comp]:
824
- decs = cfg.get('decimal_places', 3)
825
- p_txt = '\n'.join([f"{k}={format_number(v, decs)}" for k, v in self.params[comp].items()])
826
- full_txt = f"{p_txt}\nR²={format_number(self.r2.get(comp, 0), 3)}, RMSE={format_number(self.rmse.get(comp, 0), 3)}"
827
- pos_x, ha = (0.95, 'right') if 'right' in cfg.get('params_pos', 'upper right') else (0.05, 'left')
828
- ax.text(pos_x, 0.95, full_txt, transform=ax.transAxes, va='top', ha=ha,
829
- bbox=dict(boxstyle='round,pad=0.4', fc='wheat', alpha=0.7))
830
-
831
- if mode == 'combined' and cfg.get('show_legend', True):
832
- h1, l1 = axes[0].get_legend_handles_labels()
833
- h2, l2 = axes[1].get_legend_handles_labels()
834
- h3, l3 = axes[2].get_legend_handles_labels()
835
- axes[0].legend(handles=h1+h2+h3, labels=l1+l2+l3, loc=cfg.get('legend_pos', 'best'))
836
-
837
- axes[-1].set_xlabel(cfg.get('axis_labels', {}).get('x_label', 'Tiempo'))
838
- plt.tight_layout()
839
-
840
- if mode == 'combined':
841
- fig.subplots_adjust(right=0.8)
842
-
843
- return fig
844
 
845
  # --- FUNCIONES AUXILIARES ---
846
 
@@ -859,456 +763,235 @@ def format_number(value: Any, decimals: int) -> str:
859
 
860
  return str(round(value, decimals))
861
 
862
- # --- FUNCIONES DE PLOTEO MEJORADAS ---
863
 
864
- def plot_model_comparison_matplotlib(plot_config: Dict, models_results: List[Dict]) -> plt.Figure:
865
- """Crea un gráfico de comparación de modelos estático usando Matplotlib/Seaborn"""
 
866
  time_exp = plot_config['time_exp']
867
- time_fine = BioprocessFitter(list(AVAILABLE_MODELS.values())[0])._generate_fine_time_grid(time_exp)
868
- num_models = len(models_results)
869
-
870
- palettes = {
871
- C_BIOMASS: sns.color_palette("Blues", num_models),
872
- C_SUBSTRATE: sns.color_palette("Greens", num_models),
873
- C_PRODUCT: sns.color_palette("Reds", num_models)
874
- }
875
- line_styles = ['-', '--', '-.', ':']
876
-
877
- sns.set_style(plot_config.get('style', 'whitegrid'))
878
- fig, ax1 = plt.subplots(figsize=(12, 8))
879
-
880
- # Configuración de los 3 ejes Y
881
- ax1.set_xlabel(plot_config['axis_labels']['x_label'])
882
- ax1.set_ylabel(plot_config['axis_labels']['biomass_label'], color="navy", fontsize=12)
883
- ax1.tick_params(axis='y', labelcolor="navy")
884
- ax2 = ax1.twinx()
885
- ax3 = ax1.twinx()
886
- ax3.spines["right"].set_position(("axes", 1.22))
887
- ax2.set_ylabel(plot_config['axis_labels']['substrate_label'], color="darkgreen", fontsize=12)
888
- ax2.tick_params(axis='y', labelcolor="darkgreen")
889
- ax3.set_ylabel(plot_config['axis_labels']['product_label'], color="darkred", fontsize=12)
890
- ax3.tick_params(axis='y', labelcolor="darkred")
891
-
892
- # Dibujar datos experimentales
893
- data_markers = {C_BIOMASS: 'o', C_SUBSTRATE: 's', C_PRODUCT: '^'}
894
- for ax, key, color, face in [(ax1, C_BIOMASS, 'navy', 'skyblue'),
895
- (ax2, C_SUBSTRATE, 'darkgreen', 'lightgreen'),
896
- (ax3, C_PRODUCT, 'darkred', 'lightcoral')]:
897
- data_exp = plot_config.get(f'{key}_exp')
898
- data_std = plot_config.get(f'{key}_std')
899
- if data_exp is not None:
900
- if plot_config.get('show_error_bars') and data_std is not None and np.any(data_std > 0):
901
- ax.errorbar(time_exp, data_exp, yerr=data_std, fmt=data_markers[key],
902
- color=color, label=f'{key.capitalize()} (Datos)', zorder=10,
903
- markersize=8, markerfacecolor=face, markeredgecolor=color,
904
- capsize=plot_config.get('error_cap_size', 3),
905
- elinewidth=plot_config.get('error_line_width', 1))
906
- else:
907
- ax.plot(time_exp, data_exp, ls='', marker=data_markers[key],
908
- label=f'{key.capitalize()} (Datos)', zorder=10, ms=8,
909
- mfc=face, mec=color, mew=1.5)
910
-
911
- # Dibujar curvas de los modelos
912
- for i, res in enumerate(models_results):
913
- ls = line_styles[i % len(line_styles)]
914
- model_info = AVAILABLE_MODELS.get(res["name"], MagicMock(display_name=res["name"]))
915
- model_display_name = model_info.display_name
916
- for key_short, ax, name_long in [('X', ax1, C_BIOMASS), ('S', ax2, C_SUBSTRATE), ('P', ax3, C_PRODUCT)]:
917
- if res.get(key_short) is not None:
918
- ax.plot(time_fine, res[key_short], color=palettes[name_long][i], ls=ls,
919
- label=f'{name_long.capitalize()} ({model_display_name})', alpha=0.9)
920
-
921
- fig.subplots_adjust(left=0.3, right=0.78, top=0.92,
922
- bottom=0.35 if plot_config.get('show_params') else 0.1)
923
-
924
- if plot_config.get('show_legend'):
925
- h1, l1 = ax1.get_legend_handles_labels()
926
- h2, l2 = ax2.get_legend_handles_labels()
927
- h3, l3 = ax3.get_legend_handles_labels()
928
- fig.legend(h1 + h2 + h3, l1 + l2 + l3, loc='center left',
929
- bbox_to_anchor=(0.0, 0.5), fancybox=True, shadow=True, fontsize='small')
930
-
931
- if plot_config.get('show_params'):
932
- total_width = 0.95
933
- box_width = total_width / num_models
934
- start_pos = (1.0 - total_width) / 2
935
- for i, res in enumerate(models_results):
936
- model_info = AVAILABLE_MODELS.get(res["name"], MagicMock(display_name=res["name"]))
937
- text = f"**{model_info.display_name}**\n" + _generate_model_param_text(res, plot_config.get('decimal_places', 3))
938
- fig.text(start_pos + i * box_width, 0.01, text, transform=fig.transFigure,
939
- fontsize=7.5, va='bottom', ha='left',
940
- bbox=dict(boxstyle='round,pad=0.4', fc='ivory', ec='gray', alpha=0.9))
941
-
942
- fig.suptitle(f"Comparación de Modelos: {plot_config.get('exp_name', '')}", fontsize=16)
943
- return fig
944
-
945
- def plot_model_comparison_plotly(plot_config: Dict, models_results: List[Dict]) -> go.Figure:
946
- """Crea un gráfico de comparación de modelos interactivo usando Plotly"""
947
- fig = go.Figure()
948
- time_exp = plot_config['time_exp']
949
- time_fine = BioprocessFitter(list(AVAILABLE_MODELS.values())[0])._generate_fine_time_grid(time_exp)
950
- num_models = len(models_results)
951
-
952
- palettes = {
953
- C_BIOMASS: sns.color_palette("Blues", n_colors=num_models).as_hex(),
954
- C_SUBSTRATE: sns.color_palette("Greens", n_colors=num_models).as_hex(),
955
- C_PRODUCT: sns.color_palette("Reds", n_colors=num_models).as_hex()
956
- }
957
- line_styles = ['solid', 'dash', 'dot', 'dashdot']
958
- data_markers = {C_BIOMASS: 'circle-open', C_SUBSTRATE: 'square-open', C_PRODUCT: 'diamond-open'}
959
-
960
- for key, y_axis, color in [(C_BIOMASS, 'y1', 'navy'),
961
- (C_SUBSTRATE, 'y2', 'darkgreen'),
962
- (C_PRODUCT, 'y3', 'darkred')]:
963
- data_exp = plot_config.get(f'{key}_exp')
964
- data_std = plot_config.get(f'{key}_std')
965
- if data_exp is not None:
966
- error_y_config = dict(type='data', array=data_std, visible=True) if plot_config.get('show_error_bars') and data_std is not None and np.any(data_std > 0) else None
967
- fig.add_trace(go.Scatter(
968
- x=time_exp, y=data_exp, mode='markers',
969
- name=f'{key.capitalize()} (Datos)',
970
- marker=dict(color=color, size=10, symbol=data_markers[key], line=dict(width=2)),
971
- error_y=error_y_config, yaxis=y_axis, legendgroup="data"))
972
-
973
- for i, res in enumerate(models_results):
974
- ls = line_styles[i % len(line_styles)]
975
- model_display_name = AVAILABLE_MODELS.get(res["name"], MagicMock(display_name=res["name"])).display_name
976
- if res.get('X') is not None:
977
- fig.add_trace(go.Scatter(x=time_fine, y=res['X'], mode='lines',
978
- name=f'Biomasa ({model_display_name})',
979
- line=dict(color=palettes[C_BIOMASS][i], dash=ls),
980
- legendgroup=res["name"]))
981
- if res.get('S') is not None:
982
- fig.add_trace(go.Scatter(x=time_fine, y=res['S'], mode='lines',
983
- name=f'Sustrato ({model_display_name})',
984
- line=dict(color=palettes[C_SUBSTRATE][i], dash=ls),
985
- yaxis='y2', legendgroup=res["name"]))
986
- if res.get('P') is not None:
987
- fig.add_trace(go.Scatter(x=time_fine, y=res['P'], mode='lines',
988
- name=f'Producto ({model_display_name})',
989
- line=dict(color=palettes[C_PRODUCT][i], dash=ls),
990
- yaxis='y3', legendgroup=res["name"]))
991
-
992
- if plot_config.get('show_params'):
993
- x_positions = np.linspace(0, 1, num_models * 2 + 1)[1::2]
994
- for i, res in enumerate(models_results):
995
- model_display_name = AVAILABLE_MODELS.get(res["name"], MagicMock(display_name=res["name"])).display_name
996
- text = f"<b>{model_display_name}</b><br>" + _generate_model_param_text(res, plot_config.get('decimal_places', 3)).replace('\n', '<br>')
997
- fig.add_annotation(text=text, align='left', showarrow=False, xref='paper',
998
- yref='paper', x=x_positions[i], y=-0.35, bordercolor='gray',
999
- borderwidth=1, bgcolor='ivory', opacity=0.9)
1000
-
1001
- fig.update_layout(
1002
- title=f"Comparación de Modelos (Interactivo): {plot_config.get('exp_name', '')}",
1003
- xaxis=dict(domain=[0.18, 0.82]),
1004
- yaxis=dict(title=plot_config['axis_labels']['biomass_label'], titlefont=dict(color='navy'),
1005
- tickfont=dict(color='navy')),
1006
- yaxis2=dict(title=plot_config['axis_labels']['substrate_label'], titlefont=dict(color='darkgreen'),
1007
- tickfont=dict(color='darkgreen'), overlaying='y', side='right'),
1008
- yaxis3=dict(title=plot_config['axis_labels']['product_label'], titlefont=dict(color='darkred'),
1009
- tickfont=dict(color='darkred'), overlaying='y', side='right', position=0.85),
1010
- legend=dict(traceorder="grouped", yanchor="middle", y=0.5, xanchor="right", x=-0.15),
1011
- margin=dict(l=200, r=150, b=250 if plot_config.get('show_params') else 80, t=80),
1012
- template="plotly_white" if plot_config.get('theme', 'light') == 'light' else "plotly_dark",
1013
- showlegend=plot_config.get('show_legend', True)
1014
- )
1015
- return fig
1016
-
1017
- def _generate_model_param_text(result: Dict, decimals: int) -> str:
1018
- """Genera el texto formateado de los parámetros para las cajas de anotación"""
1019
- text = ""
1020
- for comp in COMPONENTS:
1021
- if params := result.get('params', {}).get(comp):
1022
- p_str = ', '.join([f"{k}={format_number(v, decimals)}" for k, v in params.items()])
1023
- r2 = result.get('r2', {}).get(comp, 0)
1024
- rmse = result.get('rmse', {}).get(comp, 0)
1025
- text += f"{comp[:4].capitalize()}: {p_str}\n(R²={format_number(r2, 3)}, RMSE={format_number(rmse, 3)})\n"
1026
- return text.strip()
1027
-
1028
- # --- FUNCIONES DE DESCARGA Y REPORTES ---
1029
-
1030
- def create_zip_file(image_list: List[Any]) -> Optional[str]:
1031
- """Crea un archivo ZIP con todas las imágenes"""
1032
- if not image_list:
1033
- gr.Warning("No hay gráficos para descargar.")
1034
- return None
1035
- try:
1036
- zip_buffer = io.BytesIO()
1037
- with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
1038
- for i, fig in enumerate(image_list):
1039
- buf = io.BytesIO()
1040
- if isinstance(fig, go.Figure):
1041
- buf.write(fig.to_image(format="png", scale=2, engine="kaleido"))
1042
- elif isinstance(fig, plt.Figure):
1043
- fig.savefig(buf, format='png', dpi=200, bbox_inches='tight')
1044
- plt.close(fig)
1045
- elif isinstance(fig, Image.Image):
1046
- fig.save(buf, 'PNG')
1047
- else:
1048
- continue
1049
- buf.seek(0)
1050
- zf.writestr(f"grafico_{i+1}.png", buf.read())
1051
-
1052
- with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp:
1053
- tmp.write(zip_buffer.getvalue())
1054
- return tmp.name
1055
- except Exception as e:
1056
- traceback.print_exc()
1057
- gr.Error(f"Error al crear el archivo ZIP: {e}")
1058
- return None
1059
-
1060
- def create_word_report(image_list: List[Any], table_df: pd.DataFrame, decimals: int) -> Optional[str]:
1061
- """Crea un reporte en Word con imágenes y tablas"""
1062
- if not image_list and (table_df is None or table_df.empty):
1063
- gr.Warning("No hay datos ni gráficos para crear el reporte.")
1064
- return None
1065
- try:
1066
- doc = Document()
1067
- doc.add_heading('Reporte de Análisis de Cinéticas', 0)
1068
-
1069
- # Resumen ejecutivo
1070
- doc.add_heading('Resumen Ejecutivo', level=1)
1071
- doc.add_paragraph(f'Fecha del análisis: {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M")}')
1072
- doc.add_paragraph(f'Total de experimentos analizados: {len(table_df["Experimento"].unique()) if table_df is not None and not table_df.empty else 0}')
1073
- doc.add_paragraph(f'Modelos utilizados: {", ".join(table_df["Modelo"].unique()) if table_df is not None and not table_df.empty else "N/A"}')
1074
-
1075
- if table_df is not None and not table_df.empty:
1076
- doc.add_heading('Tabla de Resultados', level=1)
1077
- table = doc.add_table(rows=1, cols=len(table_df.columns), style='Table Grid')
1078
- for i, col in enumerate(table_df.columns):
1079
- table.cell(0, i).text = str(col)
1080
- for _, row in table_df.iterrows():
1081
- cells = table.add_row().cells
1082
- for i, val in enumerate(row):
1083
- cells[i].text = str(format_number(val, decimals))
1084
-
1085
- if image_list:
1086
- doc.add_page_break()
1087
- doc.add_heading('Gráficos Generados', level=1)
1088
- for i, fig in enumerate(image_list):
1089
- buf = io.BytesIO()
1090
- if isinstance(fig, go.Figure):
1091
- buf.write(fig.to_image(format="png", scale=2, engine="kaleido"))
1092
- elif isinstance(fig, plt.Figure):
1093
- fig.savefig(buf, format='png', dpi=200, bbox_inches='tight')
1094
- plt.close(fig)
1095
- elif isinstance(fig, Image.Image):
1096
- fig.save(buf, 'PNG')
1097
- else:
1098
- continue
1099
- buf.seek(0)
1100
- doc.add_paragraph(f'Gráfico {i+1}', style='Heading 3')
1101
- doc.add_picture(buf, width=Inches(6.0))
1102
- doc.add_paragraph('') # Espacio entre imágenes
1103
-
1104
- with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
1105
- doc.save(tmp.name)
1106
- return tmp.name
1107
- except Exception as e:
1108
- traceback.print_exc()
1109
- gr.Error(f"Error al crear el reporte de Word: {e}")
1110
- return None
1111
-
1112
- def create_pdf_report(image_list: List[Any], table_df: pd.DataFrame, decimals: int) -> Optional[str]:
1113
- """Crea un reporte en PDF con imágenes y tablas"""
1114
- if not image_list and (table_df is None or table_df.empty):
1115
- gr.Warning("No hay datos ni gráficos para crear el reporte.")
1116
- return None
1117
- try:
1118
- pdf = FPDF()
1119
- pdf.set_auto_page_break(auto=True, margin=15)
1120
- pdf.add_page()
1121
- pdf.set_font("Helvetica", 'B', 16)
1122
- pdf.cell(0, 10, 'Reporte de Análisis de Cinéticas', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
1123
-
1124
- # Resumen ejecutivo
1125
- pdf.ln(10)
1126
- pdf.set_font("Helvetica", '', 10)
1127
- pdf.cell(0, 10, f'Fecha del análisis: {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M")}',
1128
- new_x=XPos.LMARGIN, new_y=YPos.NEXT)
1129
 
1130
- if table_df is not None and not table_df.empty:
1131
- pdf.ln(10)
1132
- pdf.set_font("Helvetica", 'B', 12)
1133
- pdf.cell(0, 10, 'Tabla de Resultados', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
1134
- pdf.set_font("Helvetica", 'B', 8)
1135
-
1136
- effective_page_width = pdf.w - 2 * pdf.l_margin
1137
- num_cols = len(table_df.columns)
1138
- col_width = effective_page_width / num_cols if num_cols > 0 else 0
1139
 
1140
- if num_cols > 15:
1141
- pdf.set_font_size(6)
1142
- elif num_cols > 10:
1143
- pdf.set_font_size(7)
1144
-
1145
- for col in table_df.columns:
1146
- pdf.cell(col_width, 10, str(col), border=1, align='C')
1147
- pdf.ln()
 
 
1148
 
1149
- pdf.set_font("Helvetica", '', 7)
1150
- if num_cols > 15:
1151
- pdf.set_font_size(5)
1152
- elif num_cols > 10:
1153
- pdf.set_font_size(6)
1154
-
1155
- for _, row in table_df.iterrows():
1156
- for val in row:
1157
- pdf.cell(col_width, 10, str(format_number(val, decimals)), border=1, align='R')
1158
- pdf.ln()
1159
 
1160
- if image_list:
1161
- for i, fig in enumerate(image_list):
1162
- pdf.add_page()
1163
- pdf.set_font("Helvetica", 'B', 12)
1164
- pdf.cell(0, 10, f'Gráfico {i+1}', new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
1165
- pdf.ln(5)
 
 
 
 
 
1166
 
1167
- buf = io.BytesIO()
1168
- if isinstance(fig, go.Figure):
1169
- buf.write(fig.to_image(format="png", scale=2, engine="kaleido"))
1170
- elif isinstance(fig, plt.Figure):
1171
- fig.savefig(buf, format='png', dpi=200, bbox_inches='tight')
1172
- plt.close(fig)
1173
- elif isinstance(fig, Image.Image):
1174
- fig.save(buf, 'PNG')
1175
  else:
1176
- continue
1177
-
1178
- buf.seek(0)
1179
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
1180
- tmp_img.write(buf.read())
1181
- pdf.image(tmp_img.name, x=None, y=None, w=pdf.w - 20)
1182
- os.remove(tmp_img.name)
1183
-
1184
- pdf_bytes = pdf.output()
1185
- with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
1186
- tmp.write(pdf_bytes)
1187
- return tmp.name
1188
- except Exception as e:
1189
- traceback.print_exc()
1190
- gr.Error(f"Error al crear el reporte PDF: {e}")
1191
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
 
1193
  # --- FUNCIÓN PRINCIPAL DE ANÁLISIS ---
1194
-
1195
- def run_analysis(file, model_names, mode, engine, exp_names, settings):
1196
- """Ejecuta el análisis completo con todos los modos"""
1197
- if not file:
1198
- return [], pd.DataFrame(), "Error: Sube un archivo Excel.", pd.DataFrame()
1199
- if not model_names:
1200
- return [], pd.DataFrame(), "Error: Selecciona un modelo.", pd.DataFrame()
1201
 
1202
- try:
1203
  xls = pd.ExcelFile(file.name)
1204
- except Exception as e:
1205
- return [], pd.DataFrame(), f"Error al leer archivo: {e}", pd.DataFrame()
1206
 
1207
- figs = []
1208
- results_data = []
1209
- msgs = []
1210
 
1211
- exp_list = [n.strip() for n in exp_names.split('\n') if n.strip()]
1212
 
1213
  for i, sheet in enumerate(xls.sheet_names):
1214
  exp_name = exp_list[i] if i < len(exp_list) else f"Hoja '{sheet}'"
1215
-
1216
  try:
1217
  df = pd.read_excel(xls, sheet_name=sheet, header=[0,1])
1218
  reader = BioprocessFitter(list(AVAILABLE_MODELS.values())[0])
1219
  reader.process_data_from_df(df)
1220
 
1221
- if reader.data_time is None:
1222
  msgs.append(f"WARN: Sin datos de tiempo en '{sheet}'.")
1223
  continue
1224
-
1225
- cfg = settings.copy()
1226
- cfg.update({'exp_name': exp_name, 'time_exp': reader.data_time})
1227
 
1228
- for c in COMPONENTS:
1229
- cfg[f'{c}_exp'] = reader.data_means[c]
1230
- cfg[f'{c}_std'] = reader.data_stds[c]
 
 
 
 
 
 
1231
 
1232
  t_fine = reader._generate_fine_time_grid(reader.data_time)
1233
- plot_results = []
1234
 
1235
  for m_name in model_names:
1236
- if m_name not in AVAILABLE_MODELS:
1237
  msgs.append(f"WARN: Modelo '{m_name}' no disponible.")
1238
  continue
1239
 
1240
- fitter = BioprocessFitter(AVAILABLE_MODELS[m_name], maxfev=int(settings.get('maxfev', 50000)))
 
 
 
 
1241
  fitter.data_time = reader.data_time
1242
  fitter.data_means = reader.data_means
1243
  fitter.data_stds = reader.data_stds
1244
- fitter.raw_data = reader.raw_data
1245
  fitter.fit_all_models()
1246
 
1247
- # Guardar resultados numéricos
1248
  row = {'Experimento': exp_name, 'Modelo': fitter.model.display_name}
1249
  for c in COMPONENTS:
1250
- if fitter.params[c]:
1251
  row.update({f'{c.capitalize()}_{k}': v for k, v in fitter.params[c].items()})
1252
  row[f'R2_{c.capitalize()}'] = fitter.r2.get(c)
1253
  row[f'RMSE_{c.capitalize()}'] = fitter.rmse.get(c)
1254
  row[f'MAE_{c.capitalize()}'] = fitter.mae.get(c)
1255
  row[f'AIC_{c.capitalize()}'] = fitter.aic.get(c)
1256
  row[f'BIC_{c.capitalize()}'] = fitter.bic.get(c)
 
1257
  results_data.append(row)
1258
 
1259
- # Generar gráficos según el modo
1260
- if mode in ["average", "combined"]:
1261
- if hasattr(fitter, 'plot_individual_or_combined'):
1262
- figs.append(fitter.plot_individual_or_combined(cfg, mode))
1263
- elif mode == "individual":
1264
- # Crear gráficos para cada réplica
1265
- for rep_idx, rep_data in enumerate(fitter.raw_data[C_BIOMASS]):
1266
- cfg_rep = cfg.copy()
1267
- cfg_rep['exp_name'] = f"{exp_name} - Réplica {rep_idx + 1}"
1268
- for c in COMPONENTS:
1269
- if len(fitter.raw_data[c]) > rep_idx:
1270
- cfg_rep[f'{c}_exp'] = fitter.raw_data[c][rep_idx]
1271
- cfg_rep[f'{c}_std'] = None # No hay std para réplicas individuales
1272
- figs.append(fitter.plot_individual_or_combined(cfg_rep, "average"))
1273
- else:
1274
- # Modo comparación de modelos
1275
- X, S, P = fitter.get_model_curves_for_plot(t_fine, settings.get('use_differential', False))
1276
- plot_results.append({
1277
- 'name': m_name,
1278
- 'X': X,
1279
- 'S': S,
1280
- 'P': P,
1281
- 'params': fitter.params,
1282
- 'r2': fitter.r2,
1283
- 'rmse': fitter.rmse
1284
- })
1285
-
1286
- if mode == "model_comparison" and plot_results:
1287
- plot_func = plot_model_comparison_plotly if engine == 'Plotly (Interactivo)' else plot_model_comparison_matplotlib
1288
- figs.append(plot_func(cfg, plot_results))
1289
 
1290
- except Exception as e:
1291
  msgs.append(f"ERROR en '{sheet}': {e}")
1292
  traceback.print_exc()
1293
 
1294
  msg = "Análisis completado." + ("\n" + "\n".join(msgs) if msgs else "")
1295
  df_res = pd.DataFrame(results_data).dropna(axis=1, how='all')
1296
 
1297
- if not df_res.empty:
1298
- # Ordenar columnas
1299
- id_c = ['Experimento', 'Modelo']
1300
- p_c = sorted([c for c in df_res.columns if '_' in c and not any(m in c for m in ['R2', 'RMSE', 'MAE', 'AIC', 'BIC'])])
1301
- m_c = sorted([c for c in df_res.columns if any(m in c for m in ['R2', 'RMSE', 'MAE', 'AIC', 'BIC'])])
1302
- df_res = df_res[[c for c in id_c + p_c + m_c if c in df_res.columns]]
1303
-
1304
- # Crear DataFrame formateado para UI
1305
- df_ui = df_res.copy()
1306
- for c in df_ui.select_dtypes(include=np.number).columns:
1307
- df_ui[c] = df_ui[c].apply(lambda x: format_number(x, settings.get('decimal_places', 3)) if pd.notna(x) else '')
1308
- else:
1309
- df_ui = pd.DataFrame()
1310
 
1311
- return figs, df_ui, msg, df_res
1312
 
1313
  # --- API ENDPOINTS PARA AGENTES DE IA ---
1314
 
@@ -1400,15 +1083,16 @@ async def predict_kinetics(
1400
  except Exception as e:
1401
  return {"status": "error", "message": str(e)}
1402
 
1403
- # --- INTERFAZ GRADIO COMPLETA ---
1404
 
1405
  def create_gradio_interface() -> gr.Blocks:
1406
- """Crea la interfaz completa con todas las funcionalidades"""
1407
 
1408
  def change_language(lang_key: str) -> Dict:
1409
  """Cambia el idioma de la interfaz"""
1410
  lang = Language[lang_key]
1411
  trans = TRANSLATIONS.get(lang, TRANSLATIONS[Language.ES])
 
1412
  return trans["title"], trans["subtitle"]
1413
 
1414
  # Obtener opciones de modelo
@@ -1442,304 +1126,196 @@ def create_gradio_interface() -> gr.Blocks:
1442
  )
1443
 
1444
  with gr.Tabs() as tabs:
1445
- # --- TAB 1: GUÍA Y FORMATO ---
1446
- with gr.TabItem("1. Guía y Formato de Datos"):
1447
- with gr.Row():
1448
- with gr.Column(scale=2):
1449
- gr.Markdown("""
1450
- ### Bienvenido al Analizador de Cinéticas
1451
- Esta herramienta te permite ajustar modelos matemáticos a tus datos de crecimiento microbiano.
1452
-
1453
- **Pasos a seguir:**
1454
- 1. Prepara tu archivo Excel según el formato especificado a la derecha.
1455
- 2. Ve a la pestaña **"2. Configuración y Ejecución"**.
1456
- 3. Sube tu archivo y selecciona los modelos cinéticos que deseas probar.
1457
- 4. Ajusta las opciones de visualización y análisis según tus preferencias.
1458
- 5. Haz clic en **"Analizar y Graficar"**.
1459
- 6. Explora los resultados en la pestaña **"3. Resultados"**.
1460
-
1461
- ### Modos de Análisis
1462
- - **Individual**: Un gráfico por cada réplica
1463
- - **Promedio**: Promedio de réplicas con barras de error
1464
- - **Combinado**: Todos los componentes en un solo gráfico
1465
- - **Comparación**: Comparación de múltiples modelos
1466
- """)
1467
- with gr.Column(scale=3):
1468
- gr.Markdown("### Formato del Archivo Excel")
1469
- gr.Markdown("Usa una **cabecera de dos niveles** para tus datos.")
1470
- df_ejemplo = pd.DataFrame({
1471
- ('Rep1', 'Tiempo'): [0, 2, 4, 6],
1472
- ('Rep1', 'Biomasa'): [0.1, 0.5, 2.5, 5.0],
1473
- ('Rep1', 'Sustrato'): [10.0, 9.5, 7.0, 2.0],
1474
- ('Rep1', 'Producto'): [0.0, 0.1, 0.5, 1.2],
1475
- ('Rep2', 'Tiempo'): [0, 2, 4, 6],
1476
- ('Rep2', 'Biomasa'): [0.12, 0.48, 2.6, 5.2],
1477
- ('Rep2', 'Sustrato'): [10.2, 9.6, 7.1, 2.1],
1478
- ('Rep2', 'Producto'): [0.0, 0.12, 0.48, 1.1],
1479
- })
1480
- gr.DataFrame(df_ejemplo, interactive=False, label="Ejemplo de Formato")
1481
 
1482
- # --- TAB 2: CONFIGURACIÓN Y EJECUCIÓN ---
1483
- with gr.TabItem("2. Configuración y Ejecución"):
1484
  with gr.Row():
1485
  with gr.Column(scale=1):
1486
- file_input = gr.File(label="Sube tu archivo Excel (.xlsx)", file_types=['.xlsx'])
 
 
 
 
1487
  exp_names_input = gr.Textbox(
1488
- label="Nombres de Experimentos (opcional)",
1489
- placeholder="Nombre Hoja 1\nNombre Hoja 2\n...",
1490
- lines=3,
1491
- info="Un nombre por línea, en el mismo orden que las hojas del Excel."
1492
  )
 
1493
  model_selection_input = gr.CheckboxGroup(
1494
  choices=MODEL_CHOICES,
1495
- label="Modelos a Probar",
1496
  value=DEFAULT_MODELS
1497
  )
1498
- analysis_mode_input = gr.Radio(
1499
- ["individual", "average", "combined", "model_comparison"],
1500
- label="Modo de Análisis",
1501
- value="average",
1502
- info="Individual: por réplica. Average: promedio. Combined: 3 ejes. Comparación: todos los modelos."
1503
- )
1504
- plotting_engine_input = gr.Radio(
1505
- ["Seaborn (Estático)", "Plotly (Interactivo)"],
1506
- label="Motor Gráfico (en modo Comparación)",
1507
- value="Plotly (Interactivo)"
1508
- )
1509
-
1510
- with gr.Column(scale=2):
1511
- with gr.Accordion("Opciones Generales de Análisis", open=True):
1512
- decimal_places_input = gr.Slider(0, 10, value=3, step=1, label="Precisión Decimal")
1513
- show_params_input = gr.Checkbox(label="Mostrar Parámetros en Gráfico", value=True)
1514
- show_legend_input = gr.Checkbox(label="Mostrar Leyenda en Gráfico", value=True)
1515
- use_differential_input = gr.Checkbox(label="Usar EDO para graficar", value=False)
1516
- maxfev_input = gr.Number(label="Iteraciones Máximas de Ajuste", value=50000)
1517
-
1518
- with gr.Accordion("Etiquetas de los Ejes", open=True):
1519
- with gr.Row():
1520
- xlabel_input = gr.Textbox(label="Etiqueta Eje X", value="Tiempo (h)")
1521
- with gr.Row():
1522
- ylabel_biomass_input = gr.Textbox(label="Etiqueta Biomasa", value="Biomasa (g/L)")
1523
- ylabel_substrate_input = gr.Textbox(label="Etiqueta Sustrato", value="Sustrato (g/L)")
1524
- ylabel_product_input = gr.Textbox(label="Etiqueta Producto", value="Producto (g/L)")
1525
 
1526
- with gr.Accordion("Opciones de Estilo", open=False):
1527
- style_input = gr.Dropdown(
1528
- ['whitegrid', 'darkgrid', 'white', 'dark', 'ticks'],
1529
- label="Estilo General (Matplotlib)",
1530
- value='whitegrid'
1531
  )
1532
- with gr.Row():
1533
- with gr.Column():
1534
- gr.Markdown("**Biomasa**")
1535
- biomass_point_color_input = gr.ColorPicker(label="Color Puntos", value='#0072B2')
1536
- biomass_line_color_input = gr.ColorPicker(label="Color Línea", value='#56B4E9')
1537
- biomass_marker_style_input = gr.Dropdown(
1538
- ['o', 's', '^', 'D', 'p', '*', 'X'],
1539
- label="Marcador",
1540
- value='o'
1541
- )
1542
- biomass_line_style_input = gr.Dropdown(
1543
- ['-', '--', '-.', ':'],
1544
- label="Estilo Línea",
1545
- value='-'
1546
- )
1547
- with gr.Column():
1548
- gr.Markdown("**Sustrato**")
1549
- substrate_point_color_input = gr.ColorPicker(label="Color Puntos", value='#009E73')
1550
- substrate_line_color_input = gr.ColorPicker(label="Color Línea", value='#34E499')
1551
- substrate_marker_style_input = gr.Dropdown(
1552
- ['o', 's', '^', 'D', 'p', '*', 'X'],
1553
- label="Marcador",
1554
- value='s'
1555
- )
1556
- substrate_line_style_input = gr.Dropdown(
1557
- ['-', '--', '-.', ':'],
1558
- label="Estilo Línea",
1559
- value='--'
1560
- )
1561
- with gr.Column():
1562
- gr.Markdown("**Producto**")
1563
- product_point_color_input = gr.ColorPicker(label="Color Puntos", value='#D55E00')
1564
- product_line_color_input = gr.ColorPicker(label="Color Línea", value='#F0E442')
1565
- product_marker_style_input = gr.Dropdown(
1566
- ['o', 's', '^', 'D', 'p', '*', 'X'],
1567
- label="Marcador",
1568
- value='^'
1569
- )
1570
- product_line_style_input = gr.Dropdown(
1571
- ['-', '--', '-.', ':'],
1572
- label="Estilo Línea",
1573
- value='-.'
1574
- )
1575
 
1576
- with gr.Row():
1577
- legend_pos_input = gr.Radio(
1578
- ["best", "upper right", "upper left", "lower left", "lower right", "center"],
1579
- label="Posición Leyenda",
1580
- value="best"
1581
- )
1582
- params_pos_input = gr.Radio(
1583
- ["upper right", "upper left", "lower right", "lower left"],
1584
- label="Posición Parámetros",
1585
- value="upper right"
1586
- )
 
 
 
 
 
 
1587
 
1588
- with gr.Accordion("Opciones de Barra de Error", open=False):
1589
- show_error_bars_input = gr.Checkbox(label="Mostrar barras de error", value=True)
1590
- error_cap_size_input = gr.Slider(1, 10, 3, step=1, label="Tamaño Tapa Error")
1591
- error_line_width_input = gr.Slider(0.5, 5, 1.0, step=0.5, label="Grosor Línea Error")
1592
-
1593
- simulate_btn = gr.Button("Analizar y Graficar", variant="primary")
1594
-
1595
  # --- TAB 3: RESULTADOS ---
1596
- with gr.TabItem("3. Resultados"):
1597
- status_output = gr.Textbox(label="Estado del Análisis", interactive=False, lines=2)
1598
- gallery_output = gr.Gallery(
1599
- label="Gráficos Generados",
1600
- columns=2,
1601
- height=600,
1602
- object_fit="contain",
1603
- preview=True
1604
  )
1605
 
1606
- with gr.Accordion("Descargar Reportes y Gráficos", open=True):
1607
- with gr.Row():
1608
- zip_btn = gr.Button("📦 Descargar Gráficos (.zip)")
1609
- word_btn = gr.Button("📄 Descargar Reporte (.docx)")
1610
- pdf_btn = gr.Button("📄 Descargar Reporte (.pdf)")
1611
- download_output = gr.File(label="Archivo de Descarga", interactive=False)
1612
-
1613
- gr.Markdown("### Tabla de Resultados Numéricos")
1614
- table_output = gr.DataFrame(wrap=True)
1615
 
1616
  with gr.Row():
1617
- excel_btn = gr.Button("📊 Descargar Tabla (.xlsx)")
1618
- csv_btn = gr.Button("📊 Descargar Tabla (.csv)")
1619
- download_table_output = gr.File(label="Descargar Tabla", interactive=False)
1620
 
1621
- # Estados para almacenar datos
1622
- df_for_export = gr.State(pd.DataFrame())
1623
- figures_for_export = gr.State([])
1624
-
1625
- # --- EVENTOS ---
1626
-
1627
- def simulation_wrapper(file, models, mode, engine, names, use_diff, s_par, s_leg, maxfev,
1628
- decimals, x_label, bio_label, sub_label, prod_label, style, s_err,
1629
- cap, lw, l_pos, p_pos, bio_pc, bio_lc, bio_ms, bio_ls, sub_pc,
1630
- sub_lc, sub_ms, sub_ls, prod_pc, prod_lc, prod_ms, prod_ls):
1631
- try:
1632
- def rgba_to_hex(rgba_string: str) -> str:
1633
- if not isinstance(rgba_string, str) or rgba_string.startswith('#'):
1634
- return rgba_string
1635
- try:
1636
- parts = rgba_string.lower().replace('rgba', '').replace('rgb', '').replace('(', '').replace(')', '')
1637
- r, g, b, *_ = map(float, parts.split(','))
1638
- return f'#{int(r):02x}{int(g):02x}{int(b):02x}'
1639
- except (ValueError, TypeError):
1640
- return "#000000"
1641
-
1642
- plot_settings = {
1643
- 'decimal_places': int(decimals),
1644
- 'use_differential': use_diff,
1645
- 'style': style,
1646
- 'show_legend': s_leg,
1647
- 'show_params': s_par,
1648
- 'maxfev': int(maxfev),
1649
- 'axis_labels': {
1650
- 'x_label': x_label,
1651
- 'biomass_label': bio_label,
1652
- 'substrate_label': sub_label,
1653
- 'product_label': prod_label
1654
  },
1655
- 'legend_pos': l_pos,
1656
- 'params_pos': p_pos,
1657
- 'show_error_bars': s_err,
1658
- 'error_cap_size': cap,
1659
- 'error_line_width': lw,
1660
- f'{C_BIOMASS}_point_color': rgba_to_hex(bio_pc),
1661
- f'{C_BIOMASS}_line_color': rgba_to_hex(bio_lc),
1662
- f'{C_BIOMASS}_marker_style': bio_ms,
1663
- f'{C_BIOMASS}_line_style': bio_ls,
1664
- f'{C_SUBSTRATE}_point_color': rgba_to_hex(sub_pc),
1665
- f'{C_SUBSTRATE}_line_color': rgba_to_hex(sub_lc),
1666
- f'{C_SUBSTRATE}_marker_style': sub_ms,
1667
- f'{C_SUBSTRATE}_line_style': sub_ls,
1668
- f'{C_PRODUCT}_point_color': rgba_to_hex(prod_pc),
1669
- f'{C_PRODUCT}_line_color': rgba_to_hex(prod_lc),
1670
- f'{C_PRODUCT}_marker_style': prod_ms,
1671
- f'{C_PRODUCT}_line_style': prod_ls,
1672
  }
 
 
 
1673
 
1674
- figures, df_ui, msg, df_export = run_analysis(file, models, mode, engine, names, plot_settings)
 
1675
 
1676
- # Convertir figuras a imágenes para galería
1677
- image_list = []
1678
- for fig in figures:
1679
- buf = io.BytesIO()
1680
- if isinstance(fig, go.Figure):
1681
- buf.write(fig.to_image(format="png", scale=2, engine="kaleido"))
1682
- elif isinstance(fig, plt.Figure):
1683
- fig.savefig(buf, format='png', bbox_inches='tight', dpi=150)
1684
- plt.close(fig)
1685
- buf.seek(0)
1686
- image_list.append(Image.open(buf).convert("RGB"))
1687
 
1688
- return image_list, df_ui, msg, df_export, figures
 
 
 
 
1689
 
1690
- except Exception as e:
1691
- print(f"--- ERROR CAPTURADO EN WRAPPER ---\n{traceback.format_exc()}")
1692
- return [], pd.DataFrame(), f"Error Crítico: {e}", pd.DataFrame(), []
1693
-
1694
- all_inputs = [
1695
- file_input, model_selection_input, analysis_mode_input, plotting_engine_input, exp_names_input,
1696
- use_differential_input, show_params_input, show_legend_input, maxfev_input, decimal_places_input,
1697
- xlabel_input, ylabel_biomass_input, ylabel_substrate_input, ylabel_product_input,
1698
- style_input, show_error_bars_input, error_cap_size_input, error_line_width_input,
1699
- legend_pos_input, params_pos_input,
1700
- biomass_point_color_input, biomass_line_color_input, biomass_marker_style_input, biomass_line_style_input,
1701
- substrate_point_color_input, substrate_line_color_input, substrate_marker_style_input, substrate_line_style_input,
1702
- product_point_color_input, product_line_color_input, product_marker_style_input, product_line_style_input
1703
- ]
1704
-
1705
- all_outputs = [gallery_output, table_output, status_output, df_for_export, figures_for_export]
1706
-
1707
- simulate_btn.click(fn=simulation_wrapper, inputs=all_inputs, outputs=all_outputs)
1708
 
1709
- # Funciones de descarga
1710
- zip_btn.click(fn=create_zip_file, inputs=[figures_for_export], outputs=[download_output])
1711
- word_btn.click(
1712
- fn=create_word_report,
1713
- inputs=[figures_for_export, df_for_export, decimal_places_input],
1714
- outputs=[download_output]
1715
- )
1716
- pdf_btn.click(
1717
- fn=create_pdf_report,
1718
- inputs=[figures_for_export, df_for_export, decimal_places_input],
1719
- outputs=[download_output]
1720
- )
1721
 
1722
- def export_table_to_file(df: pd.DataFrame, file_format: str) -> Optional[str]:
1723
- if df is None or df.empty:
1724
- gr.Warning("No hay datos para exportar.")
1725
- return None
1726
- suffix = ".xlsx" if file_format == "excel" else ".csv"
1727
- with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
1728
- if file_format == "excel":
1729
- df.to_excel(tmp.name, index=False)
1730
- else:
1731
- df.to_csv(tmp.name, index=False, encoding='utf-8-sig')
1732
- return tmp.name
1733
 
1734
- excel_btn.click(
1735
- fn=lambda df: export_table_to_file(df, "excel"),
1736
- inputs=[df_for_export],
1737
- outputs=[download_table_output]
1738
- )
1739
- csv_btn.click(
1740
- fn=lambda df: export_table_to_file(df, "csv"),
1741
- inputs=[df_for_export],
1742
- outputs=[download_table_output]
 
 
 
1743
  )
1744
 
1745
  # Cambio de idioma
@@ -1751,17 +1327,46 @@ def create_gradio_interface() -> gr.Blocks:
1751
 
1752
  # Cambio de tema
1753
  def apply_theme(is_dark):
1754
- return gr.Info("Tema cambiado. Los nuevos gráficos usarán el tema seleccionado.")
1755
 
1756
  theme_toggle.change(
1757
  fn=apply_theme,
1758
  inputs=[theme_toggle],
1759
  outputs=[]
1760
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1761
 
1762
  return demo
1763
 
1764
- # --- PUNTO DE ENTRADA PRINCIPAL ---
1765
 
1766
  if __name__ == '__main__':
1767
  # Lanzar aplicación Gradio
 
17
  from dataclasses import dataclass
18
  from enum import Enum
19
  import json
 
20
 
21
  from PIL import Image
22
  import gradio as gr
23
  import plotly.graph_objects as go
24
  from plotly.subplots import make_subplots
 
25
  import numpy as np
26
  import pandas as pd
27
  import matplotlib.pyplot as plt
 
68
  "language": "Idioma",
69
  "theory": "Teoría y Modelos",
70
  "guide": "Guía de Uso",
71
+ "api_docs": "Documentación API"
 
 
 
 
72
  },
73
  Language.EN: {
74
  "title": "🔬 Bioprocess Kinetics Analyzer",
 
91
  "language": "Language",
92
  "theory": "Theory and Models",
93
  "guide": "User Guide",
94
+ "api_docs": "API Documentation"
 
 
 
 
95
  },
96
  }
97
 
 
100
  C_BIOMASS = 'biomass'
101
  C_SUBSTRATE = 'substrate'
102
  C_PRODUCT = 'product'
103
+ C_OXYGEN = 'oxygen'
104
+ C_CO2 = 'co2'
105
+ C_PH = 'ph'
106
  COMPONENTS = [C_BIOMASS, C_SUBSTRATE, C_PRODUCT]
107
 
108
  # --- SISTEMA DE TEMAS ---
 
541
  self.data_time: Optional[np.ndarray] = None
542
  self.data_means: Dict[str, Optional[np.ndarray]] = {c: None for c in COMPONENTS}
543
  self.data_stds: Dict[str, Optional[np.ndarray]] = {c: None for c in COMPONENTS}
 
544
 
545
  def _get_biomass_at_t(self, t: np.ndarray, p: List[float]) -> np.ndarray:
546
  return self.model.model_function(t, *p)
 
579
  self.data_time = df[time_col].dropna().to_numpy()
580
  min_len = len(self.data_time)
581
 
582
+ def extract(name: str) -> Tuple[np.ndarray, np.ndarray]:
583
  cols = [c for c in df.columns if c[1].strip().lower() == name.lower()]
584
+ if not cols: return np.array([]), np.array([])
585
  reps = [df[c].dropna().values[:min_len] for c in cols]
586
  reps = [r for r in reps if len(r) == min_len]
587
+ if not reps: return np.array([]), np.array([])
588
  arr = np.array(reps)
589
  mean = np.mean(arr, axis=0)
590
  std = np.std(arr, axis=0, ddof=1) if arr.shape[0] > 1 else np.zeros_like(mean)
591
+ return mean, std
592
 
593
+ self.data_means[C_BIOMASS], self.data_stds[C_BIOMASS] = extract('Biomasa')
594
+ self.data_means[C_SUBSTRATE], self.data_stds[C_SUBSTRATE] = extract('Sustrato')
595
+ self.data_means[C_PRODUCT], self.data_stds[C_PRODUCT] = extract('Producto')
 
 
 
 
596
  except (IndexError, KeyError) as e:
597
  raise ValueError(f"Estructura de DataFrame inválida. Error: {e}")
598
 
 
745
  if self.params[C_PRODUCT]:
746
  P = self.product(t_fine, *list(self.params[C_PRODUCT].values()), bio_p)
747
  return X, S, P
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
 
749
  # --- FUNCIONES AUXILIARES ---
750
 
 
763
 
764
  return str(round(value, decimals))
765
 
766
+ # --- FUNCIONES DE PLOTEO MEJORADAS CON PLOTLY ---
767
 
768
+ def create_interactive_plot(plot_config: Dict, models_results: List[Dict],
769
+ selected_component: str = "all") -> go.Figure:
770
+ """Crea un gráfico interactivo mejorado con Plotly"""
771
  time_exp = plot_config['time_exp']
772
+ time_fine = np.linspace(min(time_exp), max(time_exp), 500)
773
+
774
+ # Configuración de subplots si se muestran todos los componentes
775
+ if selected_component == "all":
776
+ fig = make_subplots(
777
+ rows=3, cols=1,
778
+ subplot_titles=('Biomasa', 'Sustrato', 'Producto'),
779
+ vertical_spacing=0.08,
780
+ shared_xaxes=True
781
+ )
782
+ components_to_plot = [C_BIOMASS, C_SUBSTRATE, C_PRODUCT]
783
+ rows = [1, 2, 3]
784
+ else:
785
+ fig = go.Figure()
786
+ components_to_plot = [selected_component]
787
+ rows = [None]
788
+
789
+ # Colores para diferentes modelos
790
+ colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
791
+ '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
792
+
793
+ # Agregar datos experimentales
794
+ for comp, row in zip(components_to_plot, rows):
795
+ data_exp = plot_config.get(f'{comp}_exp')
796
+ data_std = plot_config.get(f'{comp}_std')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
+ if data_exp is not None:
799
+ error_y = dict(
800
+ type='data',
801
+ array=data_std,
802
+ visible=True
803
+ ) if data_std is not None and np.any(data_std > 0) else None
 
 
 
804
 
805
+ trace = go.Scatter(
806
+ x=time_exp,
807
+ y=data_exp,
808
+ mode='markers',
809
+ name=f'{comp.capitalize()} (Experimental)',
810
+ marker=dict(size=10, symbol='circle'),
811
+ error_y=error_y,
812
+ legendgroup=comp,
813
+ showlegend=True
814
+ )
815
 
816
+ if selected_component == "all":
817
+ fig.add_trace(trace, row=row, col=1)
818
+ else:
819
+ fig.add_trace(trace)
820
+
821
+ # Agregar curvas de modelos
822
+ for i, res in enumerate(models_results):
823
+ color = colors[i % len(colors)]
824
+ model_name = AVAILABLE_MODELS[res["name"]].display_name
 
825
 
826
+ for comp, row, key in zip(components_to_plot, rows, ['X', 'S', 'P']):
827
+ if res.get(key) is not None:
828
+ trace = go.Scatter(
829
+ x=time_fine,
830
+ y=res[key],
831
+ mode='lines',
832
+ name=f'{model_name} - {comp.capitalize()}',
833
+ line=dict(color=color, width=2),
834
+ legendgroup=f'{res["name"]}_{comp}',
835
+ showlegend=True
836
+ )
837
 
838
+ if selected_component == "all":
839
+ fig.add_trace(trace, row=row, col=1)
 
 
 
 
 
 
840
  else:
841
+ fig.add_trace(trace)
842
+
843
+ # Actualizar diseño
844
+ theme = plot_config.get('theme', 'light')
845
+ template = "plotly_white" if theme == 'light' else "plotly_dark"
846
+
847
+ fig.update_layout(
848
+ title=f"Análisis de Cinéticas: {plot_config.get('exp_name', '')}",
849
+ template=template,
850
+ hovermode='x unified',
851
+ legend=dict(
852
+ orientation="v",
853
+ yanchor="middle",
854
+ y=0.5,
855
+ xanchor="left",
856
+ x=1.02
857
+ ),
858
+ margin=dict(l=80, r=250, t=100, b=80)
859
+ )
860
+
861
+ # Actualizar ejes
862
+ if selected_component == "all":
863
+ fig.update_xaxes(title_text="Tiempo", row=3, col=1)
864
+ fig.update_yaxes(title_text="Biomasa (g/L)", row=1, col=1)
865
+ fig.update_yaxes(title_text="Sustrato (g/L)", row=2, col=1)
866
+ fig.update_yaxes(title_text="Producto (g/L)", row=3, col=1)
867
+ else:
868
+ fig.update_xaxes(title_text="Tiempo")
869
+ labels = {
870
+ C_BIOMASS: "Biomasa (g/L)",
871
+ C_SUBSTRATE: "Sustrato (g/L)",
872
+ C_PRODUCT: "Producto (g/L)"
873
+ }
874
+ fig.update_yaxes(title_text=labels.get(selected_component, "Valor"))
875
+
876
+ # Agregar botones para cambiar entre modos de visualización
877
+ fig.update_layout(
878
+ updatemenus=[
879
+ dict(
880
+ type="dropdown",
881
+ showactive=True,
882
+ buttons=[
883
+ dict(label="Todos los componentes",
884
+ method="update",
885
+ args=[{"visible": [True] * len(fig.data)}]),
886
+ dict(label="Solo Biomasa",
887
+ method="update",
888
+ args=[{"visible": [i < len(fig.data)//3 for i in range(len(fig.data))]}]),
889
+ dict(label="Solo Sustrato",
890
+ method="update",
891
+ args=[{"visible": [len(fig.data)//3 <= i < 2*len(fig.data)//3 for i in range(len(fig.data))]}]),
892
+ dict(label="Solo Producto",
893
+ method="update",
894
+ args=[{"visible": [i >= 2*len(fig.data)//3 for i in range(len(fig.data))]}]),
895
+ ],
896
+ x=0.1,
897
+ y=1.15,
898
+ xanchor="left",
899
+ yanchor="top"
900
+ )
901
+ ]
902
+ )
903
+
904
+ return fig
905
 
906
  # --- FUNCIÓN PRINCIPAL DE ANÁLISIS ---
907
+ def run_analysis(file, model_names, component, use_de, maxfev, exp_names, theme='light'):
908
+ if not file: return None, pd.DataFrame(), "Error: Sube un archivo Excel."
909
+ if not model_names: return None, pd.DataFrame(), "Error: Selecciona un modelo."
 
 
 
 
910
 
911
+ try:
912
  xls = pd.ExcelFile(file.name)
913
+ except Exception as e:
914
+ return None, pd.DataFrame(), f"Error al leer archivo: {e}"
915
 
916
+ results_data, msgs = [], []
917
+ models_results = []
 
918
 
919
+ exp_list = [n.strip() for n in exp_names.split('\n') if n.strip()] if exp_names else []
920
 
921
  for i, sheet in enumerate(xls.sheet_names):
922
  exp_name = exp_list[i] if i < len(exp_list) else f"Hoja '{sheet}'"
 
923
  try:
924
  df = pd.read_excel(xls, sheet_name=sheet, header=[0,1])
925
  reader = BioprocessFitter(list(AVAILABLE_MODELS.values())[0])
926
  reader.process_data_from_df(df)
927
 
928
+ if reader.data_time is None:
929
  msgs.append(f"WARN: Sin datos de tiempo en '{sheet}'.")
930
  continue
 
 
 
931
 
932
+ plot_config = {
933
+ 'exp_name': exp_name,
934
+ 'time_exp': reader.data_time,
935
+ 'theme': theme
936
+ }
937
+
938
+ for c in COMPONENTS:
939
+ plot_config[f'{c}_exp'] = reader.data_means[c]
940
+ plot_config[f'{c}_std'] = reader.data_stds[c]
941
 
942
  t_fine = reader._generate_fine_time_grid(reader.data_time)
 
943
 
944
  for m_name in model_names:
945
+ if m_name not in AVAILABLE_MODELS:
946
  msgs.append(f"WARN: Modelo '{m_name}' no disponible.")
947
  continue
948
 
949
+ fitter = BioprocessFitter(
950
+ AVAILABLE_MODELS[m_name],
951
+ maxfev=int(maxfev),
952
+ use_differential_evolution=use_de
953
+ )
954
  fitter.data_time = reader.data_time
955
  fitter.data_means = reader.data_means
956
  fitter.data_stds = reader.data_stds
 
957
  fitter.fit_all_models()
958
 
 
959
  row = {'Experimento': exp_name, 'Modelo': fitter.model.display_name}
960
  for c in COMPONENTS:
961
+ if fitter.params[c]:
962
  row.update({f'{c.capitalize()}_{k}': v for k, v in fitter.params[c].items()})
963
  row[f'R2_{c.capitalize()}'] = fitter.r2.get(c)
964
  row[f'RMSE_{c.capitalize()}'] = fitter.rmse.get(c)
965
  row[f'MAE_{c.capitalize()}'] = fitter.mae.get(c)
966
  row[f'AIC_{c.capitalize()}'] = fitter.aic.get(c)
967
  row[f'BIC_{c.capitalize()}'] = fitter.bic.get(c)
968
+
969
  results_data.append(row)
970
 
971
+ X, S, P = fitter.get_model_curves_for_plot(t_fine, False)
972
+ models_results.append({
973
+ 'name': m_name,
974
+ 'X': X,
975
+ 'S': S,
976
+ 'P': P,
977
+ 'params': fitter.params,
978
+ 'r2': fitter.r2,
979
+ 'rmse': fitter.rmse
980
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981
 
982
+ except Exception as e:
983
  msgs.append(f"ERROR en '{sheet}': {e}")
984
  traceback.print_exc()
985
 
986
  msg = "Análisis completado." + ("\n" + "\n".join(msgs) if msgs else "")
987
  df_res = pd.DataFrame(results_data).dropna(axis=1, how='all')
988
 
989
+ # Crear gráfico interactivo
990
+ fig = None
991
+ if models_results and reader.data_time is not None:
992
+ fig = create_interactive_plot(plot_config, models_results, component)
 
 
 
 
 
 
 
 
 
993
 
994
+ return fig, df_res, msg
995
 
996
  # --- API ENDPOINTS PARA AGENTES DE IA ---
997
 
 
1083
  except Exception as e:
1084
  return {"status": "error", "message": str(e)}
1085
 
1086
+ # --- INTERFAZ GRADIO MEJORADA ---
1087
 
1088
  def create_gradio_interface() -> gr.Blocks:
1089
+ """Crea la interfaz mejorada con soporte multiidioma y tema"""
1090
 
1091
  def change_language(lang_key: str) -> Dict:
1092
  """Cambia el idioma de la interfaz"""
1093
  lang = Language[lang_key]
1094
  trans = TRANSLATIONS.get(lang, TRANSLATIONS[Language.ES])
1095
+
1096
  return trans["title"], trans["subtitle"]
1097
 
1098
  # Obtener opciones de modelo
 
1126
  )
1127
 
1128
  with gr.Tabs() as tabs:
1129
+ # --- TAB 1: TEORÍA Y MODELOS ---
1130
+ with gr.TabItem("📚 Teoría y Modelos"):
1131
+ gr.Markdown("""
1132
+ ## Introducción a los Modelos Cinéticos
1133
+
1134
+ Los modelos cinéticos en biotecnología describen el comportamiento dinámico
1135
+ de los microorganismos durante su crecimiento. Estos modelos son fundamentales
1136
+ para:
1137
+
1138
+ - **Optimización de procesos**: Determinar condiciones óptimas de operación
1139
+ - **Escalamiento**: Predecir comportamiento a escala industrial
1140
+ - **Control de procesos**: Diseñar estrategias de control efectivas
1141
+ - **Análisis económico**: Evaluar viabilidad de procesos
1142
+ """)
1143
+
1144
+ # Cards para cada modelo
1145
+ for model_name, model in AVAILABLE_MODELS.items():
1146
+ with gr.Accordion(f"📊 {model.display_name}", open=False):
1147
+ with gr.Row():
1148
+ with gr.Column(scale=3):
1149
+ gr.Markdown(f"""
1150
+ **Descripción**: {model.description}
1151
+
1152
+ **Ecuación**: ${model.equation}$
1153
+
1154
+ **Parámetros**: {', '.join(model.param_names)}
1155
+
1156
+ **Referencia**: {model.reference}
1157
+ """)
1158
+ with gr.Column(scale=1):
1159
+ gr.Markdown(f"""
1160
+ **Características**:
1161
+ - Parámetros: {model.num_params}
1162
+ - Complejidad: {'' * min(model.num_params, 5)}
1163
+ """)
 
1164
 
1165
+ # --- TAB 2: ANÁLISIS ---
1166
+ with gr.TabItem("🔬 Análisis"):
1167
  with gr.Row():
1168
  with gr.Column(scale=1):
1169
+ file_input = gr.File(
1170
+ label="📁 Sube tu archivo Excel (.xlsx)",
1171
+ file_types=['.xlsx']
1172
+ )
1173
+
1174
  exp_names_input = gr.Textbox(
1175
+ label="🏷️ Nombres de Experimentos",
1176
+ placeholder="Experimento 1\nExperimento 2\n...",
1177
+ lines=3
 
1178
  )
1179
+
1180
  model_selection_input = gr.CheckboxGroup(
1181
  choices=MODEL_CHOICES,
1182
+ label="📊 Modelos a Probar",
1183
  value=DEFAULT_MODELS
1184
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1185
 
1186
+ with gr.Accordion("⚙️ Opciones Avanzadas", open=False):
1187
+ use_de_input = gr.Checkbox(
1188
+ label="Usar Evolución Diferencial",
1189
+ value=False,
1190
+ info="Optimización global más robusta pero más lenta"
1191
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
 
1193
+ maxfev_input = gr.Number(
1194
+ label="Iteraciones máximas",
1195
+ value=50000
1196
+ )
1197
+
1198
+ with gr.Column(scale=2):
1199
+ # Selector de componente para visualización
1200
+ component_selector = gr.Dropdown(
1201
+ choices=[
1202
+ ("Todos los componentes", "all"),
1203
+ ("Solo Biomasa", C_BIOMASS),
1204
+ ("Solo Sustrato", C_SUBSTRATE),
1205
+ ("Solo Producto", C_PRODUCT)
1206
+ ],
1207
+ value="all",
1208
+ label="📈 Componente a visualizar"
1209
+ )
1210
 
1211
+ plot_output = gr.Plot(label="Visualización Interactiva")
1212
+
1213
+ analyze_button = gr.Button("🚀 Analizar y Graficar", variant="primary")
1214
+
 
 
 
1215
  # --- TAB 3: RESULTADOS ---
1216
+ with gr.TabItem("📊 Resultados"):
1217
+ status_output = gr.Textbox(
1218
+ label="Estado del Análisis",
1219
+ interactive=False
 
 
 
 
1220
  )
1221
 
1222
+ results_table = gr.DataFrame(
1223
+ label="Tabla de Resultados",
1224
+ wrap=True
1225
+ )
 
 
 
 
 
1226
 
1227
  with gr.Row():
1228
+ download_excel = gr.Button("📥 Descargar Excel")
1229
+ download_json = gr.Button("📥 Descargar JSON")
1230
+ api_docs_button = gr.Button("📖 Ver Documentación API")
1231
 
1232
+ download_file = gr.File(label="Archivo descargado")
1233
+
1234
+ # --- TAB 4: API ---
1235
+ with gr.TabItem("🔌 API"):
1236
+ gr.Markdown("""
1237
+ ## Documentación de la API
1238
+
1239
+ La API REST permite integrar el análisis de cinéticas en aplicaciones externas
1240
+ y agentes de IA.
1241
+
1242
+ ### Endpoints disponibles:
1243
+
1244
+ #### 1. `GET /api/models`
1245
+ Retorna la lista de modelos disponibles con su información.
1246
+
1247
+ ```python
1248
+ import requests
1249
+ response = requests.get("http://localhost:8000/api/models")
1250
+ models = response.json()
1251
+ ```
1252
+
1253
+ #### 2. `POST /api/analyze`
1254
+ Analiza datos con los modelos especificados.
1255
+
1256
+ ```python
1257
+ data = {
1258
+ "data": {
1259
+ "time": [0, 1, 2, 3, 4],
1260
+ "biomass": [0.1, 0.3, 0.8, 1.5, 2.0],
1261
+ "substrate": [10, 8, 5, 2, 0.5]
 
 
 
1262
  },
1263
+ "models": ["logistic", "gompertz"],
1264
+ "options": {"maxfev": 50000}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1265
  }
1266
+ response = requests.post("http://localhost:8000/api/analyze", json=data)
1267
+ results = response.json()
1268
+ ```
1269
 
1270
+ #### 3. `POST /api/predict`
1271
+ Predice valores usando un modelo y parámetros específicos.
1272
 
1273
+ ```python
1274
+ data = {
1275
+ "model_name": "logistic",
1276
+ "parameters": {"X0": 0.1, "Xm": 10.0, "μm": 0.5},
1277
+ "time_points": [0, 1, 2, 3, 4, 5]
1278
+ }
1279
+ response = requests.post("http://localhost:8000/api/predict", json=data)
1280
+ predictions = response.json()
1281
+ ```
 
 
1282
 
1283
+ ### Iniciar servidor API:
1284
+ ```bash
1285
+ uvicorn script_name:app --reload --port 8000
1286
+ ```
1287
+ """)
1288
 
1289
+ # Botón para copiar comando
1290
+ gr.Textbox(
1291
+ value="uvicorn bioprocess_analyzer:app --reload --port 8000",
1292
+ label="Comando para iniciar API",
1293
+ interactive=False
1294
+ )
 
 
 
 
 
 
 
 
 
 
 
 
1295
 
1296
+ # --- EVENTOS ---
 
 
 
 
 
 
 
 
 
 
 
1297
 
1298
+ def run_analysis_wrapper(file, models, component, use_de, maxfev, exp_names, theme):
1299
+ """Wrapper para ejecutar el análisis"""
1300
+ try:
1301
+ return run_analysis(file, models, component, use_de, maxfev, exp_names,
1302
+ 'dark' if theme else 'light')
1303
+ except Exception as e:
1304
+ print(f"--- ERROR EN ANÁLISIS ---\n{traceback.format_exc()}")
1305
+ return None, pd.DataFrame(), f"Error: {str(e)}"
 
 
 
1306
 
1307
+ analyze_button.click(
1308
+ fn=run_analysis_wrapper,
1309
+ inputs=[
1310
+ file_input,
1311
+ model_selection_input,
1312
+ component_selector,
1313
+ use_de_input,
1314
+ maxfev_input,
1315
+ exp_names_input,
1316
+ theme_toggle
1317
+ ],
1318
+ outputs=[plot_output, results_table, status_output]
1319
  )
1320
 
1321
  # Cambio de idioma
 
1327
 
1328
  # Cambio de tema
1329
  def apply_theme(is_dark):
1330
+ return gr.Info("Tema cambiado. Los gráficos nuevos usarán el tema seleccionado.")
1331
 
1332
  theme_toggle.change(
1333
  fn=apply_theme,
1334
  inputs=[theme_toggle],
1335
  outputs=[]
1336
  )
1337
+
1338
+ # Funciones de descarga
1339
+ def download_results_excel(df):
1340
+ if df is None or df.empty:
1341
+ gr.Warning("No hay datos para descargar")
1342
+ return None
1343
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
1344
+ df.to_excel(tmp.name, index=False)
1345
+ return tmp.name
1346
+
1347
+ def download_results_json(df):
1348
+ if df is None or df.empty:
1349
+ gr.Warning("No hay datos para descargar")
1350
+ return None
1351
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
1352
+ df.to_json(tmp.name, orient='records', indent=2)
1353
+ return tmp.name
1354
+
1355
+ download_excel.click(
1356
+ fn=download_results_excel,
1357
+ inputs=[results_table],
1358
+ outputs=[download_file]
1359
+ )
1360
+
1361
+ download_json.click(
1362
+ fn=download_results_json,
1363
+ inputs=[results_table],
1364
+ outputs=[download_file]
1365
+ )
1366
 
1367
  return demo
1368
 
1369
+ # --- PUNTO DE ENTRADA ---
1370
 
1371
  if __name__ == '__main__':
1372
  # Lanzar aplicación Gradio