-
Notifications
You must be signed in to change notification settings - Fork 53
Description
Hi, I will post this again because the other entry remained inactive sofar. I know you only have limited time for BioSTEAM and this github, but I would really appreciate your support on this issue, as it's the main unit for the process and of integral importance for my thesis, in which I evaluate BioSTEAM as a tool for LCI creation.
My fermentor is endothermic, and I have tried various fixes but nothing works. The only "success" I had, was arbitrarily adjusting the Hf value of Calcium Lactate, from literature backed value -1686100 J/mol to ex. -1930000 J/mol. This "knob" makes the fermentor unit exothermic. I was not able to spot any other setting to make it work.
**
- Can this be fixed or is it a bug?
- Is the issue that salts can't be properly simulated in Thermosteam? (dissociated species and such)
- Is there any way to "workaround"?**
Below my code (separate py files):
_settings.py:
import thermosteam as tmo
import biosteam as bst
from _chemicals import chemicals
from thermosteam import settings
#Activate chemicals list & register them in bio/thermosteam
tmo.settings.set_thermo(chemicals)
bst.settings.set_thermo(chemicals)
_chemicals.py:
import thermosteam as tmo
import biosteam as bst
from thermosteam import Chemicals, Chemical
def create_chemicals():
chemicals = Chemicals([])
def add_chemical(ID, ref=None, **data):
chemical = Chemical(ID, **data) if ref is None else ref.copy(ID, **data)
chemicals.append(chemical)
return chemical
Water = add_chemical('H2O')
Glucose = add_chemical('Glucose', phase='l', rho=1560, Cp=1.213)
CaCO3 = add_chemical('CaCO3', phase='s', rho=2710, Cp=0.834, default=True)
CalciumLactate = add_chemical('CalciumLactate', phase='l', Hf=-1930000, rho=1494)
lla = add_chemical('L-LacticAcid', phase='l', Hf=-686300, Cp=2.109, rho=1206)
SulfuricAcid = add_chemical('H2SO4', phase='l', rho=1840, Cp=1.38)
CarbonDioxide = add_chemical('CO2', phase='g')
Gypsum = add_chemical('CaSO4_2H2O', phase='s', rho=2320, MW=172.18, Hf=-2023000, Cp=1.081, CAS='10101-41-4', mu = 0.00095, search_db=False)
Biomass_l = add_chemical('Biomass_l', phase='l', formula='CH1.8O0.5N0.2', MW=24.626, rho=1050, Hf=-106800, Cp=1.25, search_db=False)
Biomass_s = add_chemical('Biomass_s', phase='s', formula='CH1.8O0.5N0.2', MW=24.626, rho=1093, Hf=-130412, Cp=1.25, search_db=False) # Hf value taken from bioindustrial park, no literature given
nutrient_N = add_chemical('NH3', phase='l')
Octanol = add_chemical('Octanol', phase='l')
Trioctylamine = add_chemical('Trioctylamine', phase='l')
SodiumHydroxide = add_chemical('NaOH', phase='l')
SodiumSulfate = add_chemical('Na2SO4', phase='l', rho=1200, Cp=3.5, mu = 0.001, search_db=True)
chemicals.compile()
return chemicals
chemicals = create_chemicals()
# show chemical properties
for chemical in chemicals:
chemical.show()
_reactions.py:
import thermosteam as tmo
# Fermentor Unit Parallel Reactions
def fermentation_reaction():
fermentation = tmo.Reaction(reaction='Glucose -> 1.84 L-LacticAcid + 0.512 Biomass_s', reactant='Glucose', X=1, basis='mol', correct_atomic_balance=False)
return fermentation
# NOTE: Homo-lactic fermentation should theoretically yield 2 moles of lactic acid per mole of consumed glucose
# with a theoretical yield of 1 g of product per g of substrate, but the experimental yields are usually
# lower (0.74–0.99 g/g) because a portion of the carbon source is used for biomass production (0.07–0.22 g/g) [AIMS Microbiology, 4(4): 665–684.]
# --> This was used to calculate adjusted coefficients that already implement the empirical yields!
# Separate Neutralization for correct mass balance (has to happen when lla alreaedy formed)
def neutralization_reaction():
neutralization = tmo.Reaction(reaction='2 L-LacticAcid + CaCO3 -> CalciumLactate + CO2 + H2O', reactant='L-LacticAcid', X=1, basis='mol')
return neutralization
# Acidification Tank Unit
def acidification_reaction():
acidification = tmo.Reaction(reaction='CalciumLactate + H2SO4 -> 2 L-LacticAcid + CaSO4_2H2O', reactant='CalciumLactate', X=1, basis='mol')
return acidification
# Caustic Wash
def caustic_wash_reaction():
caustic_wash = tmo.Reaction(reaction='2 NaOH + H2SO4 -> Na2SO4 + 2 H2O', reactant='H2SO4', X=1, basis='mol')
return caustic_wash
_units.py:
import biosteam as bst
import thermosteam as tmo
from biosteam import CSTR
import numpy as np
from _reactions import fermentation_reaction, neutralization_reaction, acidification_reaction
# -----------------------------------CUSTOM CLASSES----------------------------------------
# Custom Fermentor based on CSTR for cont. ferm.
class Fermentor(bst.CSTR):
_N_ins = 1
_N_outs = 2
def __init__(self, ID='Fermentor', ins=None, outs=(), thermo=None, *, T=323.15, **kwargs):
if outs == ():
outs = [None, None] # liquid, gas
super().__init__(ID, ins, outs, thermo=thermo, **kwargs)
self.T=T
self.fermentation_rxn = fermentation_reaction()
self.neutralization_rxn = neutralization_reaction()
def _run(self):
feed = self.ins[0]
liquid_effluent, gas_vent = self.outs
liquid_effluent.copy_like(feed)
# Apply reactions using the class-defined reactions, first parallel fermentation, then neutralization
self.fermentation_rxn(liquid_effluent.mol)
self.neutralization_rxn(liquid_effluent.mol)
# Handle gas separation - remove CO2 from liquid and send to gas vent
gas_vent.copy_flow(liquid_effluent, 'CO2', remove=True)
liquid_effluent.phase = 'l'
gas_vent.phase = 'g'
liquid_effluent.T = gas_vent.T = self.T
# Custom Acidification Reactor
class AcidificationReactor(bst.CSTR):
_N_ins = 2
_N_outs = 1
def __init__(self, ID='', ins=None, outs=(), P=101325, T=298.15, tau=1.0, V=1.0):
bst.CSTR.__init__(self, ID, ins, outs, tau=tau, T=T, P=P)
self.acidification_rxn = acidification_reaction()
def _run(self):
feed, acid = self.ins
effluent = self.outs[0]
# Acid dosing
n_CL = feed.imol['CalciumLactate']
acid.imol['H2SO4'] = n_CL * 1.1
acid.imass['H2O'] = acid.imass['H2SO4'] / 0.93 * 0.07
# Mix and pre‑instantiate phases
effluent.mix_from([feed, acid])
effluent.phases = ('l', 's')
effluent.T = self.T
effluent.P = self.P
self.acidification_rxn(effluent)
# Force gypsum into solid phase (safety net)
n_total = effluent.imol['CaSO4_2H2O']
if n_total:
effluent.imol['l', 'CaSO4_2H2O'] = 0
effluent.imol['s', 'CaSO4_2H2O'] = n_total
# Keep outlet as slurry/liquid
effluent.phase = 'l'
_system.py:
import biosteam as bst
from _units import Fermentor, AcidificationReactor # custom classes
from _reactions import fermentation_reaction, acidification_reaction, caustic_wash_reaction # reaction functions
from _settings import chemicals # compiled thermo package
# -------------------- FEED STREAMS FOR ALL UNITS --------------------
glucose_feed = bst.Stream('glucose_feed', Glucose=80, Water=920, units='kg/hr') # 80 g/L
CaCO3_feed = bst.Stream('CaCO3_feed', CaCO3=44.5, Water=82.6, units='kg/hr') # Based on mol calc. from reaction; 1mol CaCO3 per 1mol Glucose
Nutrient_feed = bst.Stream('Nutrient_feed', NH3 = 1.34, H2O = 3.13, units='kg/hr')
H2SO4_feed = bst.Stream('H2SO4_feed', units='kg/hr')
Organic_Solvent = bst.Stream('Organic_Solvent', Octanol=172.5, Trioctylamine=172.5, units='kg/hr') # 1.5 : 1 ratio -> org : aq phase
Extr_Water_1 = bst.Stream('Extraction_water_1', H2O=100, units='kg/hr') # NOTE How much?
Extr_Water_2 = bst.Stream('Extraction_water_2', H2O=100, units='kg/hr') # NOTE How much?
Caustic_Sol = bst.Stream('Caustic_Solution', NaOH=4, H2O=50, units='kg/hr')
OrgSolvent_Makeup = bst.Stream('Solvent_Makeup', Octanol=1.72, Trioctylamine=1.73, units='kg/hr') # Add the 1% of solvent lost in caustic wash
Recycled_Solvent = bst.Stream('Recycled_Solvent')
# -------------------- SUBSYSTEM 1: CONVERSION --------------------
# Unit and stream definitions
with bst.System('conversion_sys') as conversion_sys:
feedstock_mixer = bst.Mixer('feedstock_mixer', ins=[glucose_feed, CaCO3_feed, Nutrient_feed], outs=('crude_feed'))
feed_pump = bst.Pump('feed_pump', feedstock_mixer.outs[0], outs=('crude_feed'), P=110000) # NOTE: Provisional Pump; missing correct settings
preheater = bst.HXutility('preheater', feed_pump.outs[0], outs=('heated_feed'), T=323.15) # NOTE: Placeholder 50°C
fermentor = Fermentor('fermentor', preheater.outs[0], outs=['fermented_broth', 'CO2'], P=110000, T=323.15, tau=72, V_wf=0.8, kW_per_m3=0.05, dT_hx_loop=35) # NOTE find source for possible kW_per_m3 value for anaerobic ferm.
ferm_cooler = bst.HXutility('fermentor_cooler', fermentor.outs[0], outs=('cooled_broth'), T=298.15)
# -------------------- SUBSYSTEM 2.1: SEPARATION --------------------
# Biomass Removal --> NOTE: Fine tuning of splits, realism vs. idealism
# Acidification & Gypsum Filtration
with bst.System('separation_sys') as separation_sys:
gravity_decanter = bst.units.SolidsSeparator('GravityDecanter',
ins=ferm_cooler.outs[0], outs=('decanted_cake', 'decanted_broth'),
split={'Biomass_s': 1, 'CaCO3': 0.85, 'CalciumLactate': 0.05}, # NOTE: FINE TUNING, HOW MUCH PRODUCT LOST?
moisture_content=0.35)
bm_Filter = bst.units.SolidsSeparator('BM_Filter',
ins=gravity_decanter.outs[0], outs=('BM_cake', 'BM_filtrate'),
split={'Biomass_s': 1, 'CaCO3': 0.75},
moisture_content=0.08)
filtrate_mixer = bst.Mixer('filtrate_mixer', ins=(gravity_decanter.outs[1], bm_Filter.outs[1]), outs='clarified_broth')
acidification_reactor = AcidificationReactor('AcidificationReactor', ins=[filtrate_mixer.outs[0], 'H2SO4_feed'], outs=('acidified_slurry'))
gypsum_filter = bst.units.SolidsSeparator('GypsumFilter', ins=acidification_reactor.outs[0], outs=('gypsum_cake', 'lactic_acid_solution'),
split={'CaSO4_2H2O': 0.99}, # Fine particles stay, removed by extraction
moisture_content=0.2)
# -------------------- SUBSYSTEM 2.2A: PURIFICATION --------------------
# MEE 1 to conc. extraction feed
# Extraction with organic amine solvent
# 2 Stage Back-Extraction into fresh water
# MEE 2 to 88wt% lla
with bst.System('purification_sys') as purification_sys:
Evaporator_1 = bst.units.MultiEffectEvaporator(ID='MEE_pre_extr', ins=[gypsum_filter.outs[1]], outs=('conc_la_sol', 'condensate'),
V=0.85, V_definition='Overall', # NOTE adjust to 30wt% lactic acid conc. before extraction for eff.
P=(101325, 90000, 70000))
ExtrMixer1 = bst.LiquidsMixingTank(ID='Extraction_M1', ins=[Evaporator_1.outs[0], Organic_Solvent], outs=['mixed_phase_1'])
PreExtr_Cooler = bst.HXutility('extraction_cooler', ins=ExtrMixer1.outs[0], T=313.15) # 40°C extr feed for optimal eff.
ExtrSettler1 = bst.LiquidsSplitSettler(ID='Extraction_S1', ins=[PreExtr_Cooler.outs[0]], outs=['organic_phase_1', 'aq_phase_1'],
split={'L-LacticAcid': 0.99, # Simulate two stage extraction in one
'H2SO4': 0.98,
'Octanol': 1,
'Trioctylamine': 1,
'H2O': 0.01, # some water carry-over
'CaCO3': 0,
'CaSO4_2H2O': 0,
'NH3' : 0}) # NOTE "realistic" assumptions of split
BackExtrMixer1 = bst.LiquidsMixingTank('BackExtr_Mixer1', ins=[ExtrSettler1.outs[0], Extr_Water_1])
PreBackExtr_Heater1 = bst.HXutility('backextr_heater1', ins=BackExtrMixer1.outs[0], T=348.15) # ca. 20-30°C higher T for Back Extraction
BackExtrSettler1 = bst.LiquidsSplitSettler('BackExtr_Settler1',
ins=PreBackExtr_Heater1.outs[0],
outs=('back_extr_aq1', 'back_extr_org1'),
split={'L-LacticAcid': 0.85,
'H2SO4': 0,
'H2O': 0.99,
'Octanol': 0,
'Trioctylamine': 0})
BackExtrMixer2 = bst.LiquidsMixingTank('BackExtr_Mixer2', ins=[BackExtrSettler1.outs[1], Extr_Water_2])
PreBackExtr_Heater2 = bst.HXutility('backextr_heater2', ins=BackExtrMixer2.outs[0], T=348.15) # ca. 20-30°C higher T for Back Extraction
BackExtrSettler2 = bst.LiquidsSplitSettler('BackExtr_Settler2',
ins=PreBackExtr_Heater2.outs[0],
outs=('back_extr_aq2', 'back_extr_org2'),
split={'L-LacticAcid': 0.8, # 80% of remaining LLA (total 97%)
'H2SO4': 0,
'H2O': 0.99,
'Octanol': 0,
'Trioctylamine': 0})
AqPhaseMixer = bst.MixTank('PostExtraction_AqPhase', ins=[BackExtrSettler1.outs[0], BackExtrSettler2.outs[0]], outs='pure_lactic_acid_solution')
# -------------------- SUBSYSTEM 2.2B: SOLVENT RECOVERY & RECYCLING --------------------
#Organic solvent washed (neutralized) with aq. NaOH solution
Caustic_Mixer = bst.LiquidsMixingTank('Caustic_Mixer', ins=[BackExtrSettler2.outs[1], Caustic_Sol], outs=('caustic_mix'))
CausticWash_Reactor = bst.SinglePhaseReactor('Caustic_Wash', ins=Caustic_Mixer.outs[0], outs=('neutralized_solvent'),
T=323.15, P=101325, V_wf=0.8,
tau=0.5,
reaction=caustic_wash_reaction())
Caustic_Settler = bst.LiquidsSplitSettler('Caustic_Settler',
ins=CausticWash_Reactor.outs[0],
outs=('caustic_w_waste_brine', 'recovered_solvent'),
split={
'Octanol': 0.01,
'Trioctylamine': 0.01,
'NaOH': 1,
'Na2SO4': 1,
'H2O': 1,
'L-LacticAcid': 1})
OrgSolvent_Recycler = bst.Mixer('Solvent_Mixer', ins=[Caustic_Settler.outs[1], OrgSolvent_Makeup], outs=Organic_Solvent)
# MEE 2
Evaporator_2 = bst.MultiEffectEvaporator(ID='MEE_post_extr', ins=[AqPhaseMixer.outs[0]], outs=('88wt_product', 'condensate'),
V=0.95, V_definition='Overall', P=(101325, 80000, 60000, 30000)) # NOTE More details required, what pressure? PEP not detailed enough... how many effects? only one flash drum?
Evaporator_2.split={
'H2O': 1,
'LacticAcid': 0,
'H2SO4': 0,
'Glucose': 0}
# -------------------- MAIN SYSTEM --------------------
# Combine subsystems into the main system
lactic_acid_system = bst.System('lactic_acid_system',
path=[conversion_sys, separation_sys, purification_sys],
recycle=[Organic_Solvent])
# SIMULATION
if __name__ == '__main__':
lactic_acid_system.simulate()
print("Simulation completed!")
# ------KPI -------
glucose_feed.show()
CaCO3_feed.show()
feedstock_mixer.outs[0].show()
fermentor.ins[0].show()
fermentor.ins[0].T
fermentor.outs[0].show() # Liquid
fermentor.outs[1].show() # Gas
print("--Fermentor PU & HU--")
fermentor.power_utility.show()
fermentor.heat_utilities[0].show()
The output I get for the fermentor duty with different Calcium Lactate Hf values:
Hf = -1930000 --> Chosen arbitrarily to illustrate.
PowerUtility:
consumption: 34.6 kW
production: 0 kW
power: 34.6 kW
cost: 2.71 USD/hr
HeatUtility: chilled_water
duty:-4.57e+04 kJ/hr
flow: 26.7 kmol/hr
cost: 0.202 USD/hr
Hf = -1686100
PowerUtility:
consumption: 46.2 kW
production: 0 kW
power: 46.2 kW
cost: 3.61 USD/hr
HeatUtility: low_pressure_steam
duty: 5.68e+04 kJ/hr
flow: 1.61 kmol/hr
cost: 0.384 USD/hr
As you can see the shift in Hf for CalciumLactate has a big influence.
EDIT: Also note that the duty numbers are quite small, although that could be because of streams being rather small.
EDIT 2: Biomass Hf value is also a knob to make the fermentation more exothermic. I adjusted the Hf value of Biomass_s to match your bioindustrial park lactic acid simulation (from -106800 to -130412 J/mol). That makes the fermentation reaction more exothermic, but not enough to make the fermentor exothermic. I also checked the two reactions above separately for their reaction enthalpies (using rxn.dH) and got the follwing:
Fermentation --> H = -78395.9
Neutralization --> H = 107400.5 (per mole of LA)
Additional thought:
Could this be a limitation on reaction realism in biosteam? As I've read that electrolyte speciation does affect some thermo properties and can distort enthalpies (even to this degree?).
Id really appreciate any support here...