Skip to content

Commit f84e4de

Browse files
committed
fix infeasible default aerobic bioreactor values; add tests
1 parent 9278c44 commit f84e4de

7 files changed

Lines changed: 204 additions & 23 deletions

File tree

biosteam/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
1414
"""
1515
from __future__ import annotations
16-
__version__ = '2.53.3'
16+
__version__ = '2.53.4'
1717

1818
#: Chemical engineering plant cost index (defaults to 567.5 at 2017).
1919
CE: float = 567.5

biosteam/units/abstract_stirred_tank_reactor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class AbstractStirredTankReactor(PressureVessel, Unit, isabstract=True):
5151
Fraction of working volume over total volume. Defaults to 0.8.
5252
V_max :
5353
Maximum volume of a reactor [m3]. Defaults to 355.
54+
length_to_diameter :
55+
Length to diameter ratio of bioreactor.
5456
kW_per_m3 :
5557
Power usage of agitator. Defaults to 0.985 [kW / m3] converted from
5658
5 hp/1000 gal as in [1]_, for liquid–liquid reaction or extraction.

biosteam/units/aerated_bioreactor.py

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ class AeratedBioreactor(AbstractStirredTankReactor):
7575
Operating pressure [Pa].
7676
V_wf :
7777
Fraction of working volume over total volume. Defaults to 0.8.
78+
length_to_diameter :
79+
Length to diameter ratio of bioreactor.
7880
V_max :
7981
Maximum volume of a reactor [m3]. Defaults to 355.
8082
kW_per_m3 :
@@ -205,15 +207,23 @@ def _init(
205207
AbstractStirredTankReactor._init(self, **kwargs)
206208
self.theta_O2 = theta_O2 # Average concentration of O2 in the liquid as a fraction of saturation.
207209
self.Q_O2_consumption = Q_O2_consumption # Forced duty per O2 consummed [kJ/kmol].
208-
self.optimize_power = True if optimize_power is None else optimize_power
209210
if design is None:
210-
design = 'Stirred tank'
211+
if self.kW_per_m3 == 0:
212+
design = 'Bubble column'
213+
else:
214+
design = 'Stirred tank'
211215
elif design not in aeration.kLa_method_names:
212216
raise ValueError(
213217
f"{design!r} is not a valid design; only "
214218
f"{list(aeration.kLa_method_names)} are valid"
215219
)
216220
self.design = design
221+
self.optimize_power = (
222+
design == 'Stirred tank'
223+
if optimize_power is None else
224+
optimize_power
225+
)
226+
self.design = design
217227
if method is None:
218228
method = self.default_methods[design]
219229
if (key:=(design, method)) in aeration.kLa_methods:
@@ -271,7 +281,10 @@ def get_agitation_power(self, kLa):
271281
self.superficial_gas_flow = U = F / A # m / s
272282
return aeration.P_at_kLa_Riet(kLa, V, U, **self.kLa_kwargs)
273283
else:
274-
raise NotImplementedError('kLa method has not been implemented in BioSTEAM yet')
284+
raise NotImplementedError(
285+
'cannot solve for the required agitation power using the '
286+
f'kLa method {self.kLa!r} in BioSTEAM yet'
287+
)
275288

276289
def _get_duty(self):
277290
if self.Q_O2_consumption is None:
@@ -368,11 +381,31 @@ def air_flow_rate_objective(O2):
368381
return OUR - self.get_OTR()
369382

370383
f = air_flow_rate_objective
384+
x0 = OUR
371385
y0 = air_flow_rate_objective(OUR)
372-
if y0 <= 0.: # Correlation is not perfect and special cases lead to OTR > OUR
373-
return
374-
flx.IQ_interpolation(f, x0=OUR, x1=10 * OUR,
375-
y0=y0, ytol=1e-3, xtol=1e-3)
386+
387+
# Correlation is not perfect and special cases lead to OTR > OUR
388+
if y0 <= 0.: return
389+
390+
f = air_flow_rate_objective
391+
x1 = 10 * OUR
392+
y1 = air_flow_rate_objective(x1)
393+
394+
# It is possible an infinite flow rate of air is not enough to
395+
# satisfy mass transfer if the titer is super high or gas O2 concentration
396+
# is too low.
397+
if y1 > 0:
398+
raise RuntimeError(
399+
'bioreactor conversion/titer cannot be satisfied; '
400+
'even an infinite flow rate of gas is not enough'
401+
)
402+
403+
# There is a known solution because y1 < 0 < y0
404+
flx.IQ_interpolation(
405+
f, x0=x0, x1=x1,
406+
y0=y0, y1=y1,
407+
ytol=1e-3, xtol=1e-3
408+
)
376409

377410
def _run_reactions(self, effluent):
378411
self.reactions.force_reaction(effluent)
@@ -422,7 +455,7 @@ def get_OTR(self):
422455
return OTR
423456

424457
def _inlet_air_pressure(self):
425-
AbstractStirredTankReactor._design(self)
458+
AbstractStirredTankReactor._design(self, size_only=True)
426459
liquid = bst.Stream(None, thermo=self.thermo)
427460
liquid.mix_from([i for i in self.ins if i.phase != 'g'], energy_balance=False)
428461
liquid.copy_thermal_condition(self.outs[0])
@@ -433,14 +466,7 @@ def _inlet_air_pressure(self):
433466
def _design(self):
434467
AbstractStirredTankReactor._design(self)
435468
if self.air.isempty(): return
436-
liquid = bst.Stream(None, thermo=self.thermo)
437-
liquid.mix_from([i for i in self.ins if i.phase != 'g'], energy_balance=False)
438-
liquid.copy_thermal_condition(self.outs[0])
439-
rho = liquid.rho
440-
length = self.get_design_result('Length', 'm') * self.V_wf
441-
compressor = self.compressor
442-
compressor.P = g * rho * length + 101325
443-
compressor.simulate()
469+
self.compressor.simulate()
444470
air_cooler = self.air_cooler
445471
air_cooler.T = self.T
446472
air_cooler.simulate()
@@ -508,6 +534,8 @@ class GasFedBioreactor(AbstractStirredTankReactor):
508534
Operating pressure [Pa].
509535
V_wf :
510536
Fraction of working volume over total volume. Defaults to 0.8.
537+
length_to_diameter :
538+
Length to diameter ratio of bioreactor.
511539
V_max :
512540
Maximum volume of a reactor [m3]. Defaults to 355.
513541
kW_per_m3 :
@@ -762,7 +790,8 @@ def _run(self):
762790
vent.T = effluent.T = self.T
763791
vent.empty()
764792
vent.phase = 'g'
765-
if not self.titer:
793+
if not self.titer:
794+
# Titer is given by the mass transfer.
766795
liquid_feeds = [i for i in self.ins if i.phase != 'g']
767796
T = self.T
768797
P = self._inlet_gas_pressure()
@@ -776,6 +805,7 @@ def _run(self):
776805
vent.empty()
777806
self._run_vent(vent, effluent)
778807
elif variable_gas_feeds:
808+
# Solve gas flow rates to meet titer.
779809
liquid_feeds = [i for i in self.ins if i.phase != 'g']
780810
effluent.mix_from(liquid_feeds, energy_balance=False)
781811
T = self.T
@@ -806,7 +836,13 @@ def load_flow_rates(F_feeds):
806836

807837
baseline_feed = bst.Stream.sum(self.normal_gas_feeds, energy_balance=False)
808838
baseline_flows = baseline_feed.get_flow('mol/s', self.gas_substrates)
809-
bounds = np.array([[max(1.01 * SURs[i] - baseline_flows[i], 1e-6), 10 * SURs[i]] for i in index])
839+
840+
# Bounds must meet substrate uptake rate (minimally).
841+
# At most, 10x the substrate uptake rate as an abritrarily high number.
842+
bounds = np.array([
843+
[max(1.01 * SURs[i] - baseline_flows[i], 1e-6), 10 * SURs[i]]
844+
for i in index
845+
])
810846
if self.optimize_power:
811847
def total_power_at_substrate_flow(F_substrates):
812848
load_flow_rates(F_substrates)
@@ -837,8 +873,13 @@ def gas_flow_rate_objective(F_substrates):
837873
bounds = bounds.T
838874
results = least_squares(f, 1.2 * SURs, bounds=bounds, ftol=SURs.min() * 1e-6)
839875
self._results = results
876+
if not results.success:
877+
raise RuntimeError(
878+
'bioreactor conversion/titer could not be satisfied'
879+
)
840880
load_flow_rates(results.x / x_substrates)
841-
else:
881+
else:
882+
# Titer given, must adjust liquid flows to meet mass transfer.
842883
try:
843884
feed, = [i for i in self.ins if i.phase != 'g']
844885
except:
@@ -871,8 +912,6 @@ def f(vent_flow_rates):
871912
return effluent.imass[product] / effluent.ivol['Water'] - titer
872913

873914
flx.IQ_interpolation(liquid_flow_rate_objective, 0.05 * F_liquid_max, F_liquid_max, ytol=1e-3)
874-
# self.show()
875-
# breakpoint()
876915

877916
def _run_reactions(self, effluent):
878917
data = effluent.get_data()

biosteam/units/anaerobic_bioreactor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class AnaerobicBioreactor(AbstractStirredTankReactor):
4141
Operating pressure [Pa].
4242
V_wf :
4343
Fraction of working volume over total volume. Defaults to 0.8.
44+
length_to_diameter :
45+
Length to diameter ratio of bioreactor.
4446
V_max :
4547
Maximum volume of a reactor [m3]. Defaults to 355.
4648
kW_per_m3 :

biosteam/units/single_phase_reactor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ class SinglePhaseReactor(AbstractStirredTankReactor):
4343
Operating pressure [Pa].
4444
V_wf :
4545
Fraction of working volume over total volume. Defaults to 0.8.
46+
length_to_diameter :
47+
Length to diameter ratio of bioreactor.
4648
V_max :
4749
Maximum volume of a reactor [m3]. Defaults to 355.
4850
kW_per_m3 :

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
name='biosteam',
1212
packages=['biosteam'],
1313
license='MIT',
14-
version='2.53.3',
14+
version='2.53.4',
1515
description='The Biorefinery Simulation and Techno-Economic Analysis Modules',
1616
long_description=open('README.rst', encoding='utf-8').read(),
1717
author='Yoel Cortes-Pena',

tests/test_aerobic_bioreactor.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# -*- coding: utf-8 -*-
2+
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
3+
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
4+
#
5+
# This module is under the UIUC open-source license. See
6+
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
7+
# for license details.
8+
"""
9+
"""
10+
import biosteam as bst
11+
from biorefineries.sugarcane import chemicals
12+
from numpy.testing import assert_allclose
13+
import pytest
14+
15+
def test_jacketed_bubble_column():
16+
# Get arguments ready
17+
bst.settings.set_thermo(chemicals)
18+
feed = bst.Stream(
19+
Water=1.20e+05,
20+
Glucose=2.5e+04,
21+
units='kg/hr',
22+
T=32+273.15
23+
)
24+
rxn = bst.Rxn(
25+
'Glucose + O2 -> H2O + CO2', reactant='Glucose', X=0.5,
26+
correct_atomic_balance=True
27+
)
28+
kwargs = dict(
29+
ins=[feed, bst.Stream('air', phase='g')],
30+
outs=('vent', 'product'), tau=12, V_max=500,
31+
design='Bubble column', reactions=rxn,
32+
)
33+
34+
# Must raise error if optimized power because default
35+
# kLa method does not depend on agitation.
36+
R1 = bst.AeratedBioreactor(optimize_power=True, **kwargs)
37+
with pytest.raises(NotImplementedError):
38+
R1.simulate()
39+
40+
R1 = bst.AeratedBioreactor(**kwargs)
41+
assert not R1.optimize_power, 'optimize power should default to false'
42+
43+
# Test design results
44+
R1.simulate()
45+
expected_results = {
46+
'Reactor volume': 453.1780985777661,
47+
'Batch time': 18.0,
48+
'Loading time': 2.9999999999999996,
49+
'Residence time': 12,
50+
'Length': 56.81446196104029,
51+
'Diameter': 18.93815398701343,
52+
'Weight': 73673.55,
53+
'Wall thickness': 0.39277612951101165
54+
}
55+
for name, expected in expected_results.items():
56+
assert_allclose(R1.design_results[name], expected)
57+
58+
assert R1.design_results['Vessel type'] == 'Vertical', 'vessel type must be vertial'
59+
60+
# Test gas flow rate sensitivity to aspect ratio
61+
air_flow_rates = []
62+
for L2D in (1, 3, 5):
63+
R1.length_to_diameter = L2D
64+
R1.simulate()
65+
air_flow_rates.append(R1.air.F_mass)
66+
67+
assert_allclose(
68+
air_flow_rates,
69+
[378670.3313278283,
70+
188239.2629457292,
71+
137890.44337525323]
72+
)
73+
74+
# Must raise error if titer/yield is not feasible (mass transfer limitation)
75+
R1.length_to_diameter = 0.01
76+
with pytest.raises(RuntimeError): R1.simulate()
77+
78+
79+
def test_jacketed_stirred_tank():
80+
# Get arguments ready
81+
bst.settings.set_thermo(chemicals)
82+
feed = bst.Stream(
83+
Water=1.20e+05,
84+
Glucose=2.5e+04,
85+
units='kg/hr',
86+
T=32+273.15
87+
)
88+
rxn = bst.Rxn(
89+
'Glucose + O2 -> H2O + CO2', reactant='Glucose', X=0.5,
90+
correct_atomic_balance=True
91+
)
92+
kwargs = dict(
93+
ins=[feed, bst.Stream('air', phase='g')],
94+
outs=('vent', 'product'), tau=12, V_max=500,
95+
reactions=rxn,
96+
)
97+
R1 = bst.AeratedBioreactor(**kwargs)
98+
assert R1.design == 'Stirred tank'
99+
assert R1.optimize_power, 'optimize power should default to True'
100+
101+
# Test design results
102+
R1.simulate()
103+
expected_results = {
104+
'Reactor volume': 453.1780985777661,
105+
'Batch time': 18.0,
106+
'Loading time': 2.9999999999999996,
107+
'Residence time': 12,
108+
'Length': 56.81446196104029,
109+
'Diameter': 18.93815398701343,
110+
'Weight': 73673.55,
111+
'Wall thickness': 0.39277612951101165
112+
}
113+
for name, expected in expected_results.items():
114+
assert_allclose(R1.design_results[name], expected)
115+
116+
assert R1.design_results['Vessel type'] == 'Vertical', 'vessel type must be vertial'
117+
118+
# Test gas flow rate sensitivity to aspect ratio
119+
air_flow_rates = []
120+
for L2D in (1, 3, 5):
121+
R1.length_to_diameter = L2D
122+
R1.simulate()
123+
air_flow_rates.append(R1.air.F_mass)
124+
125+
assert_allclose(
126+
air_flow_rates,
127+
[222037.13645703293,
128+
140467.1502394925,
129+
116058.21484691999]
130+
)
131+
132+
133+
if __name__ == '__main__':
134+
test_jacketed_bubble_column()
135+
test_jacketed_stirred_tank()
136+

0 commit comments

Comments
 (0)