27. Oleochemical Fermentation Process#

The oleochemical industry is focusing on renewable feedstocks and more sustainable pathways of production. Microbial pathways struggle to be market competitive for the production of commodity oleochemicals, but technological breakthroughs in metabolic engineering are making microbial pathways a promising platform. In this case study, we discuss how to model an aerobic fermentation system for producing microbial oil, which can be further upgraded to sustainable aviation fuel or other oleochemicals. The aerobic fermentation system (including the compressor, reactor, agitator, and heat exchange equipment) constitutes the major cost driver in the minimum oil selling price.

The goals of this case study are to:

  • Understand the design basis of sizing an aerated bioreactor in BioSTEAM.

  • Put in practice BioSTEAM’s design algorithms for microbial oil production.

  • Quantify the economic impact of key fermentation performance parameters.

27.1. Design basis#

27.1.1. Configuration#

In this case study, we implement an industry-relevant configuration consisting of a jacketed stirred tank reactor and a compressor (Figure 1). If you feedstock is in the form of diluted sugars (e.g., lignocellusic hydrolysate), you may need to concentrated your feed to achieve the a specified titer. This can be done efficiently through multi-effect evaporation, whereby the steam from the first effect is used to boil the next effect using a vacuum system. Alternatively, if you wish to operate at lower titers, you can dilute the sugars. Here we include both possibilities to be able to evaluate a spectrum of titers. Due to the high risk of bacterial contamination, we choose to operate in batch mode. Fed-batch configurations are also possible and can be modeled in a similar fashion.

Aerated bioreactor configuration

27.1.2. Mass transfer and mixing#

Aerobic processes demand an oxygen uptake rate (OUR) which is satisfied by the oxygen transfer rate (OTR) under quasi steady-state. The basic mass tranfer equation is:

\[OTR = k_La(C_{out} - C)\]

For tall vessels (>1 m tall) the log-mean driving force is more accurate. Due to the oxygen partial pressure gradient, the local concentration and the saturation concentration are different at the top and bottom of the reactor:

\[OTR = k_La \frac{(C_{sat} - C)_{out} - (C_{sat} - C)_{in}}{ln\Big(\large \frac{(C_{sat} - C)_{out}}{(C_{sat} - C)_{in}} \Big)}\]

The \(k_La\) can be correlated to the superficail gas velocity, \(U_S\), and the agitator power per unit volume, \(P\), as follows:

\[k_La = AP^BU_S^C\]

Where A, B, and C are correlation coefficients dependent on the agitator system, broth type, and size dimensions. The detailed design of the agitator is not relevant at this stage of the analysis, but the correlation coefficients will be a function of the agitator design.

Assumptions

  • Assume a dissolved oxygen concentration of 50% saturation. Note that the flow rate of air (or agitation power) required to achieve an oxygen uptake rate equal to the oxygen transfer rate will depend on the oxygen saturation.

  • Set the power consumed by the agitator to 0.2955 kW∙m3, a heuristic value common for industrial homogeneous reactions and comparable to estimates in Aspen plus. Note that is also possible to minimize power consumption by varying flow rate, but this would also lead to a greater capital cost for the compressor.

  • Assume the Van’t Riet’s non-viscous mass transfer correlation is applicable to estimate the overall mass transfer coefficient, kLa (lumped together with the specific interfacial area).

[1]:
theta_O2 = 0.5 # Dissolved oxygen concentration [% saturation]
agitation_power = 0.2955 # [kW / m3]
design = 'Stirred tank' # Reactor type; Alternatively, 'Bubble column'
method = "Riet" # Name of method; Alternatively, 'Dewes' for bubble columns

27.1.3. Heat transfer#

There are two types of bioreactor heat exchanger configurations supported in BioSTEAM: jacketed and recirculation loop.

Recirculation loop

A recirculation loop is an external heat exchanger which recirculates fluid back to the bioreactor. The fluid at the inlet of the heat exchanger is at the reactor operating temperature while the outlet fluid will be at a lower temperature, which depends on the flow rate. We can specify the outlet temperature (which cannot be too low to avoid inactivation of the microbes) and solve for the recirculation flow rate:

\[F = \frac{\mathrm{Duty}}{C_\mathrm{p} T_{\mathrm{outlet}} - C_\mathrm{p} T_{\mathrm{inlet}}}\]

The detailed design of the heat exchanger should follow standard design algorithms, but is not discussed here. In practice, however, recirculation loops are less commonly used due to the sheer stress on cells and gas entrainment in the pump and heat exchanger.

Jacketed vessel

The most commonly used bioreactor heat exchanger configuration is the jacketed vessel. A full jacket provides a constant heat transfer area to the medium. In order to meet the cooling requirement, the flow rate of the cooling agent (e.g., chilled water) can be varied. The greater the flow rate, the greater the overall heat transfer coefficient, and the greater the driving force as well (i.e., the temperature difference across the jacket).

Estimating the overall heat transfer coefficient can be challenging due to its dependence on detailed design of the bioreactor (e.g., agitation) and mixture properties (e.g., density, conductivity). As a preliminary estimate, we can assume that the temperature of the outlet utility is simply the maximum allowable temperature for smooth regeneration (chilled water cannot come back too hot to the cooler). Because the cost of chilled water usage is estimated based on the total heat transfered (not on the actual flow rate), this assumption does not impact our economic analysis.

Assumptions

  • The fermentation temperature is 32 \(^\circ\)C

  • Assume the aerobic fermenter has a high heat-production rate of 110 kcal per mol of O2 consumed.

[2]:
T_operation = 273.15 + 32 # [K]
Q_O2_consumption = -110 * 4184 # [kJ/kmol]
heat_exchanger_configuration = 'jacketed' # Alternatively, 'recirculation loop'

27.1.4. Compressor#

The compressor is needed to pressurize air to make up for the liquid head, and friction losses from the sparger ring and piping, and the pressure drop from the cooler. The cooler is needed because the compressor heats up the air due to thermodynamic inefficiencies. The isentropic efficiency of the compressor for a gas-fed bioreactor in BioSTEAM defaults to a heuristic value of 85%.

Currently, BioSTEAM only accounts for the liquid head and the pressure drop from the heat exchanger (not the sparger ring or piping, yet). Given a heuristic pressure drop for the cooler is 3 psi (20684 Pa), the outlet pressure of the compressor is computed as follows:

\[P = 101325 + g \rho h + \Delta P_{cooler}\]

Where \(g\) is the acceleration due to gravity, \(\rho\) is the liquid density, \(h\) is the height of the fluid, and \(\Delta P_{cooler}\) is the pressure drop of the cooler.

Assumptions

  • Compressor isentropic efficiency is 85%.

  • Neglect friction losses from sparger and piping.

  • The pressure drop across the cooler is 3 psi.

[3]:
cooler_pressure_drop = 20684 # [Pa]
compressor_isentropic_efficiency = 0.85

27.1.5. Reaction stoichiometry#

The production of microbial oil is an aerobic process that can be modeled as the sum of 3 stoichiometric reactions: oil production from sugar, combustion of glucose (respiration), and cell growth. The substrate not converted to cell mass or oil is assumed to be consumed for respiration. Given the yield of triolein, Yp (wt %), and the specific yield of product per unit biomass, Yb (wt %,), the extent of each reaction can be calculated.

Reaction

Stoichiometry (by mol)

Reaction extent

Production:

\[Glucose \rightarrow 0.235\ H_2O + 2.53\ O_2 + 0.118\ Tripalmitin\]
\[X_p=\frac{Y_p}{0.527}\]

aGrowth:

\[Glucose \rightarrow 1.7\ H_2O + 0.655\ CO_2 + 5.35\ Yeast\]
\[X_b= \frac{Y_p}{0.67Y_b}\]

Respiration:

\[Glucose + 6 O_2 \rightarrow 6 H_2O + 6 CO_2\]
\[X_r=1-X_p-X_b\]

aThe molecular formula of yeast is assumed to be \(CH_{1.61}O_{0.56}\)

Assumptions

  • Titer is 27.4 g∙L-1

  • Productivity is 0.31 g∙L-1∙h-1

  • Yield is 18 wt %

  • Specific yield of product per unit biomass is 63.56 wt %

  • The maximum volume of each reactor vessel is 500 \(\text{m}^\text{3}\).

[4]:
V_max = 500 # [m3]
titer = 27.4 # [g / L]
productivity = 0.31 # [g / L / h]
lipid_yield = Y_p = 0.18 # [by wt]
Y_b = 0.6356 # [by wt]

27.2. Bioreactor modeling#

With an introductory understanding of the design principles, we can now model our configuration. The first step we will take is to create the chemicals and formulate the reactions:

[5]:
import biosteam as bst
bst.nbtutorial()
bst.settings.set_thermo([
    bst.Chemical('Glucose', phase='l'),
    bst.Chemical('Water'),
    bst.Chemical('CO2'),
    bst.Chemical('O2'),
    bst.Chemical('N2'),
    bst.Chemical('Tripalmitin', phase='l', Hf=-2468.7),
    bst.Chemical('Yeast',
        phase='s',
        search_db=False,
        formula='CH1.61O0.56',
        rho=1540, # kg/m3
        Cp=1.5, # J/g
        default=True, # Default other properties
    )
])
chemicals = bst.settings.chemicals
for alias in ('Lipid', 'Oil', 'lipid'): chemicals.set_alias('Tripalmitin', alias)
chemicals.set_alias('Yeast', 'cellmass')
chemicals.Lipid.V.method = 'HTCOSTALD'
production = bst.Reaction(
    "Glucose -> H2O + O2 + Lipid", reactant='Glucose', X=1,
    correct_atomic_balance=True,
)
production.product_yield('Lipid', basis='wt', product_yield=lipid_yield)
growth = bst.Reaction(
    'Glucose -> H2O + CO2 + Yeast', 'Glucose', 1,
    correct_atomic_balance=True
)
growth.product_yield('Yeast', basis='wt', product_yield=Y_p/Y_b / (1 - production.X))
respiration = bst.Reaction(
    'Glucose + O2 -> CO2 + H2O', 'Glucose', 1. - growth.X,
    correct_atomic_balance=True
)
reactions = bst.ReactionSystem(
    production,
    bst.ParallelReaction([growth, respiration])
)
reactions.show()
ReactionSystem:
[0]  Reaction (by mol):
     stoichiometry                                         reactant    X[%]
     Glucose -> 0.235 Water + 2.53 O2 + 0.118 Tripalmitin  Glucose    34.14
[1]  ParallelReaction (by mol):
     index  stoichiometry                                  reactant    X[%]
     [0]    Glucose -> 1.7 Water + 0.655 CO2 + 5.35 Yeast  Glucose    64.15
     [1]    Glucose + 6 O2 -> 6 Water + 6 CO2              Glucose    35.85

Now let’s create our aerated bioreactor:

[6]:
effluent = bst.Stream()
AB1 = bst.AeratedBioreactor(
    ins=['feed', bst.Stream('air', phase='g')],
    outs=['vent', effluent],
    design='Stirred tank', method=method,
    V_max=V_max, Q_O2_consumption=Q_O2_consumption,
    T=T_operation, batch=True, reactions=reactions,
    kW_per_m3=agitation_power,
    tau=titer/productivity,
    cooler_pressure_drop=cooler_pressure_drop,
    compressor_isentropic_efficiency=compressor_isentropic_efficiency,
    optimize_power=False,
    heat_exchanger_configuration=heat_exchanger_configuration,
    loading_time=None, # Will assume no upstream storage (constant loading)
)
AB1.target_titer = titer # g / L
AB1.target_productivity = productivity # g / L / h
AB1.target_yield = lipid_yield  # wt %

@AB1.add_specification(run=True)
def update_reaction_time_and_yield():
    AB1.tau = AB1.target_titer / AB1.target_productivity
    production.product_yield('Lipid', basis='wt', product_yield=AB1.target_yield)

AB1.diagram()

The bioreactor system already comes with all the auxiliary units for aeration and heat transfer. All what is left is to add the concentration/dilution system along with an oil recovery system. We create the multi-effect evaporator assuming 5 effects. The required vapor fraction of the first effect will be solved for in a specification. We will leverage a pre-made system for mechanical microbial oil recovery:

[7]:
from flexsolve import fixed_point
from biorefineries.cane.systems import create_lipid_extraction_system

feedstock = bst.Stream(
    Water=0.85, Glucose=0.15,
    total_flow=1e5, units='kg/hr',
    price=0.14 * 0.15,
)
E1 = bst.MultiEffectEvaporator(
    ins=feedstock, outs=('evaporated_feed', 'condensate'),
    V=0, V_definition='First-effect',
    P=(101325, 73581, 50892, 32777, 20000),
)
dilution_water = bst.Stream()
M1 = bst.Mixer(
    ins=(E1-0, dilution_water), outs=AB1.ins[0]
)

def get_titer(): # g/L or kg/m3
    product_mass_flow = effluent.imass['Lipid'] # effluent.get_flow('kg / hr', 'lipid')
    volumetric_flow_rate = effluent.F_vol # effluent.get_total_flow('m3/hr')
    return product_mass_flow / volumetric_flow_rate

def adjust_dilution_water(water):
    dilution_water.imass['Water'] = water
    E1.run_until(AB1, inclusive=True)
    current = get_titer()
    rho = chemicals.Water.rho('l', T=T_operation, P=101325) # kg / m3
    return water + (1./AB1.target_titer - 1./current) * effluent.imass['Lipid'] * rho

@E1.add_bounded_numerical_specification(x0=0, x1=0.15, ytol=1e-3, xtol=1e-6, maxiter=20)
def evaporation(V):
    E1.V = V
    if V == 0:
        needed_water = fixed_point(adjust_dilution_water, 0, xtol=1)
        if needed_water > 0: return AB1.target_titer - get_titer()
    dilution_water.empty()
    E1.run_until(AB1, inclusive=True)
    return AB1.target_titer - get_titer()

lipid_extraction_sys = create_lipid_extraction_system(ins=effluent)
# Optimistic price for defatted biomass; https://doi.org/10.1186/s13068-021-01911-3
lipid_extraction_sys.get_outlet('cellmass').price = 0.5 * 0.68
# microbial_lipids_sys = bst.main_flowsheet.create_system('microbial_lipids_sys')
microbial_lipids_sys = bst.System.from_units(
    'microbial_lipids_sys',
    units=[E1, M1, AB1, *lipid_extraction_sys.units]
)
microbial_lipids_sys.set_tolerance(mol=1e-9, rmol=1e-9)
microbial_lipids_sys.simulate()
microbial_lipids_sys.diagram(auxiliaries=1)

Let’s have a look at the detailed results of the bioreactor:

[8]:
AB1.show('cwt')
print()
AB1.results(basis='SI')
AeratedBioreactor: AB1
ins...
[0] feed  from  Mixer-M1
    phase: 'l', T: 298.15 K, P: 101325 Pa
    flow (%): Glucose  14.3
              Water    85.7
              -------  1.05e+05 kg/hr
[1] air
    phase: 'g', T: 305.15 K, P: 101325 Pa
    flow (%): O2  23.3
              N2  76.7
              --  2.19e+04 kg/hr
outs...
[0] vent
    phase: 'g', T: 305.15 K, P: 101325 Pa
    flow (%): Water  2.73
              CO2    22.6
              O2     13.3
              N2     61.4
              -----  2.74e+04 kg/hr
[1] effluent  to  SolidLiquidsSplitCentrifuge-U401
    phase: 'l', T: 305.15 K, P: 101325 Pa
    flow (%): Water        93
              CO2          0.0192
              O2           0.000483
              N2           0.00109
              Tripalmitin  2.72
              Yeast        4.28
              -----------  9.93e+04 kg/hr

[8]:
Aerated bioreactor Units AB1
Electricity Power kW 3.71e+03
Cost USD/hr 290
Chilled water Duty kJ/hr -2.37e+07
Flow kmol/hr 1.59e+04
Cost USD/hr 119
Design Reactor volume 498
Batch time hr 95.4
Loading time hr 3.99
Number of reactors 24
Residence time hr 88.4
Vessel type Vertical
Length m 17.9
Diameter m 5.96
Weight kg 6.45e+04
Wall thickness m 0.0103
Jacketed diameter m 6.02
Vessel material Stainless steel 316
Purchase cost Vertical pressure vessel (jacketed) (x24) USD 6.7e+06
Platform and ladders (x24) USD 1.58e+06
Compressor - Compressor(s) (x24) USD 6.45e+05
Air cooler - Floating head (x24) USD 2.12e+04
Agitator - Agitator (x24) USD 1.77e+06
Total purchase cost USD 1.07e+07
Installed equipment cost USD 3.27e+07
Utility cost USD/hr 409

With our system complete, let’s proceed to perform TEA using the same preliminary assumptions used in Humbird’s 2011 report for cornstover ethanol:

[9]:
from biorefineries.tea import create_cellulosic_ethanol_tea
microbial_lipids_tea = create_cellulosic_ethanol_tea(microbial_lipids_sys)
lipid_product = lipid_extraction_sys.get_outlet('lipid')
print(f'Minimum selling price (MSP): {microbial_lipids_tea.solve_price(lipid_product):.3g} USD/kg')
Minimum selling price (MSP): 1.25 USD/kg

Now that we have a working model, let’s leverage it to evaluate a landscape of potential fermentation scenarios:

[10]:
model = bst.Model(microbial_lipids_sys) # Setup optimization model
model.parameter('P', AB1, bounds=[101325, 10 * 101325])
model.parameter('kW_per_m3', AB1, bounds=[0.1, 1])
model.parameter('length_to_diameter', AB1, bounds=[1, 12])

@model.indicator
def MSP(): return microbial_lipids_tea.solve_price(lipid_product)

print(
f"""
Heuristic design decisions
--------------------------
MSP -> {MSP():.3g} USD/Kg
Air pressure: {int(AB1.P)} Pa
Agitation power: {AB1.kW_per_m3:.3g} kW/m3
Length to diameter: {AB1.length_to_diameter:.3g}
"""
)

model.optimize(loss=MSP, method='differential evolution')

print(
f"""
Optimized design decisions
--------------------------
MSP -> {MSP():.3g} USD/Kg
Air pressure: {int(AB1.P)} Pa
Agitation power: {AB1.kW_per_m3:.3g} kW/m3
Length to diameter: {AB1.length_to_diameter:.3g}
"""
)

Heuristic design decisions
--------------------------
MSP -> 1.25 USD/Kg
Air pressure: 101325 Pa
Agitation power: 0.295 kW/m3
Length to diameter: 3


Optimized design decisions
--------------------------
MSP -> 1.21 USD/Kg
Air pressure: 101380 Pa
Agitation power: 0.141 kW/m3
Length to diameter: 4.59

Notice how the MSP decreased, but not by much. It turns out that the heuristic design decisions are actually pretty close to the optimal. This is why heuristics can be very powerful.

[11]:
import numpy as np
from matplotlib import pyplot as plt, rcParams
width = 6.6142
aspect_ratio = 0.4
rcParams['figure.figsize'] = (width, width * aspect_ratio)

def MSP_at_yield_productivity_titer(lipid_yield, productivity, titer):
    AB1.target_yield = lipid_yield
    AB1.target_productivity = productivity
    MSP = np.zeros_like(titer)
    for i, value in enumerate(titer):
        AB1.target_titer = value
        microbial_lipids_sys.simulate()
        MSP[i] = microbial_lipids_tea.solve_price(lipid_product)
    return MSP

titer_values = np.array([
    0.5 * titer,
    titer,
    2 * titer,
])
xlim = np.array([0.5 * lipid_yield, 1.5 * lipid_yield])
ylim = np.array([0.5 * productivity, 1.5 * productivity])
X, Y, Z = bst.plots.generate_contour_data(
    MSP_at_yield_productivity_titer,
    xlim=xlim, ylim=ylim,
    args=(titer_values,),
    n=10,
)

# Plot contours
xlabel = "Yield [wt %]"
ylabel = 'Productivity [$g \cdot L^{-1} \cdot h^{-1}$]'
units = r'$g \cdot L^{-1}$'
titles = [f"{round(i, 2)} {units}" for i in titer_values]
xticks = [10, 15, 20, 25]
yticks = [0.16, 0.24, 0.32, 0.40]
metric_bar = bst.plots.MetricBar(
    'MSP', '$USD \cdot kg^{-1}$', plt.cm.get_cmap('viridis_r'),
    bst.plots.rounded_tickmarks_from_data(Z, 4, 0.4, expand=0, p=0.05), 15, 1
)
fig, axes, CSs, CB, other_axes = bst.plots.plot_contour_single_metric(
    100 * X, Y, Z[:, :, None, :], xlabel, ylabel, xticks, yticks, metric_bar,
    titles=titles, fillcolor=None, styleaxiskw=dict(xtick0=False), label=True,
)
../_images/tutorial_Aerated_bioreactor_design_31_0.png

As expected, the yield and productivity are impactful but not the titer. The minimum selling price between 1-4 USD · kg-1 is close to reported costs in literature at similar processing capacities (Karamerou et al. 2021). The bioreactor, however, was heuristically designed and costs could be reduced further. The optimal design of the bioreactor (e.g., height, operating pressure, agitation) is unique for each fermentation performance scenario (i.e., titer, rate, yield) and can significantly impact costs and performance. Let’s optimize the fermenation tank for our baseline scenario.