Ejercicio 11 — Se presentan tres estrategias de operacion para un biorreactor lote/lote-alimentado modelando la produccion de 1,3-propanodiol (1,3-PDO) a partir de glicerol. Use las pestanas para navegar entre soluciones.


1 Fundamento Teorico

1.1 Modelo cinetico

\[\mu = \mu_{max} \cdot \frac{S}{S + K_S} \cdot \left[1 - \left(\frac{S}{S_m}\right)^a\right] \cdot \left[1 - \left(\frac{P}{P_m}\right)^b\right]\] \[r_s = \frac{1}{Y_{x/s}} \cdot \mu + m \qquad r_p = \alpha \cdot \mu + \beta\]

1.2 Balances de masa

\[\frac{dX}{dt} = \mu X - D X \quad \frac{dS}{dt} = -r_s X + D(S_0 - S) \quad \frac{dP}{dt} = r_p X - D P \quad \frac{dV}{dt} = F \quad D = \frac{F}{V}\]


2 Parametros Globales

# ============================================================
# MODIFICAR AQUI — Los cambios se propagan a las 3 soluciones
# ============================================================
parametros <- list(
  mu_max  = 0.65,   Ks      = 12.8,
  a       = 1.12,   b       = 1,
  Sm      = 98.3,   Pm      = 65.2,
  Yxs     = 0.067,  m       = 0.23,
  alpha   = 7.3,    beta    = 0,
  S0_feed = 180,    F_flow  = 0.4,
  V_final = 4,      S_low   = 20,
  S_high  = 54.45
)

S_ini <- 54.45; X_ini <- 0.05; P_ini <- 0; V_ini <- 1.5
t_max <- 47;    dt    <- 0.01

cat("Parametros cargados correctamente.\n")
## Parametros cargados correctamente.

3 Simulaciones

3.1 Solucion 1 — Control ON/OFF

Solucion 1 Control binario ON/OFF con flujo fijo F = 0.4 L/h

Logica de operacion: 1. Bomba OFF hasta que \(S \leq 20\) g/L (primera vez). 2. ON cuando \(S \leq 20\) g/L y \(V < 4\) L. 3. OFF cuando \(S \geq 54.45\) g/L o \(V \geq 4\) L. 4. Fin al agotarse el sustrato o a las 47 h.

library(plotly)
datos1 <- simular_biorreactor(parametros, modo="onoff")
cat("=== SOLUCION 1: Control ON/OFF ===\n")
## === SOLUCION 1: Control ON/OFF ===
cat(sprintf("  P maximo  : %.4f g/L\n", max(datos1$Producto)))
##   P maximo  : 56.5132 g/L
cat(sprintf("  P(47h)    : %.4f g/L\n", tail(datos1$Producto,1)))
##   P(47h)    : 56.5132 g/L
cat(sprintf("  X maximo  : %.4f g/L\n", max(datos1$Biomasa)))
##   X maximo  : 7.7603 g/L
cat(sprintf("  V final   : %.4f L\n",   tail(datos1$Volumen,1)))
##   V final   : 4.0000 L
cat(sprintf("  t final   : %.2f h\n",   tail(datos1$Tiempo,1)))
##   t final   : 31.46 h

3.1.1 Graficas — Solucion 1

p_F1 <- plot_ly(datos1, x=~Tiempo, y=~Flujo, type='scatter', mode='lines',
                fill='tozeroy', fillcolor='rgba(220,50,50,0.15)',
                line=list(color='#dc3232', width=2.5), name='F (L/h)') %>%
  layout(xaxis=list(title="Tiempo (h)"), yaxis=list(title="F (L/h)"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff")
p_D1 <- plot_ly(datos1, x=~Tiempo, y=~Dilucion, type='scatter', mode='lines',
                fill='tozeroy', fillcolor='rgba(130,50,180,0.12)',
                line=list(color='#7c3aed', width=2.5), name='D (1/h)') %>%
  layout(xaxis=list(title="Tiempo (h)"), yaxis=list(title="D (1/h)"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff")
subplot(p_F1, p_D1, nrows=1, shareX=FALSE, titleY=TRUE, margin=0.07) %>%
  layout(title=list(text="Sol. 1 — Flujo y Dilucion (Control ON/OFF)",
                    font=list(size=14, color="#1a6b8a")))
plot_ly(datos1, x=~Tiempo) %>%
  add_trace(y=~Biomasa,  type='scatter', mode='lines',
            line=list(color='#1d4ed8',width=2.5), name='X (g/L)', yaxis='y') %>%
  add_trace(y=~Sustrato, type='scatter', mode='lines',
            line=list(color='#ea580c',width=2.5), name='S (g/L)', yaxis='y2') %>%
  add_trace(y=~Producto, type='scatter', mode='lines',
            line=list(color='#0891b2',width=2.5), name='P (g/L)', yaxis='y2') %>%
  add_trace(y=~Volumen,  type='scatter', mode='lines',
            line=list(color='#059669',width=1.8,dash='dot'), name='V (L)', yaxis='y') %>%
  layout(title=list(text="Sol. 1 — X, S, P, V vs Tiempo",
                    font=list(size=14, color="#1a6b8a")),
         xaxis=list(title="Tiempo (h)", domain=c(0,0.88)),
         yaxis =list(title="X, V", side="left",
                     titlefont=list(color='#1d4ed8'), tickfont=list(color='#1d4ed8')),
         yaxis2=list(title="S, P (g/L)", side="right", overlaying="y",
                     titlefont=list(color='#ea580c'), tickfont=list(color='#ea580c')),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff",
         legend=list(x=0.01,y=0.98))

3.2 Solucion 2 — Optimizacion de Flujo

Solucion 2 Flujo constante optimo entre t = 20 h y t = 40 h

3.2.1 Que significa optimizar el flujo?

Problema de optimizacion:

El biorreactor opera en lote puro de t=0 a t=20 h. Entre t=20 h y t=40 h, una bomba suministra alimentacion a flujo constante F* (a determinar). De t=40 a t=47 h, vuelve al modo lote.

El objetivo es hallar F* que maximice la concentracion final de producto P(47 h).

3.2.2 Por que existe un optimo?

Trade-off entre dos efectos opuestos:

Efecto positivo del flujo — mas sustrato: Al aumentar F entra mas glicerol (\(S_0 \cdot F\)), lo que sostiene la cinetica de crecimiento y produccion por mas tiempo. Si S es limitante, mas flujo implica mayor \(\mu\) y mayor \(r_p = \alpha \mu + \beta\).

Efecto negativo del flujo — dilucion: Al aumentar F tambien aumenta la tasa de dilucion \(D = F/V\), que arrastra biomasa y producto del biorreactor. Los balances muestran esto claramente: \[\frac{dP}{dt} = r_p X - D \cdot P \qquad \frac{dX}{dt} = \mu X - D \cdot X\]

Un D muy alto hace que el termino de dilucion supere la produccion biologica. Ademas, al aumentar V rapidamente, D = F/V se mantiene elevado de forma sostenida.

Conclusion: Existe un F* optimo donde el aporte de sustrato compensa exactamente la dilucion. Este optimo se identifica numericamente barriendo valores de F.

Funcion objetivo: \[F^* = \arg\max_{F \geq 0} \; P(t = 47\ \text{h};\ F) \quad \text{sujeto a} \quad V(t) \leq V_{final} = 4\ \text{L}\]

3.2.4 Curva de optimizacion

df_opt <- data.frame(F=F_grid, P_final=P_final)

plot_ly(df_opt, x=~F, y=~P_final,
        type='scatter', mode='lines',
        line=list(color='#2eaf7d', width=3),
        fill='tozeroy', fillcolor='rgba(46,175,125,0.10)',
        name='P(47h) (g/L)') %>%
  add_trace(x=F_opt, y=P_opt, type='scatter', mode='markers',
            marker=list(color='#e8a838', size=14, symbol='star',
                        line=list(color='#c07a00', width=2)),
            name=paste0('F* = ', round(F_opt,3),' L/h')) %>%
  add_trace(x=0.4, y=P_ref, type='scatter', mode='markers',
            marker=list(color='#2563eb', size=10, symbol='circle',
                        line=list(color='#1e3a5f', width=2)),
            name='F=0.4 L/h (Sol.1 ref)') %>%
  layout(
    title=list(text="Curva de Optimizacion: P(47h) vs Flujo de Alimentacion F",
               font=list(size=14, color="#1a6b8a")),
    xaxis=list(title="Flujo de alimentacion F (L/h)", gridcolor="#eaeaea"),
    yaxis=list(title="Producto final P(47h) (g/L)",   gridcolor="#eaeaea"),
    paper_bgcolor="#fafcff", plot_bgcolor="#fafcff",
    annotations=list(list(
      x=F_opt, y=P_opt*1.04,
      text=paste0("<b>F* = ", round(F_opt,3)," L/h<br>P = ", round(P_opt,2)," g/L</b>"),
      showarrow=FALSE, font=list(color="#92400e", size=12),
      bgcolor="#fef3c7", bordercolor="#d97706", borderwidth=1, borderpad=5
    ))
  )

3.2.5 Simulacion con F optimo

datos2 <- simular_biorreactor(parametros, modo="constante",
                               F_const=F_opt, t_feed_on=20, t_feed_off=40)

cat("=== SOLUCION 2: F* =", round(F_opt,4), "L/h ===\n")
## === SOLUCION 2: F* = 0.32 L/h ===
cat(sprintf("  P maximo  : %.4f g/L\n", max(datos2$Producto)))
##   P maximo  : 62.1605 g/L
cat(sprintf("  P(47h)    : %.4f g/L\n", tail(datos2$Producto,1)))
##   P(47h)    : 62.1605 g/L
cat(sprintf("  X maximo  : %.4f g/L\n", max(datos2$Biomasa)))
##   X maximo  : 8.5246 g/L
cat(sprintf("  V final   : %.4f L\n",   tail(datos2$Volumen,1)))
##   V final   : 7.9000 L

3.2.6 Metricas — Solucion 2

<div class="result-value green">62.16</div>
<div class="result-label">P(47h) — g/L</div>
<div class="result-value green">0.3200</div>
<div class="result-label">F* optimo (L/h)</div>
<div class="result-value">8.5246</div>
<div class="result-label">Biomasa maxima (g/L)</div>
<div class="result-value orange">7.900</div>
<div class="result-label">Volumen final (L)</div>
<div class="result-value">1.3226</div>
<div class="result-label">Productividad (g/L/h)</div>

3.2.7 Graficas — Solucion 2

p_F2 <- plot_ly(datos2, x=~Tiempo, y=~Flujo, type='scatter', mode='lines',
                fill='tozeroy', fillcolor='rgba(46,175,125,0.15)',
                line=list(color='#2eaf7d', width=2.5), name='F (L/h)') %>%
  layout(xaxis=list(title="Tiempo (h)"), yaxis=list(title="F (L/h)"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff",
         annotations=list(list(x=30, y=F_opt*0.5,
           text=paste0("F* = ",round(F_opt,3)," L/h<br>t entre [20,40]h"),
           showarrow=FALSE, font=list(size=11,color="#065f46"),
           bgcolor="rgba(209,250,229,0.85)")))
p_D2 <- plot_ly(datos2, x=~Tiempo, y=~Dilucion, type='scatter', mode='lines',
                fill='tozeroy', fillcolor='rgba(46,175,125,0.12)',
                line=list(color='#059669', width=2.5), name='D (1/h)') %>%
  layout(xaxis=list(title="Tiempo (h)"), yaxis=list(title="D (1/h)"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff")
subplot(p_F2, p_D2, nrows=1, shareX=FALSE, titleY=TRUE, margin=0.07) %>%
  layout(title=list(text=paste0("Sol. 2 — Flujo optimo F* = ",round(F_opt,3)," L/h"),
                    font=list(size=14, color="#1a6b8a")))
plot_ly(datos2, x=~Tiempo) %>%
  add_trace(y=~Biomasa,  type='scatter', mode='lines',
            line=list(color='#1d4ed8',width=2.5), name='X (g/L)', yaxis='y') %>%
  add_trace(y=~Sustrato, type='scatter', mode='lines',
            line=list(color='#ea580c',width=2.5), name='S (g/L)', yaxis='y2') %>%
  add_trace(y=~Producto, type='scatter', mode='lines',
            line=list(color='#059669',width=2.5), name='P (g/L)', yaxis='y2') %>%
  add_trace(y=~Volumen,  type='scatter', mode='lines',
            line=list(color='#7c3aed',width=1.8,dash='dot'), name='V (L)', yaxis='y') %>%
  layout(title=list(text="Sol. 2 — X, S, P, V vs Tiempo (F* optimo)",
                    font=list(size=14, color="#1a6b8a")),
         xaxis=list(title="Tiempo (h)", domain=c(0,0.88)),
         yaxis =list(title="X, V", side="left",
                     titlefont=list(color='#1d4ed8'), tickfont=list(color='#1d4ed8')),
         yaxis2=list(title="S, P (g/L)", side="right", overlaying="y",
                     titlefont=list(color='#ea580c'), tickfont=list(color='#ea580c')),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff",
         legend=list(x=0.01,y=0.98),
         shapes=list(list(type="rect", x0=20, x1=40, y0=0, y1=1,
                          xref="x", yref="paper",
                          fillcolor="rgba(46,175,125,0.08)", line=list(width=0))),
         annotations=list(list(x=21, y=0.97, xref="x", yref="paper",
                               text="Alimentacion encendida",
                               showarrow=FALSE,
                               font=list(size=11,color="#065f46"),
                               bgcolor="rgba(209,250,229,0.8)")))

3.3 Solucion 3 — Pulsos de Alimentacion

Solucion 3 5 pulsos de 4 h a flujo F* entre t = 20 y t = 40 h

Patron de alimentacion por pulsos: Entre t=20 h y t=40 h se aplican 5 ciclos de: 4 horas ON (flujo = F) alternados con 4 horas OFF (flujo = 0). El flujo durante cada pulso es el mismo F optimo de la Solucion 2.

Periodo t inicio t fin Estado
Pulso 1 20 h 24 h ON (F*)
Pausa 1 24 h 28 h OFF
Pulso 2 28 h 32 h ON (F*)
Pausa 2 32 h 36 h OFF
Pulso 3 36 h 40 h ON (F*)
datos3 <- simular_biorreactor(parametros, modo="pulsos",
                               F_const=F_opt, t_feed_on=20, t_feed_off=40, dur_pulso=4)

cat("=== SOLUCION 3: Pulsos (F* =", round(F_opt,4), "L/h) ===\n")
## === SOLUCION 3: Pulsos (F* = 0.32 L/h) ===
cat(sprintf("  P maximo  : %.4f g/L\n", max(datos3$Producto)))
##   P maximo  : 57.9993 g/L
cat(sprintf("  P(47h)    : %.4f g/L\n", tail(datos3$Producto,1)))
##   P(47h)    : 57.9993 g/L
cat(sprintf("  X maximo  : %.4f g/L\n", max(datos3$Biomasa)))
##   X maximo  : 7.9591 g/L
cat(sprintf("  V final   : %.4f L\n",   tail(datos3$Volumen,1)))
##   V final   : 5.3400 L

3.3.1 Graficas — Solucion 3

p_F3 <- plot_ly(datos3, x=~Tiempo, y=~Flujo, type='scatter', mode='lines',
                fill='tozeroy', fillcolor='rgba(124,58,237,0.15)',
                line=list(color='#7c3aed', width=2.5), name='F (L/h)') %>%
  layout(xaxis=list(title="Tiempo (h)"), yaxis=list(title="F (L/h)"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff")
p_D3 <- plot_ly(datos3, x=~Tiempo, y=~Dilucion, type='scatter', mode='lines',
                fill='tozeroy', fillcolor='rgba(124,58,237,0.12)',
                line=list(color='#6d28d9', width=2.5), name='D (1/h)') %>%
  layout(xaxis=list(title="Tiempo (h)"), yaxis=list(title="D (1/h)"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff")
subplot(p_F3, p_D3, nrows=1, shareX=FALSE, titleY=TRUE, margin=0.07) %>%
  layout(title=list(text="Sol. 3 — Flujo y Dilucion (5 Pulsos)",
                    font=list(size=14, color="#1a6b8a")))
plot_ly(datos3, x=~Tiempo) %>%
  add_trace(y=~Biomasa,  type='scatter', mode='lines',
            line=list(color='#1d4ed8',width=2.5), name='X (g/L)', yaxis='y') %>%
  add_trace(y=~Sustrato, type='scatter', mode='lines',
            line=list(color='#ea580c',width=2.5), name='S (g/L)', yaxis='y2') %>%
  add_trace(y=~Producto, type='scatter', mode='lines',
            line=list(color='#7c3aed',width=2.5), name='P (g/L)', yaxis='y2') %>%
  add_trace(y=~Volumen,  type='scatter', mode='lines',
            line=list(color='#059669',width=1.8,dash='dot'), name='V (L)', yaxis='y') %>%
  layout(title=list(text="Sol. 3 — X, S, P, V vs Tiempo (Pulsos)",
                    font=list(size=14, color="#1a6b8a")),
         xaxis=list(title="Tiempo (h)", domain=c(0,0.88)),
         yaxis =list(title="X, V", side="left",
                     titlefont=list(color='#1d4ed8'), tickfont=list(color='#1d4ed8')),
         yaxis2=list(title="S, P (g/L)", side="right", overlaying="y",
                     titlefont=list(color='#ea580c'), tickfont=list(color='#ea580c')),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff",
         legend=list(x=0.01,y=0.98),
         shapes=list(list(type="rect", x0=20, x1=40, y0=0, y1=1,
                          xref="x", yref="paper",
                          fillcolor="rgba(124,58,237,0.06)", line=list(width=0))),
         annotations=list(list(x=21, y=0.97, xref="x", yref="paper",
                               text="Ventana de pulsos",
                               showarrow=FALSE,
                               font=list(size=11,color="#4c1d95"),
                               bgcolor="rgba(237,233,254,0.85)")))

3.4 Comparacion General

Sol 1Sol 2Sol 3 Comparacion directa de las tres estrategias

3.4.1 Tabla de resultados

Tabla comparativa de las tres estrategias de operacion.
Estrategia P(47h) g/L P max g/L X max g/L V fin L
Sol. 1 — ON/OFF (F=0.4 L/h) 56.513 56.513 7.7603 4.000
Sol. 2 — F* optimo (0.32 L/h, continuo) 62.161 62.161 8.5246 7.900
Sol. 3 — Pulsos F*=0.32 L/h (5x4h) 57.999 57.999 7.9591 5.340

3.4.2 Comparacion de Producto P

plot_ly() %>%
  add_trace(data=datos1, x=~Tiempo, y=~Producto, type='scatter', mode='lines',
            line=list(color='#2563eb', width=2.5), name='Sol.1 — ON/OFF') %>%
  add_trace(data=datos2, x=~Tiempo, y=~Producto, type='scatter', mode='lines',
            line=list(color='#059669', width=2.5), name=paste0('Sol.2 — F*=',round(F_opt,3))) %>%
  add_trace(data=datos3, x=~Tiempo, y=~Producto, type='scatter', mode='lines',
            line=list(color='#7c3aed', width=2.5), name='Sol.3 — Pulsos') %>%
  layout(title=list(text="Comparacion: Produccion de 1,3-PDO — Tres Estrategias",
                    font=list(size=14, color="#1a6b8a")),
         xaxis=list(title="Tiempo (h)", gridcolor="#eaeaea"),
         yaxis=list(title="P (g/L)",    gridcolor="#eaeaea"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff",
         legend=list(x=0.02, y=0.98),
         shapes=list(list(type="rect", x0=20, x1=40, y0=0, y1=1,
                          xref="x", yref="paper",
                          fillcolor="rgba(200,200,200,0.12)", line=list(width=0))),
         annotations=list(list(x=21, y=0.97, xref="x", yref="paper",
                               text="Ventana de alimentacion",
                               showarrow=FALSE,
                               font=list(size=10,color="#555"))))

3.4.3 Comparacion de Biomasa X

plot_ly() %>%
  add_trace(data=datos1, x=~Tiempo, y=~Biomasa, type='scatter', mode='lines',
            line=list(color='#2563eb',width=2), name='Sol.1') %>%
  add_trace(data=datos2, x=~Tiempo, y=~Biomasa, type='scatter', mode='lines',
            line=list(color='#059669',width=2), name='Sol.2') %>%
  add_trace(data=datos3, x=~Tiempo, y=~Biomasa, type='scatter', mode='lines',
            line=list(color='#7c3aed',width=2), name='Sol.3') %>%
  layout(title=list(text="Comparacion: Biomasa X",
                    font=list(size=13, color="#1a6b8a")),
         xaxis=list(title="Tiempo (h)"), yaxis=list(title="X (g/L)"),
         paper_bgcolor="#fafcff", plot_bgcolor="#fafcff")

3.4.4 Discusion comparativa

Solucion 2 (F* optimo continuo): Maximiza P(47h) al equilibrar el aporte de sustrato con la dilucion del caldo. El flujo optimo F* = 0.32 L/h es el punto donde la derivada parcial dP/dF = 0, obtenido numericamente por grid search.

Solucion 1 (ON/OFF): Simple y robusta pero no optimiza P. Los ciclos entre S=20 y S=54.45 g/L mantienen el sustrato en un rango operativo sin maximizar la produccion especifica.

Solucion 3 (Pulsos): Los periodos alternos de alimentacion y lote generan ciclos de dilucion seguidos de recuperacion de concentraciones. Puede ser competitiva si la cinetica de produccion es favorecida durante el rebote post-dilucion.


4 Comparacion con Kaur (2012)

Tabla. Comparacion con datos reportados por Kaur (2012).
Variable Kaur (2012) Sol. 1 Sol. 2 Sol. 3
Biomasa maxima (g/L) ~5-6 7.76 8.52 7.96
Glicerol final (g/L) ~0 (agotado) 0.0000 1.1166 0.0000
1,3-PDO maximo (g/L) ~70-75 56.51 62.16 58.00
Tiempo de fermentacion (h) ~60 31.46 47.00 47.00

Nota: Kaur (2012) opera hasta ~60 h con un protocolo distinto. Las diferencias cuantitativas se explican por el t_max=47 h y el V_final=4 L del ejercicio. La tendencia cualitativa es consistente: biomasa con maximo intermedio, consumo progresivo de glicerol y acumulacion monotona de 1,3-PDO.


5 Conclusiones

  1. Tres estrategias integradas en un documento navegable por pestanas: control ON/OFF, flujo optimo y pulsos.

  2. La optimizacion (Sol. 2) demuestra que existe F* = 0.32 L/h que maximiza P(47h). El trade-off entre aporte de sustrato y dilucion genera un optimo identificable visualmente en la curva P vs F.

  3. El metodo grid search es una herramienta simple pero poderosa para optimizar sistemas con una sola variable de decision (F). Para multiples variables podria usarse optim() de R.

  4. Los pulsos (Sol. 3) representan una alternativa practica que evita el control continuo, con resultados que dependen de la cinetica especifica del sistema.

  5. Automatizacion: Al modificar cualquier parametro en el bloque parametros-globales, las tres simulaciones y la optimizacion se recalculan automaticamente al re-compilar el documento.


6 Referencias

  • Kaur, G., Srivastava, A. K., & Chand, S. (2012). Advances in biotechnological production of 1,3-propanediol. Biochemical Engineering Journal, 64, 106-118.
  • Monod, J. (1949). The growth of bacterial cultures. Annual Review of Microbiology, 3(1), 371-394.
  • Luong, J. H. T. (1987). Generalization of Monod kinetics for analysis of growth data with substrate inhibition. Biotechnology and Bioengineering, 29(2), 242-248.

Generado con R Markdown · 2026-05-28 11:02 · Ingenieria de Bioprocesos