Simulacion de Biorreactor — Ejercicio 11
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.
\[\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\]
\[\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}\]
# ============================================================
# 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.
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 ===
## P maximo : 56.5132 g/L
## P(47h) : 56.5132 g/L
## X maximo : 7.7603 g/L
## V final : 4.0000 L
## t final : 31.46 h
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))Solucion 2 Flujo constante optimo entre t = 20 h y t = 40 h
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).
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}\]
Se evalua la simulacion completa para cada F en una cuadricula \(F \in [0,\ 2]\) L/h con paso 0.02 L/h. Para cada F se registra P(47 h). El valor que maximiza P(47 h) es F*. Este enfoque es robusto (no depende de la forma de la funcion objetivo) y transparente (se puede visualizar la curva P vs F).
# Grid Search: barrer F de 0 a 2 L/h en pasos de 0.02
F_grid <- seq(0, 2.0, by=0.02)
P_final <- numeric(length(F_grid))
for (k in seq_along(F_grid)) {
d_tmp <- simular_biorreactor(parametros, modo="constante",
F_const=F_grid[k],
t_feed_on=20, t_feed_off=40)
P_final[k] <- tail(d_tmp$Producto, 1)
}
idx_opt <- which.max(P_final)
F_opt <- F_grid[idx_opt]
P_opt <- P_final[idx_opt]
P_ref <- P_final[which.min(abs(F_grid - 0.4))]
cat("=== RESULTADO DE LA OPTIMIZACION ===\n")
cat(sprintf(" F optimo (F*) : %.4f L/h\n", F_opt))
cat(sprintf(" P(47h) con F* : %.4f g/L\n", P_opt))
cat(sprintf(" P(47h) con F=0.4 : %.4f g/L\n", P_ref))
cat(sprintf(" Mejora relativa : +%.2f%%\n", 100*(P_opt-P_ref)/P_ref))## === RESULTADO DE LA OPTIMIZACION ===
## F optimo (F*) : 0.3200 L/h
## P(47h) con F* : 62.1605 g/L
## P(47h) con F=0.4 : 10.7710 g/L
## Mejora relativa : +477.11%
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
))
)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 ===
## P maximo : 62.1605 g/L
## P(47h) : 62.1605 g/L
## X maximo : 8.5246 g/L
## V final : 7.9000 L
<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>
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)")))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) ===
## P maximo : 57.9993 g/L
## P(47h) : 57.9993 g/L
## X maximo : 7.9591 g/L
## V final : 5.3400 L
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)")))Sol 1Sol 2Sol 3 Comparacion directa de las tres estrategias
| 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 |
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"))))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")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.
| 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.
Tres estrategias integradas en un documento navegable por pestanas: control ON/OFF, flujo optimo y pulsos.
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.
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.
Los pulsos (Sol. 3) representan una alternativa practica que evita el control continuo, con resultados que dependen de la cinetica especifica del sistema.
Automatizacion: Al modificar cualquier parametro
en el bloque parametros-globales, las tres simulaciones y
la optimizacion se recalculan automaticamente al re-compilar el
documento.
Generado con R Markdown · 2026-05-28 11:02 · Ingenieria de Bioprocesos