# -*- coding: utf-8 -*-
"""
.. contents:: :local:
.. autoclass:: biosteam.units.aerated_bioreactor.AeratedBioreactor
.. autoclass:: biosteam.units.aerated_bioreactor.GasFedBioreactor
References
----------
.. [1] Benz, G. T. Optimize Power Consumption in Aerobic Fermenters.
Chem. Eng. Progress 2003, 99 (5), 100–103.
.. [2] Benz, G. T. Bioreactor Design for Chemical Engineers. Chem. Eng.\
Progress 2011, 21–26.
"""
import biosteam as bst
from .stirred_tank_reactor import AbstractStirredTankReactor
from math import pi
import numpy as np
from scipy.constants import g
import flexsolve as flx
from warnings import filterwarnings, catch_warnings
from scipy.optimize import minimize_scalar, minimize, least_squares, differential_evolution
from biosteam.units.design_tools import aeration
__all__ = (
'AeratedBioreactor', 'ABR',
'GasFedBioreactor', 'GFB',
)
[docs]
class AeratedBioreactor(AbstractStirredTankReactor):
"""
Same as StirredTankReactor but includes aeration. The agitator power may
vary to minimize the total power requirement of both the compressor and agitator
yet achieve the required oxygen transfer rate.
Examples
--------
>>> import biosteam as bst
>>> from biorefineries.sugarcane import chemicals
>>> bst.settings.set_thermo(chemicals)
>>> feed = bst.Stream('feed',
... Water=1.20e+05,
... Glucose=2.5e+04,
... units='kg/hr',
... T=32+273.15)
>>> # Model oxygen uptake as combustion
>>> rxn = bst.Rxn('Glucose + O2 -> H2O + CO2', reactant='Glucose', X=0.5, correct_atomic_balance=True)
>>> R1 = bst.AeratedBioreactor(
... 'R1', ins=[feed, bst.Stream('air', phase='g')], outs=('vent', 'product'), tau=12, V_max=500,
... reactions=rxn,
... )
>>> R1.simulate()
>>> R1.show()
AeratedBioreactor: R1
ins...
[0] feed
phase: 'l', T: 305.15 K, P: 101325 Pa
flow (kmol/hr): Water 6.66e+03
Glucose 139
[1] air
phase: 'g', T: 305.15 K, P: 101325 Pa
flow (kmol/hr): O2 1.05e+03
N2 3.93e+03
outs...
[0] vent
phase: 'g', T: 305.15 K, P: 101325 Pa
flow (kmol/hr): Water 136
CO2 416
O2 629
N2 3.93e+03
[1] product
phase: 'l', T: 305.15 K, P: 101325 Pa
flow (kmol/hr): Water 6.94e+03
Glucose 69.4
"""
_N_ins = 2
_N_outs = 2
_ins_size_is_fixed = False
auxiliary_unit_names = (
'compressor',
'air_cooler',
*AbstractStirredTankReactor.auxiliary_unit_names
)
T_default = 273.15 + 32
P_default = 101325
kW_per_m3_default = 0.2955 # Reaction in homogeneous liquid; reference [1]
batch_default = True
default_methods = {
'Stirred tank': 'Riet',
'Bubble column': 'Dewes',
}
def _init(
self, reactions, theta_O2=0.5, Q_O2_consumption=None,
optimize_power=None, design=None, method=None, kLa_kwargs=None,
**kwargs,
):
AbstractStirredTankReactor._init(self, **kwargs)
self.reactions = reactions
self.theta_O2 = theta_O2 # Average concentration of O2 in the liquid as a fraction of saturation.
self.Q_O2_consumption = Q_O2_consumption # Forced duty per O2 consummed [kJ/kmol].
self.optimize_power = True if optimize_power is None else optimize_power
if design is None:
design = 'Stirred tank'
elif design not in aeration.kLa_method_names:
raise ValueError(
f"{design!r} is not a valid design; only "
f"{list(aeration.kLa_method_names)} are valid"
)
self.design = design
if method is None:
method = self.default_methods[design]
if (key:=(design, method)) in aeration.kLa_methods:
self.kLa = aeration.kLa_methods[key]
elif hasattr(method, '__call__'):
self.kLa = method
else:
raise ValueError(
f"{method!r} is not a valid kLa method; only "
f"{aeration.kLa_method_names[design]} are valid"
)
self.kLa_kwargs = {} if kLa_kwargs is None else kLa_kwargs
def get_kLa(self):
if self.kLa is aeration.kLa_stirred_Riet:
V = self.get_design_result('Reactor volume', 'm3') * self.V_wf
operating_time = self.tau / self.design_results.get('Batch time', 1.)
N_reactors = self.parallel['self']
P = 1000 * self.kW_per_m3 * V # W
air_in = self.sparged_gas
D = self.get_design_result('Diameter', 'm')
F = air_in.get_total_flow('m3/s') / N_reactors / operating_time
R = 0.5 * D
A = pi * R * R
self.superficial_gas_flow = U = F / A # m / s
return aeration.kLa_stirred_Riet(P, V, U, **self.kLa_kwargs) # 1 / s
elif self.kLa is aeration.kla_bubcol_Dewes:
V = self.get_design_result('Reactor volume', 'm3') * self.V_wf
operating_time = self.tau / self.design_results.get('Batch time', 1.)
N_reactors = self.parallel['self']
air_in = self.sparged_gas
D = self.get_design_result('Diameter', 'm')
F = air_in.get_total_flow('m3/s') / N_reactors / operating_time
R = 0.5 * D
A = pi * R * R
self.superficial_gas_flow = U = F / A # m / s
feed = self.ins[0]
return aeration.kla_bubcol_Dewes(U, feed.get_property('mu', 'mPa*s'), air_in.get_property('rho', 'kg/m3'))
else:
raise NotImplementedError('kLa method has not been implemented in BioSTEAM yet')
def get_agitation_power(self, kLa):
if self.kLa is aeration.kLa_stirred_Riet:
air_in = self.sparged_gas
N_reactors = self.parallel['self']
operating_time = self.tau / self.design_results.get('Batch time', 1.)
V = self.get_design_result('Reactor volume', 'm3') * self.V_wf
D = self.get_design_result('Diameter', 'm')
F = air_in.get_total_flow('m3/s') / N_reactors / operating_time
R = 0.5 * D
A = pi * R * R
self.superficial_gas_flow = U = F / A # m / s
return aeration.P_at_kLa_Riet(kLa, V, U, **self.kLa_kwargs)
else:
raise NotImplementedError('kLa method has not been implemented in BioSTEAM yet')
def _get_duty(self):
if self.Q_O2_consumption is None:
H_in = sum([i.H for i in self.ins if i.phase != 'g'], self.air_cooler.outs[0].H)
return self.H_out - H_in + self.Hf_out - self.Hf_in
else:
return self.Q_O2_consumption * (
sum([i.imol['O2'] for i in self.ins])
- sum([i.imol['O2'] for i in self.outs])
)
@property
def feed(self):
return self._ins[0]
@property
def air(self):
for i in self._ins:
if i.phase == 'g': return i
@property
def sparged_gas(self):
return self.air_cooler.outs[0]
@property
def vent(self):
return self._outs[0]
def load_auxiliaries(self):
super().load_auxiliaries()
compressor = self.auxiliary(
'compressor', bst.IsentropicCompressor, self.air, eta=0.85, P=2 * 101325
)
self.auxiliary(
'air_cooler', bst.HXutility, compressor-0, T=self.T
)
def _run_vent(self, vent, effluent):
vent.receive_vent(effluent, energy_balance=False, ideal=True)
def _run(self):
air = self.air
if air is None:
air = bst.Stream(phase='g', thermo=self.thermo)
self.ins.insert(1, air)
self.compressor.ins[0] = self.auxlet(air)
feeds = [i for i in self.ins if i.phase != 'g']
vent, effluent = self.outs
air.P = vent.P = effluent.P = self.P
air.T = vent.T = effluent.T = self.T
vent.empty()
vent.phase = 'g'
air.phase = 'g'
air.empty()
compressor = self.compressor
effluent.mix_from(feeds, energy_balance=False)
self._run_reactions(effluent)
effluent_no_air_data = effluent.get_data()
OUR = -effluent.get_flow('mol/s', 'O2') # Oxygen uptake rate
if OUR <= 1e-2:
if OUR > 0: effluent.imol['O2'] = 0.
self._run_vent(vent, effluent)
return
air_cc = self.sparged_gas
air_cc.copy_like(air)
air_cc.P = compressor.P = self._inlet_air_pressure()
air_cc.T = self.T
if self.optimize_power:
def total_power_at_oxygen_flow(O2):
air.set_flow([O2, O2 * 79. / 21.], 'mol/s', ['O2', 'N2'])
air_cc.copy_flow(air) # Skip simulation of air cooler
compressor.simulate()
effluent.set_data(effluent_no_air_data)
effluent.mix_from([effluent, air_cc], energy_balance=False)
vent.empty()
self._run_vent(vent, effluent)
total_power = self._solve_total_power(OUR)
return total_power
f = total_power_at_oxygen_flow
minimize_scalar(f, 1.2 * OUR, bounds=[OUR, 10 * OUR], tol=OUR * 1e-3)
else:
def air_flow_rate_objective(O2):
air.set_flow([O2, O2 * 79. / 21.], 'mol/s', ['O2', 'N2'])
air_cc.copy_flow(air) # Skip simulation of air cooler
compressor.simulate()
effluent.set_data(effluent_no_air_data)
effluent.mix_from([effluent, air_cc], energy_balance=False)
vent.empty()
self._run_vent(vent, effluent)
return OUR - self.get_OTR()
f = air_flow_rate_objective
y0 = air_flow_rate_objective(OUR)
if y0 <= 0.: # Correlation is not perfect and special cases lead to OTR > OUR
return
flx.IQ_interpolation(f, x0=OUR, x1=10 * OUR,
y0=y0, ytol=1e-3, xtol=1e-3)
def _run_reactions(self, effluent):
self.reactions.force_reaction(effluent)
def _solve_total_power(self, OUR): # For OTR = OUR [mol / s]
air_in = self.sparged_gas
N_reactors = self.parallel['self']
operating_time = self.tau / self.design_results.get('Batch time', 1.)
V = self.get_design_result('Reactor volume', 'm3') * self.V_wf
vent = self.vent
P_O2_air = air_in.get_property('P', 'bar') * air_in.imol['O2'] / air_in.F_mol
P_O2_vent = 0. if vent.isempty() else vent.get_property('P', 'bar') * vent.imol['O2'] / vent.F_mol
C_O2_sat_air = aeration.C_O2_L(self.T, P_O2_air) # mol / kg
C_O2_sat_vent = aeration.C_O2_L(self.T, P_O2_vent) # mol / kg
theta_O2 = self.theta_O2
LMDF = aeration.log_mean_driving_force(C_O2_sat_vent, C_O2_sat_air, theta_O2 * C_O2_sat_vent, theta_O2 * C_O2_sat_air)
kLa = OUR / (LMDF * V * self.effluent_density * N_reactors * operating_time)
P = self.get_agitation_power(kLa)
agitation_power_kW = P / 1000
total_power_kW = (agitation_power_kW + self.compressor.power_utility.consumption / N_reactors) / V
self.kW_per_m3 = agitation_power_kW / V
return total_power_kW
def get_OUR(self):
"""Return the oxygen uptake rate in mol/s."""
feeds = [i for i in self.ins if i.phase != 'g']
effluent = self.effluent.copy()
effluent.mix_from(feeds, energy_balance=False)
self._run_reactions(effluent)
return -effluent.get_flow('mol/s', 'O2') # Oxygen uptake rate
def get_OTR(self):
"""Return the oxygen transfer rate in mol/s."""
V = self.get_design_result('Reactor volume', 'm3') * self.V_wf
operating_time = self.tau / self.design_results.get('Batch time', 1.)
N_reactors = self.parallel['self']
kLa = self.get_kLa()
air_in = self.sparged_gas
vent = self.vent
P_O2_air = air_in.get_property('P', 'bar') * air_in.imol['O2'] / air_in.F_mol
P_O2_vent = vent.get_property('P', 'bar') * vent.imol['O2'] / vent.F_mol
C_O2_sat_air = aeration.C_O2_L(self.T, P_O2_air) # mol / kg
C_O2_sat_vent = aeration.C_O2_L(self.T, P_O2_vent) # mol / kg
theta_O2 = self.theta_O2
LMDF = aeration.log_mean_driving_force(C_O2_sat_vent, C_O2_sat_air, theta_O2 * C_O2_sat_vent, theta_O2 * C_O2_sat_air)
OTR = kLa * LMDF * self.effluent_density * V * N_reactors * operating_time # mol / s
return OTR
def _inlet_air_pressure(self):
AbstractStirredTankReactor._design(self)
liquid = bst.Stream(None, thermo=self.thermo)
liquid.mix_from([i for i in self.ins if i.phase != 'g'], energy_balance=False)
liquid.copy_thermal_condition(self.outs[0])
self.effluent_density = rho = liquid.rho
length = self.get_design_result('Length', 'm') * self.V_wf
return g * rho * length + 101325 # Pa
def _design(self):
AbstractStirredTankReactor._design(self)
if self.air.isempty(): return
liquid = bst.Stream(None, thermo=self.thermo)
liquid.mix_from([i for i in self.ins if i.phase != 'g'], energy_balance=False)
liquid.copy_thermal_condition(self.outs[0])
rho = liquid.rho
length = self.get_design_result('Length', 'm') * self.V_wf
compressor = self.compressor
compressor.P = g * rho * length + 101325
compressor.simulate()
air_cooler = self.air_cooler
air_cooler.T = self.T
air_cooler.simulate()
self.parallel['compressor'] = 1
self.parallel['air_cooler'] = 1
# For robust process control, do not include in HXN
for unit in self.auxiliary_units:
for hu in unit.heat_utilities: hu.hxn_ok = False
[docs]
class GasFedBioreactor(AbstractStirredTankReactor):
"""
Same as AbstractStirredTankReactor but includes multiple gas feeds. The agitator power may
vary to minimize the total power requirement of both the compressor and agitator
yet achieve the required oxygen transfer rate.
Examples
--------
>>> import biosteam as bst
>>> bst.settings.set_thermo(['H2', 'CO2', 'N2', 'O2', 'H2O', 'AceticAcid'])
>>> media = bst.Stream(ID='media', H2O=10000, units='kg/hr')
>>> H2 = bst.Stream(ID='H2', phase='g')
>>> fluegas = bst.Stream(ID='fluegas', phase='g')
>>> recycle = bst.Stream(ID='recycle', phase='g', N2=70, CO2=23, H2O=3, O2=4, total_flow=10, units='kg/hr')
>>> # Model acetic acid production from H2 and CO2
>>> rxn = bst.Rxn('H2 + CO2 -> AceticAcid + H2O', reactant='H2', correct_atomic_balance=True)
>>> brxn = rxn.backwards(reactant='AceticAcid')
>>> R1 = bst.GasFedBioreactor(
... 'R1', ins=[media, H2, fluegas, recycle], outs=('vent', 'product'), tau=68, V_max=500,
... reactions=rxn, backward_reactions=brxn,
... feed_gas_compositions={
... 1: dict(H2=100, units='kg/hr'),
... 2: dict(N2=70, CO2=25, H2O=3, O2=2, units='kg/hr'),
... },
... gas_substrates=('H2', 'CO2'),
... titer={'AceticAcid': 5},
... mixins={2: [3]}, # Recycle gets mixed with fluegas
... optimize_power=False,
... kW_per_m3=0.,
... )
>>> R1.simulate()
>>> R1.show()
GasFedBioreactor: R1
ins...
[0] media
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): H2O 555
[1] H2
phase: 'g', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): H2 18.1
[2] fluegas
phase: 'g', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): CO2 1.65
N2 7.27
O2 0.182
H2O 0.485
[3] recycle
phase: 'g', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): CO2 0.0523
N2 0.25
O2 0.0125
H2O 0.0167
outs...
[0] vent
phase: 'g', T: 305.15 K, P: 101325 Pa
flow (kmol/hr): H2 14.8
CO2 0.0324
N2 7.52
O2 0.194
H2O 1.02
AceticAcid 0.000984
[1] product
phase: 'l', T: 305.15 K, P: 101325 Pa
flow (kmol/hr): H2O 556
AceticAcid 0.836
"""
_N_ins = 2
_N_outs = 2
_ins_size_is_fixed = False
auxiliary_unit_names = (
'mixers',
'sparger',
'compressors',
'gas_coolers',
*AbstractStirredTankReactor.auxiliary_unit_names
)
T_default = 273.15 + 32
P_default = 101325
kW_per_m3_default = 0.2955 # Reaction in homogeneous liquid
batch_default = True
default_methods = AeratedBioreactor.default_methods
get_kLa = AeratedBioreactor.get_kLa
get_agitation_power = AeratedBioreactor.get_agitation_power
def _init(self,
reactions, gas_substrates, titer, backward_reactions,
feed_gas_compositions, design=None, method=None, kLa_kwargs=None,
theta=0.5, Q_consumption=None,
optimize_power=None,
mixins=None,
fed_gas_hook=None,
**kwargs,
):
self.fed_gas_hook = fed_gas_hook
self.reactions = reactions
self.backward_reactions = backward_reactions
self.theta = theta # Average concentration of gas substrate in the liquid as a fraction of saturation.
self.Q_consumption = Q_consumption # Forced duty per gas substrate consummed [kJ/kmol].
self.kLa_kwargs = {} if kLa_kwargs is None else kLa_kwargs
self.optimize_power = True if optimize_power is None else optimize_power
self.feed_gas_compositions = feed_gas_compositions # dict[int, dict] Feed index and composition pairs.
self.gas_substrates = gas_substrates
self.titer = titer # dict[str, float] g / L
self.mixins = {} if mixins is None else mixins # dict[int, tuple[int]] Pairs of variable feed gas index and inlets that will be mixed.
AbstractStirredTankReactor._init(self, **kwargs)
if design is None:
if self.kW_per_m3 == 0:
design = 'Bubble column'
else:
design = 'Stirred tank'
elif design not in aeration.kLa_method_names:
raise ValueError(
f"{design!r} is not a valid design; only "
f"{list(aeration.kLa_method_names)} are valid"
)
self.design = design
if method is None:
method = self.default_methods[design]
if (key:=(design, method)) in aeration.kLa_methods:
self.kLa = aeration.kLa_methods[key]
elif hasattr(method, '__call__'):
self.kLa = method
else:
raise ValueError(
f"{method!r} is not a valid kLa method; only "
f"{aeration.kLa_method_names[design]} are valid"
)
def _get_duty(self):
if self.Q_consumption is None:
H_in = sum(
[i.H for i in self.ins if i.phase != 'g']
+ [i.outs[0].H for i in self.gas_coolers]
)
return self.H_out - H_in + self.Hf_out - self.Hf_in
else:
return self.Q_consumption * (
sum([i.imol['O2'] for i in self.ins])
- sum([i.imol['O2'] for i in self.outs])
)
@property
def vent(self):
return self._outs[0]
@property
def variable_gas_feeds(self):
return [(i if isinstance(i, bst.Stream) else self.ins[i]) for i in self.feed_gas_compositions]
@property
def normal_gas_feeds(self):
variable = set(self.variable_gas_feeds)
return [i for i in self.ins if i not in variable and i.phase == 'g']
@property
def liquid_feeds(self):
return [i for i in self.ins if i.phase != 'g']
@property
def sparged_gas(self):
return self.sparger-0
def load_auxiliaries(self):
super().load_auxiliaries()
self.compressors = []
self.gas_coolers = []
self.mixers = []
mixins = self.mixins
for i in self.feed_gas_compositions:
if i in mixins:
other_ins = [self.ins[j] for j in mixins[i]]
mixer = self.auxiliary(
'mixers', bst.Mixer, (self.ins[i], *other_ins),
)
inlet = mixer-0
else:
inlet = self.ins[i]
compressor = self.auxiliary(
'compressors', bst.IsentropicCompressor, inlet, eta=0.85, P=2 * 101325
)
self.auxiliary(
'gas_coolers', bst.HXutility, compressor-0, T=self.T
)
self.auxiliary(
'sparger', bst.Mixer, [i-0 for i in self.gas_coolers]
)
def _run_vent(self, vent, effluent):
vent.receive_vent(effluent, energy_balance=False, ideal=True)
def get_SURs(self, effluent):
F_vol = effluent.F_vol # m3 / hr
produced = bst.Stream(None, thermo=self.thermo)
for ID, concentration in self.titer.items():
produced.imass[ID] = F_vol * concentration
consumed = produced.copy()
self.backward_reactions.force_reaction(consumed)
return consumed.get_flow('mol/s', self.gas_substrates), consumed, produced
def _load_gas_feeds(self):
if self.fed_gas_hook is not None: self.fed_gas_hook()
for i in self.mixers: i.simulate()
for i in self.compressors: i.simulate()
for i in self.gas_coolers: i.simulate()
self.sparger.simulate()
def _run(self):
variable_gas_feeds = self.variable_gas_feeds
for i in variable_gas_feeds: i.phase = 'g'
liquid_feeds = [i for i in self.ins if i.phase != 'g']
vent, effluent = self.outs
vent.P = effluent.P = self.P
vent.T = effluent.T = self.T
vent.empty()
vent.phase = 'g'
effluent.mix_from(liquid_feeds, energy_balance=False)
effluent_liquid_data = effluent.get_data()
SURs, s_consumed, s_produced = self.get_SURs(effluent) # Gas substrate uptake rate [mol / s]
if (SURs <= 1e-2).all():
effluent.imol[self.gas_substrates] = 0.
self._run_vent(vent, effluent)
return
T = self.T
P = self._inlet_gas_pressure()
for i in self.compressors: i.P = P
for i in self.gas_coolers: i.T = T
x_substrates = []
for (i, dct), ID in zip(self.feed_gas_compositions.items(), self.gas_substrates):
gas = self.ins[i]
gas.reset_flow(**dct)
x_substrates.append(gas.get_molar_fraction(ID))
index = range(len(self.gas_substrates))
def load_flow_rates(F_feeds):
for i in index:
gas = variable_gas_feeds[i]
gas.set_total_flow(F_feeds[i], 'mol/s')
self._load_gas_feeds()
effluent.set_data(effluent_liquid_data)
effluent.mix_from([self.sparged_gas, -s_consumed, s_produced, *liquid_feeds], energy_balance=False)
if (effluent.mol < 0).any(): breakpoint()
vent.empty()
self._run_vent(vent, effluent)
baseline_feed = bst.Stream.sum(self.normal_gas_feeds, energy_balance=False)
baseline_flows = baseline_feed.get_flow('mol/s', self.gas_substrates)
bounds = np.array([[max(1.01 * SURs[i] - baseline_flows[i], 0), 10 * SURs[i]] for i in index])
if self.optimize_power:
def total_power_at_substrate_flow(F_substrates):
load_flow_rates(F_substrates)
total_power = self._solve_total_power(SURs)
return total_power
f = total_power_at_substrate_flow
with catch_warnings():
filterwarnings('ignore')
results = minimize(f, 1.2 * SURs, bounds=bounds, tol=SURs.max() * 1e-6)
load_flow_rates(results.x / x_substrates)
else:
def gas_flow_rate_objective(F_substrates):
F_feeds = F_substrates / x_substrates
load_flow_rates(F_feeds)
STRs = self.get_STRs() # Must meet all substrate demands
F_ins = F_substrates + baseline_flows
mask = STRs - F_ins > 0
STRs[mask] = F_ins[mask]
diff = SURs - STRs
diff[diff > 0] *= 1e3 # Force transfer rate to meet uptake rate
SE = (diff * diff).sum()
return SE
f = gas_flow_rate_objective
with catch_warnings():
filterwarnings('ignore')
bounds = bounds.T
results = least_squares(f, 1.2 * SURs, bounds=bounds, ftol=SURs.min() * 1e-6)
# results = differential_evolution(f, bounds=bounds, tol=SURs.min() * 1e-6)
# print('----')
# print(results)
if getattr(self, 'debug', None):
breakpoint()
self._results = results
load_flow_rates(results.x / x_substrates)
# self.show()
# breakpoint()
def _solve_total_power(self, SURs): # For STR = SUR [mol / s]
gas_in = self.sparged_gas
N_reactors = self.parallel['self']
operating_time = self.tau / self.design_results.get('Batch time', 1.)
V = self.get_design_result('Reactor volume', 'm3') * self.V_wf
D = self.get_design_result('Diameter', 'm')
F = gas_in.get_total_flow('m3/s') / N_reactors / operating_time
R = 0.5 * D
A = pi * R * R
self.superficial_gas_flow = U = F / A # m / s
vent = self.vent
Ps = []
for gas_substrate, SUR in zip(self.gas_substrates, SURs):
Py_gas = gas_in.get_property('P', 'bar') * gas_in.imol[gas_substrate] / gas_in.F_mol
Py_vent = 0. if vent.isempty() else vent.get_property('P', 'bar') * vent.imol[gas_substrate] / vent.F_mol
C_sat_gas = aeration.C_L(self.T, Py_gas, gas_substrate) # mol / kg
C_sat_vent = aeration.C_L(self.T, Py_vent, gas_substrate) # mol / kg
theta = self.theta
LMDF = aeration.log_mean_driving_force(C_sat_vent, C_sat_gas, theta * C_sat_vent, theta * C_sat_gas)
kLa = SUR / (LMDF * V * self.effluent_density * N_reactors * operating_time)
Ps.append(aeration.P_at_kLa_Riet(kLa, V, U, **self.kLa_kwargs))
P = max(Ps)
agitation_power_kW = P / 1000
compressor_power_kW = sum([i.power_utility.consumption for i in self.compressors]) / N_reactors
total_power_kW = (agitation_power_kW + compressor_power_kW) / V
self.kW_per_m3 = agitation_power_kW / V
return total_power_kW
def get_STRs(self):
"""Return the gas substrate transfer rate in mol/s."""
V = self.get_design_result('Reactor volume', 'm3') * self.V_wf
operating_time = self.tau / self.design_results.get('Batch time', 1.)
N_reactors = self.parallel['self']
gas_in = self.sparged_gas
kLa = self.get_kLa() # 1 / s
vent = self.vent
P_gas = gas_in.get_property('P', 'bar')
P_vent = vent.get_property('P', 'bar')
STRs = []
for ID in self.gas_substrates:
Py_gas = P_gas * gas_in.imol[ID] / gas_in.F_mol
Py_vent = P_vent * vent.imol[ID] / vent.F_mol
C_sat_gas = aeration.C_L(self.T, Py_gas, ID) # mol / kg
C_sat_vent = aeration.C_L(self.T, Py_vent, ID) # mol / kg
theta = self.theta
LMDF = aeration.log_mean_driving_force(C_sat_vent, C_sat_gas, theta * C_sat_vent, theta * C_sat_gas)
STRs.append(
kLa * LMDF * self.effluent_density * V * N_reactors * operating_time # mol / s
)
return np.array(STRs)
def _inlet_gas_pressure(self):
AbstractStirredTankReactor._design(self)
liquid = bst.Stream(None, thermo=self.thermo)
liquid.mix_from([i for i in self.ins if i.phase != 'g'], energy_balance=False)
liquid.copy_thermal_condition(self.outs[0])
self.effluent_density = rho = liquid.rho
length = self.get_design_result('Length', 'm') * self.V_wf
return g * rho * length + 101325 # Pa
def _design(self):
AbstractStirredTankReactor._design(self)
self.parallel['sparger'] = 1
self.parallel['mixers'] = 1
self.parallel['compressors'] = 1
self.parallel['gas_coolers'] = 1
# For robust process control, do not include in HXN
for unit in self.auxiliary_units:
for hu in unit.heat_utilities: hu.hxn_ok = False
GFB = GasFedBioreactor
ABR = AeratedBioreactor