23. Bioproduct separation#
The separation and purification of bioproducts from fermentation broths often require a combination of liquid-liquid exatraction (large, non-polar molecules), distillation (small, polar molecules), and adsorption (dilute non-polar products). In this case study, we will build a typical separation process for acetic acid and review BioSTEAM’s capabilities for extraction and distillation.
23.1. Thermodynamic considerations#
Before creating the process, it can be useful to study the physical phenomena being used in the separation. Let’s first plot the phase envelope of acetic acid and water to see if distillation is possible:
[1]:
import biosteam as bst
bst.nbtutorial() # For light-mode diagrams, ignore warnings
[2]:
bst.plot_vle_binary_phase_envelope(['Water', 'AceticAcid'], P=101325)

Water and acetic acid have an azeotrope, so volatility based separation cannot completely separate acetic acid. Now let’s check whether a solvent such as ethyl acetate can be used for liquid-liquid extraction:
[3]:
bst.plot_lle_ternary_diagram(
carrier='Water', solvent='EthylAcetate', solute='AceticAcid', T=298.15, P=101325,
)

Ethyl acetate is a good solvent. Now let’s confirm that we can separate acetic acid from the solvent more easily:
[4]:
bst.plot_vle_binary_phase_envelope(['EthylAcetate', 'AceticAcid'], P=101325)

Confirmed, a liquid-liquid extraction separation process is feasible. Let’s check the volatility of the components to understand which should be the distillate and bottoms products:
[5]:
for i in bst.Chemicals(['Water', 'EthylAcetate', 'AceticAcid']): print(i, round(i.Tb, 2), 'K')
Water 373.12 K
EthylAcetate 350.25 K
AceticAcid 391.05 K
23.2. Glacial acetic acid production#
Ethyl acetate will need to be recoved from the raffinate as the more volatile component (light key). Acetic acid can be recoved from the extact as a the heavy key. Based on this knowledge, we can construct the following flowsheet (adapted from Seader’s Separation Process Principles, 3rd Edition) glacial acetic acid production from dilute acetic acid:
23.3. Preliminary design#
Let’s start by creating a preliminary system using shortcut columns for speed and flexibility:
[6]:
# Define chemicals used in the process
bst.settings.set_thermo(['Water', 'AceticAcid', 'EthylAcetate'])
# Amount of ethyl-acetate to fermentation broth
solvent_feed_ratio = 1.5
# Fermentation broth with dilute acetic acid
acetic_acid_broth = bst.Stream(ID='acetic_acid_broth', AceticAcid=1000, Water=9000, units='kg/hr')
# Solvent
ethyl_acetate = bst.Stream(ID='ethyl_acetate', EthylAcetate=1)
# Products
glacial_acetic_acid = bst.Stream(ID='glacial_acetic_acid')
wastewater = bst.Stream(ID='wastewater')
# Recycles
solvent_recycle = bst.Stream('solvent_rich')
water_rich = bst.Stream('water_rich')
distillate = bst.Stream('raffinate_distillate')
# System and unit operations
with bst.System('AAsep') as sys:
extractor = bst.MultiStageMixerSettlers(
'extractor',
ins=(acetic_acid_broth, ethyl_acetate, solvent_recycle),
outs=('extract', 'raffinate'),
top_chemical='EthylAcetate',
feed_stages=(0, -1, -1),
N_stages=12,
use_cache=True,
)
@extractor.add_specification(run=True)
def adjust_fresh_solvent_flow_rate():
broth = acetic_acid_broth.F_mass
EtAc_recycle = solvent_recycle.imass['EthylAcetate']
EtAc_required = broth * solvent_feed_ratio
if EtAc_required < EtAc_recycle:
solvent_recycle.F_mass *= EtAc_required / EtAc_recycle
EtAc_recycle = solvent_recycle.imass['EthylAcetate']
EtAc_fresh = EtAc_required - EtAc_recycle
ethyl_acetate.imass['EthylAcetate'] = max(
0, EtAc_fresh
)
HX = bst.HXutility(
'extract_heater',
ins=(extractor.extract),
outs=('hot_extract'),
rigorous=True,
V=0,
)
ED = bst.ShortcutColumn(
'extract_distiller',
ins=HX-0,
outs=['', 'acetic_acid'],
LHK=('Water', 'AceticAcid'),
Lr=0.95,
Hr=0.95,
k=1.4,
partial_condenser=False,
)
ED2 = bst.ShortcutColumn(
'acetic_acid_purification',
ins=ED-1,
outs=('', glacial_acetic_acid),
LHK=('EthylAcetate', 'AceticAcid'),
Lr=0.999,
Hr=0.999,
k=1.4,
partial_condenser=False
)
ED.check_LHK = ED2.check_LHK = False
mixer = bst.Mixer(
ins=(ED-0, ED2-0, distillate)
)
HX = bst.HXutility(ins=mixer-0, T=310)
settler = bst.MixerSettler(
'settler',
ins=HX-0,
outs=(solvent_recycle, water_rich),
top_chemical='EthylAcetate',
)
mixer = bst.Mixer(ins=[extractor.raffinate, water_rich])
RD = bst.ShortcutColumn(
'raffinate_distiller',
LHK=('EthylAcetate', 'Water'),
ins=mixer-0,
outs=[distillate, wastewater],
partial_condenser=False,
Lr=0.99,
Hr=0.99,
k=1.5,
)
sys.simulate()
sys.diagram(kind='cluster', format='png')
sys.show()

System: AAsep
Highest convergence error among components in recycle
stream H1-0 after 2 loops:
- flow rate 4.90e-01 kmol/hr (1.2%)
- temperature 0.00e+00 K (0%)
ins...
[0] acetic_acid_broth
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 500
AceticAcid 16.7
[1] ethyl_acetate
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): EthylAcetate 0.225
outs...
[0] wastewater
phase: 'l', T: 372.37 K, P: 101325 Pa
flow (kmol/hr): Water 499
AceticAcid 2.75
EthylAcetate 0.0853
[1] glacial_acetic_acid
phase: 'l', T: 390.75 K, P: 101325 Pa
flow (kmol/hr): Water 0.000126
AceticAcid 13.9
EthylAcetate 0.0352
[7]:
sys.operating_hours = 330 * 24
print('CAPEX', round(sys.installed_equipment_cost / 1e6, 3), 'MMUSD')
print('OPEX', round(sys.material_cost + sys.utility_cost / 1e6, 4), 'MMUSD/yr')
CAPEX 1.752 MMUSD
OPEX 0.8838 MMUSD/yr
23.4. Rigorous design#
To model side draws and feeds at multiple stages, we will need to use rigorous MESH-based (Mass, Equilibrium, Summation, and entHalpy) distillation models. We can look at the design results for the ShortcutColumn to get heuristic design specifications for the rigorous column:
[8]:
print(ED.results())
outlet = ED.reboiler.outs[0]
boilup = outlet['g'].F_mol / outlet['l'].F_mol
distillate, condensate = ED.top_split.outs
split = condensate.F_mol / ED.condenser.outs[0].F_mol # Or from ED.design_results['Reflux']
N_stages = int(ED.design_results['Theoretical stages'])
feed_stage = int(ED.design_results['Theoretical feed stage'])
Distillation Column Units extract_distiller
Electricity Power kW 1.62
Cost USD/hr 0.127
Cooling water Duty kJ/hr -7.78e+06
Flow kmol/hr 5.31e+03
Cost USD/hr 2.59
Low pressure steam Duty kJ/hr 8.21e+06
Flow kmol/hr 212
Cost USD/hr 50.4
Design Theoretical feed stage 7
Theoretical stages 10
Minimum reflux Ratio 0.268
Reflux Ratio 0.376
Actual stages 20
Height ft 40.6
Diameter ft 4.49
Wall thickness in 0.312
Weight lb 8.01e+03
Purchase cost Trays USD 1.8e+04
Tower USD 5.41e+04
Platform and ladders USD 1.72e+04
Condenser - Floating head USD 3.56e+04
Pump - Pump USD 4.34e+03
Pump - Motor USD 415
Reboiler - Floating head USD 2.26e+04
Total purchase cost USD 1.52e+05
Utility cost USD/hr 53.2
[9]:
reflux = bst.Stream('reflux')
# Amount of ethyl-acetate to fermentation broth
solvent_feed_ratio = 1.5
# System and unit operations
with bst.System('AAsep') as sys:
extractor = bst.MultiStageMixerSettlers(
'extractor',
ins=(acetic_acid_broth, ethyl_acetate, solvent_recycle),
outs=('extract', 'raffinate'),
top_chemical='EthylAcetate',
feed_stages=(0, -1, -1),
N_stages=12,
use_cache=True,
)
@extractor.add_specification(run=True)
def adjust_fresh_solvent_flow_rate():
broth = acetic_acid_broth.F_mass
EtAc_recycle = solvent_recycle.imass['EthylAcetate']
EtAc_required = broth * solvent_feed_ratio
if EtAc_required < EtAc_recycle:
solvent_recycle.F_mass *= EtAc_required / EtAc_recycle
EtAc_recycle = solvent_recycle.imass['EthylAcetate']
EtAc_fresh = EtAc_required - EtAc_recycle
ethyl_acetate.imass['EthylAcetate'] = max(
0, EtAc_fresh
)
HX = bst.HXutility(
'extract_heater',
ins=(extractor.extract),
outs=('hot_extract'),
rigorous=True,
V=0,
)
ED = bst.MESHDistillation(
'extract_distiller',
ins=(HX-0, reflux),
outs=('', 'acetic_acid', 'distillate'),
feed_stages=[feed_stage-2, 1],
N_stages=N_stages,
full_condenser=True,
boilup=boilup,
LHK=('Water', 'AceticAcid'),
use_cache=True,
)
ED2 = bst.ShortcutColumn(
'acetic_acid_purification',
ins=ED-1,
outs=('', glacial_acetic_acid),
LHK=('EthylAcetate', 'AceticAcid'),
Lr=0.999,
Hr=0.999,
k=1.4,
partial_condenser=False
)
ED.check_LHK = ED2.check_LHK = False
mixer = bst.Mixer(
ins=(ED-2, ED2-0, distillate)
)
HX = bst.HXutility(ins=mixer-0, T=310)
settler = bst.MixerSettler(
'settler',
ins=HX-0,
outs=('', water_rich),
top_chemical='EthylAcetate',
)
splitter = bst.Splitter(
'splitter',
ins=settler-0,
outs=(reflux, solvent_recycle),
split=split,
)
mixer = bst.Mixer(ins=[extractor.raffinate, water_rich])
RD = bst.ShortcutColumn(
'raffinate_distiller',
LHK=('EthylAcetate', 'Water'),
ins=mixer-0,
outs=[distillate, wastewater],
partial_condenser=False,
Lr=0.99,
Hr=0.99,
k=1.5,
)
sys.set_tolerance(rmol=1e-3, mol=1e-3, subsystems=True)
sys.simulate()
sys.diagram(format='png')
sys.show()

System: AAsep
Highest convergence error among components in recycle
streams {extract_distiller-2, acetic_acid_purification-0, splitter-1, settler-1} after 10 loops:
- flow rate 1.55e-02 kmol/hr (0.05%)
- temperature 1.76e-05 K (5.1e-06%)
ins...
[0] acetic_acid_broth
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 500
AceticAcid 16.7
[1] ethyl_acetate
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): EthylAcetate 0.152
outs...
[0] wastewater
phase: 'l', T: 372.38 K, P: 101325 Pa
flow (kmol/hr): Water 500
AceticAcid 2.47
EthylAcetate 0.085
[1] glacial_acetic_acid
phase: 'l', T: 390.68 K, P: 101325 Pa
flow (kmol/hr): Water 1.68e-05
AceticAcid 14.2
EthylAcetate 0.0453
[2] s9
phase: 'g', T: 344.79 K, P: 101325 Pa
flow: 0
[10]:
sys.operating_hours = 330 * 24
print('CAPEX', round(sys.installed_equipment_cost / 1e6, 3), 'MMUSD')
print('OPEX', round(sys.material_cost + sys.utility_cost / 1e6, 3), 'MMUSD/yr')
CAPEX 1.807 MMUSD
OPEX 0.963 MMUSD/yr