# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
.. contents:: :local:
Abstract Unit Operations
------------------------
.. autoclass:: biosteam.units.liquid_liquid_extraction.LLEUnit
.. autoclass:: biosteam.units.liquid_liquid_extraction.LiquidsCentrifuge
.. autoclass:: biosteam.units.liquid_liquid_extraction.LiquidsSettler
Centrifuges
-----------
.. autoclass:: biosteam.units.liquid_liquid_extraction.LiquidsSplitCentrifuge
.. autoclass:: biosteam.units.liquid_liquid_extraction.LLECentrifuge
.. autoclass:: biosteam.units.liquid_liquid_extraction.SLLECentrifuge
.. autoclass:: biosteam.units.liquid_liquid_extraction.SolidLiquidsSplitCentrifuge
Mixer-Settlers
--------------
.. autoclass:: biosteam.units.liquid_liquid_extraction.LiquidsMixingTank
.. autoclass:: biosteam.units.liquid_liquid_extraction.LLESettler
.. autoclass:: biosteam.units.liquid_liquid_extraction.LiquidsSplitSettler
.. autoclass:: biosteam.units.liquid_liquid_extraction.LiquidsPartitionSettler
.. autoclass:: biosteam.units.liquid_liquid_extraction.MixerSettler
.. autoclass:: biosteam.units.liquid_liquid_extraction.MultiStageMixerSettlers
References
----------
.. [1] Apostolakou, A. A.; Kookos, I. K.; Marazioti, C.; Angelopoulos,
K. C. Techno-Economic Analysis of a Biodiesel Production Process
from Vegetable Oils. Fuel Process. Technol. 2009, 90, 1023−1031
.. [2] Kwiatkowski, J. R.; McAloon, A. J.; Taylor, F.; Johnston, D. B.
Modeling the Process and Costs of Fuel Ethanol Production by the Corn
Dry-Grind Process. Industrial Crops and Products 2006, 23 (3), 288–296.
https://doi.org/10.1016/j.indcrop.2005.08.004.
"""
import biosteam as bst
from .splitting import Splitter
from .design_tools import CEPCI_by_year, geometry, PressureVessel
from .decorators import cost, copy_algorithm
from .stage import MultiStageEquilibrium
from thermosteam._graphics import mixer_settler_graphics
from .. import Unit
from thermosteam import separations as sep
import numpy as np
__all__ = (
'LLEUnit',
'LiquidsCentrifuge',
'LiquidsSplitCentrifuge',
'LiquidsRatioCentrifuge',
'SLLECentrifuge',
'SolidLiquidsSplitCentrifuge',
'LLECentrifuge',
'LiquidsMixingTank',
'LiquidsSettler',
'LLESettler',
'LiquidsSplitSettler',
'LiquidsPartitionSettler',
'MixerSettler',
'MultiStageMixerSettlers',
)
# %% Abstract
[docs]
class LLEUnit(bst.Unit, isabstract=True):
r"""
Abstract class for simulating liquid-liquid equilibrium.
Parameters
----------
ins :
Inlet fluid.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
top_chemical : str, optional
Identifier of chemical that will be favored in the low density phase.
efficiency=1. : float, optional
Fraction of feed in liquid-liquid equilibrium.
The rest of the feed is divided equally between phases.
forced_split_IDs : tuple[str], optional
IDs of component with a user defined split.
forced_split : 1d array, optional
Component-wise split to 0th stream.
Examples
--------
>>> from biorefineries.lipidcane import chemicals
>>> from biosteam import units, settings, Stream
>>> settings.set_thermo(chemicals['Methanol', 'Glycerol', 'Biodiesel', 'TAG'])
>>> feed = Stream('feed', T=333.15,
... TAG=0.996, Biodiesel=26.9,
... Methanol=32.9, Glycerol=8.97)
>>> C1 = units.LLEUnit('C1', ins=feed, outs=('light', 'heavy'))
>>> C1.simulate()
>>> C1.outs[0].show()
Stream: light from <LLEUnit: C1>
phase: 'l', T: 333.15 K, P: 101325 Pa
flow (kmol/hr): Methanol 11.5
Glycerol 0.033
Biodiesel 26.7
TriOlein 0.996
"""
_N_outs = 2
def _init(self, top_chemical=None, efficiency=1.0, forced_split_IDs=None, forced_split=None):
#: [str] Identifier of chemical that will be favored in the low density phase.
self.top_chemical = top_chemical
#: [float] Fraction of feed in liquid-liquid equilibrium.
#: The rest of the feed is divided equally between phases.
self.efficiency = efficiency
#: array[float] Forced splits to 0th stream for given IDs.
self.forced_split = forced_split
#: tuple[str] IDs corresponding to forced splits.
self.forced_split_IDs = forced_split_IDs
self.multi_stream = bst.MultiStream(phases='lL', thermo=self.thermo)
@property
def solvent(self):
return self.top_chemical
@solvent.setter
def solvent(self, solvent):
self.top_chemical = solvent
def _run(self):
if all([i.isempty() for i in self.ins]): return
sep.lle(*self.ins, *self.outs, self.top_chemical, self.efficiency, self.multi_stream)
IDs = self.forced_split_IDs
if IDs:
feed, = self.ins
liquid, LIQUID = self.outs
mol = feed.imol[IDs]
liquid.imol[IDs] = mol_liquid = mol * self.forced_split
LIQUID.imol[IDs] = mol - mol_liquid
# %% Centrifuge
# Electricity kW/(m3/hr) from USDA biosdiesel Super Pro model
# Possibly 1.4 kW/(m3/hr)
# https://www.sciencedirect.com/topics/engineering/disc-stack-centrifuge
# Microalgal fatty acids—From harvesting until extraction H.M. Amaro, et. al.,
# in Microalgae-Based Biofuels and Bioproducts, 2017
[docs]
@cost('Flow rate', units='m^3/hr', CE=525.4, cost=28100,
n=0.574, kW=1.4, ub=100, BM=2.03, N='Number of centrifuges')
class LiquidsCentrifuge(Unit, isabstract=True):
r"""
Abstract class for liquid centrifuges.
Parameters
----------
ins :
Inlet fluid.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
Notes
-----
The f.o.b purchase cost is given by [1]_:
.. math::
C_{f.o.b}^{2007} = 28100 Q^{0.574} \ (Q < 100 \frac{m^3}{h})
"""
_N_outs = 2
line = 'Liquids centrifuge'
# TODO: Remove this in favor of partition coefficients
class LiquidsRatioCentrifuge(LiquidsCentrifuge):
line = 'Liquids centrifuge'
def _init(self,
K_chemicals, Ks, top_solvents=(), top_split=(),
bot_solvents=(), bot_split=()
):
self._load_components()
self.K_chemicals = K_chemicals
self.Ks = Ks
self.top_solvents = top_solvents
self.top_split = top_split
self.bot_solvents = bot_solvents
self.bot_split = bot_split
def _run(self):
feed = self.ins[0]
top, bot = self.outs
indices = self.chemicals.get_index
def flattend(indices, split):
flat_index = []
flat_split = []
integer = int
isa = isinstance
for i, j in zip(indices, split):
if isa(i, integer):
flat_index.append(i)
flat_split.append(j)
else:
flat_index.extend(i)
flat_split.extend([j] * len(i))
return flat_index, np.array(flat_split)
K_index, Ks = flattend(indices(self.K_chemicals), self.Ks)
top_index, top_split = flattend(indices(self.top_solvents), self.top_split)
bot_index, bot_split = flattend(indices(self.bot_solvents), self.bot_split)
top_mol = top.mol; bot_mol = bot.mol; feed_mol = feed.mol
top_mol[top_index] = feed_mol[top_index] * top_split
bot_mol[top_index] = feed_mol[top_index] - top_mol[top_index]
bot_mol[bot_index] = feed_mol[bot_index] * bot_split
top_mol[bot_index] = feed_mol[bot_index] - bot_mol[bot_index]
topnet = top_mol[top_index].sum()
botnet = bot_mol[bot_index].sum()
molnet = topnet+botnet
top_mol[K_index] = Ks * topnet * feed_mol[K_index] / molnet # solvent * mol ratio
bot_mol[K_index] = feed_mol[K_index] - top_mol[K_index]
top.T, top.P = feed.T, feed.P
bot.T, bot.P = feed.T, feed.P
[docs]
class LiquidsSplitCentrifuge(LiquidsCentrifuge):
r"""
Create a liquids centrifuge simulated by component splits.
Parameters
----------
ins :
Inlet fluid.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
split : Should be one of the following
* [float] The fraction of net feed in the 0th outlet stream.
* [array_like] Componentwise split of feed to 0th outlet stream.
* [dict] ID-split pairs of feed to 0th outlet stream.
order=None : Iterable[str], defaults to biosteam.settings.chemicals.IDs
Chemical order of split.
Notes
-----
The f.o.b purchase cost is given by [1]_:
.. math::
C_{f.o.b}^{2007} = 28100 Q^{0.574} (Q < 100 \frac{m^3}{h})
"""
line = 'Liquids centrifuge'
_init = Splitter._init
_run = Splitter._run
split = Splitter.split
isplit = Splitter.isplit
[docs]
class LLECentrifuge(LLEUnit, LiquidsCentrifuge):
r"""
Create a liquids centrifuge simulated by liquid-liquid equilibrium.
Parameters
----------
ins :
Inlet fluid.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
top_chemical : str, optional
Identifier of chemical that will be favored in the low density phase.
efficiency : float,
Fraction of feed in liquid-liquid equilibrium.
The rest of the feed is divided equally between phases.
Notes
-----
The f.o.b purchase cost is given by [1]_:
.. math::
C_{f.o.b}^{2007} = 28100 Q^{0.574} (Q < 100 \frac{m^3}{h})
Examples
--------
>>> from biorefineries.lipidcane import chemicals
>>> from biosteam import units, settings, Stream
>>> settings.set_thermo(chemicals['Methanol', 'Glycerol', 'Biodiesel', 'TAG'])
>>> feed = Stream('feed', T=333.15,
... TAG=0.996, Biodiesel=26.9,
... Methanol=32.9, Glycerol=8.97)
>>> C1 = units.LLECentrifuge('C1', ins=feed, outs=('light', 'heavy'))
>>> C1.simulate()
>>> C1.outs[0].show()
Stream: light from <LLECentrifuge: C1>
phase: 'l', T: 333.15 K, P: 101325 Pa
flow (kmol/hr): Methanol 11.5
Glycerol 0.033
Biodiesel 26.7
TriOlein 0.996
>>> C1.results()
Liquids centrifuge Units C1
Electricity Power kW 17.5
Cost USD/hr 1.37
Design Flow rate m^3/hr 12.5
Purchase cost Liquids centrifuge USD 1.29e+05
Total purchase cost USD 1.29e+05
Utility cost USD/hr 1.37
"""
line = 'Liquids centrifuge'
[docs]
@cost('Flow rate', S=1725.61, units='L/min',
CE=CEPCI_by_year[2007], cost=849000., n=0.6, kW=0.07, ub=2000., BM=2.03,
N='Number of centrifuges')
class SLLECentrifuge(Unit):
"""
Create a SLLECentrifuge object that separates the feed into solid, oil, and
aqueous phases.
Parameters
----------
ins :
feed
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
* [2] Solids.
solids_split : dict[str, float]
Splits to 2nd outlet stream.
top_chemical : str, optional
Identifier of chemical that will be favored in the low density phase.
efficiency : float, optional
Fraction of feed in liquid-liquid equilibrium.
The rest of the feed is divided equally between phases.
Defaults to 1.0.
moisture_content : float, optional
Moisture content of solids. Defaults to 0.5.
Notes
-----
Cost algorithm is based on a 3-phase decanter centrifuge from
a conventional dry-grind corn ethanol plant that separates
aqueous, oil, and solid fractions (i.e. DDGS) from the bottoms product
of the beer column [2]_.
Examples
--------
>>> import biosteam as bst
>>> bst.settings.set_thermo(['Water', 'Hexane', bst.Chemical('Solids', search_db=False, default=True, phase='s')], cache=True)
>>> feed = bst.Stream('feed', Water=100, Hexane=100, Solids=10)
>>> C1 = bst.SLLECentrifuge('C1', feed, ['oil', 'aqueous', 'solids'], top_chemical='Hexane', solids_split={'Solids':1.0})
>>> C1.simulate()
>>> C1.show()
SLLECentrifuge: C1
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 100
Hexane 100
Solids 10
outs...
[0] oil
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 0.747
Hexane 100
[1] aqueous
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 98.7
Hexane 0.0156
[2] solids
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 0.555
Solids 10
>>> C1.results()
3-Phase decanter centrifuge Units C1
Electricity Power kW 0.0101
Cost USD/hr 0.000792
Design Flow rate L/min 250
Purchase cost 3-Phase decanter centrifuge USD 2.88e+05
Total purchase cost USD 2.88e+05
Utility cost USD/hr 0.000792
"""
line = '3-Phase decanter centrifuge'
_N_ins = 1
_N_outs = 3
solvent = LLEUnit.solvent
@property
def solids_split(self):
return self._solids_isplit.data
@property
def solids_isplit(self):
return self._solids_isplit
def _init(self, solids_split, top_chemical=None, efficiency=1.0,
moisture_content=0.5):
# [ChemicalIndexer] Splits to 0th outlet stream.
self._solids_isplit = self.thermo.chemicals.isplit(solids_split)
#: [str] Identifier of chemical that will be favored in the low density phase.
self.top_chemical = top_chemical
#: Fraction of feed in liquid-liquid equilibrium.
#: The rest of the feed is divided equally between phases.
self.efficiency = efficiency
#: Moisture content of retentate
self.moisture_content = moisture_content
assert self._solids_isplit['7732-18-5'] == 0, 'cannot define water split, only moisture content'
def _run(self):
feed = self.ins[0]
top, bottom, solids = self.outs
feed.split_to(solids, top, self.solids_split, energy_balance=False)
sep.lle(top, top, bottom, self.top_chemical, self.efficiency)
sep.adjust_moisture_content(solids, bottom, self.moisture_content)
[docs]
@copy_algorithm(SLLECentrifuge, run=False)
class SolidLiquidsSplitCentrifuge(Unit):
"""
Create a SolidLiquidsSplitCentrifuge object that separates the feed into solid, oil, and
aqueous phases.
Parameters
----------
ins :
feed
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
* [2] Solids.
aqueous_split : dict[str, float]
Splits to [0] outlet stream.
solids_split : dict[str, float]
Splits to [2] outlet stream.
moisture_content : float, optional
Moisture content of solids. Defaults to 0.5.
Notes
-----
Cost algorithm is based on a 3-phase decanter centrifuge from
a conventional dry-grind corn ethanol plant that separates
aqueous, oil, and solid fractions (i.e. DDGS) from the bottoms product
of the beer column [2]_.
The unit operation first splits the feed to the light and heavy fractions,
then fractionates the solids from the heavy phase.
Examples
--------
>>> import biosteam as bst
>>> bst.settings.set_thermo(['Water', 'Hexane', bst.Chemical('Solids', search_db=False, default=True, phase='s')], cache=True)
>>> feed = bst.Stream('feed', Water=100, Hexane=100, Solids=10)
>>> C1 = bst.SolidLiquidsSplitCentrifuge(
... 'C1', feed, ['oil', 'aqueous', 'solids'],
... solids_split={'Solids':1.0},
... aqueous_split={'Water':0.99, 'Hexane':0.01, 'Solids': 0.9}
... )
>>> C1.simulate()
>>> C1.show(flow='kg/hr')
SolidLiquidsSplitCentrifuge: C1
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 1.8e+03
Hexane 8.62e+03
Solids 10
outs...
[0] oil
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 18
Hexane 8.53e+03
Solids 1
[1] aqueous
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 1.77e+03
Hexane 86.2
[2] solids
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 9
Solids 9
>>> C1.results()
3-Phase decanter centrifuge Units C1
Electricity Power kW 0.0101
Cost USD/hr 0.000792
Design Flow rate L/min 250
Purchase cost 3-Phase decanter centrifuge USD 2.88e+05
Total purchase cost USD 2.88e+05
Utility cost USD/hr 0.000792
"""
line = SLLECentrifuge.line
_N_ins = 1
_N_outs = 3
@property
def solids_split(self):
return self._solids_isplit.data
@property
def solids_isplit(self):
return self._solids_isplit
@property
def aqueous_split(self):
return self._aqueous_isplit.data
@property
def aqueous_isplit(self):
return self._aqueous_isplit
def _init(self, aqueous_split, solids_split, moisture_content=0.5):
# [ChemicalIndexer] Splits to 1st outlet stream (aqueous/heavy phase).
self._aqueous_isplit = self.thermo.chemicals.isplit(aqueous_split)
# [ChemicalIndexer] Splits to 2th outlet stream (solids) from aqueous/heavy phase.
self._solids_isplit = self.thermo.chemicals.isplit(solids_split)
#: Moisture content of retentate
self.moisture_content = moisture_content
assert self._solids_isplit['7732-18-5'] == 0, 'cannot define water split to solids, only moisture content'
def _run(self):
oil, aqueous, solids = self.outs
self.ins[0].split_to(aqueous, oil, self.aqueous_split)
aqueous.split_to(solids, aqueous, self.solids_split)
sep.adjust_moisture_content(solids, aqueous, self.moisture_content)
# %% Mixing
# Cost base on table 16.32 of Seider's Product and Process Design Principles, 3rd edition
[docs]
@cost('Power', 'Turbine agitator', N='Number of agitators',
ub=60, CE=567, cost=3730, n=0.54, BM=2.25)
class LiquidsMixingTank(bst.Unit, PressureVessel):
"""
Create a LiquidsMixingTank for mixing two liquid phases.
Parameters
----------
ins :
Inlet fluids to be mixed.
outs :
Mixed outlet fluid.
tau=0.022 : float
Residence time [hr].
agitator_kW_per_m3=1.0 : float
Electricity consumption in kW / m3 of volume.
vessel_material='Carbon steel' : str, optional
Vessel construction material.
vessel_type='Horizontal': 'Horizontal' or 'Vertical', optional
Vessel type.
length_to_diameter=1 : float
Length to diameter ratio.
"""
_units = {**PressureVessel._units,
'Volume': 'm^3',
'Power': 'hp'}
_ins_size_is_fixed = False
_N_ins = 3
_N_outs = 1
def _init(self, tau=0.022, agitator_kW_per_m3=1.0, length_to_diameter=1,
vessel_material='Carbon steel', vessel_type='Vertical'):
self.length_to_diameter = length_to_diameter
self.vessel_material = vessel_material
self.vessel_type = vessel_type
self.agitator_kW_per_m3 = agitator_kW_per_m3
self.tau = tau
def _run(self):
self.outs[0].mix_from(self.ins)
def _design(self):
results = self.design_results
results['Volume'] = volume = self.tau * self.outs[0].F_vol
P = self.ins[0].get_property('P', 'psi')
length_to_diameter = self.length_to_diameter
results = self.design_results
rate = self.agitator_kW_per_m3 * volume
self.power_utility(rate)
results['Power'] = 1.341 * rate # in hp
D = geometry.cylinder_diameter_from_volume(volume, length_to_diameter)
L = length_to_diameter * D
results.update(self._vessel_design(P, D, L))
def _cost(self):
self._decorated_cost()
D = self.design_results
self.purchase_costs.update(
self._vessel_purchase_cost(D['Weight'], D['Diameter'], D['Length'])
)
# %% Settling
[docs]
class LiquidsSettler(bst.Unit, PressureVessel, isabstract=True):
"""
Abstract Settler class for liquid-liquid extraction.
Parameters
----------
ins :
Inlet fluid with two liquid phases.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
vessel_material='Carbon steel' : str, optional
Vessel construction material.
vessel_type='Horizontal': 'Horizontal' or 'Vertical', optional
Vessel type.
length_to_diameter=4 : float
Length to diameter ratio.
area_to_feed=0.1 : float
Diameter * length per gpm of feed [ft2/gpm].
"""
_N_ins = 1
_N_outs = 2
def _init(self, area_to_feed=0.1,
length_to_diameter=4,
vessel_material='Carbon steel',
vessel_type='Horizontal'):
self.vessel_material = vessel_material
self.vessel_type = vessel_type
self.length_to_diameter = length_to_diameter #: Length to diameter ratio
self.area_to_feed = area_to_feed #: [ft2/gpm] Diameter * length per gpm of feed
@staticmethod
def _default_vessel_type():
return 'Horizontal'
def _design(self):
feed = self.ins[0]
F_vol_gpm = feed.get_total_flow('gpm')
area = self.area_to_feed * F_vol_gpm
length_to_diameter = self.length_to_diameter
P = feed.get_property('P', 'psi')
D = (area / length_to_diameter) ** 0.5
L = length_to_diameter * D
self.design_results.update(self._vessel_design(P, D, L))
def _cost(self):
D = self.design_results
self.purchase_costs.update(
self._vessel_purchase_cost(D['Weight'], D['Diameter'], D['Length'])
)
[docs]
class LLESettler(LLEUnit, LiquidsSettler):
"""
Create a LLESettler object that rigorously simulates liquid-liquid extraction.
Parameters
----------
ins :
Inlet fluid with two liquid phases.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
vessel_material='Carbon steel' : str, optional
Vessel construction material.
vessel_type='Horizontal': 'Horizontal' or 'Vertical', optional
Vessel type.
length_to_diameter=4 : float, optional
Length to diameter ratio.
area_to_feed=0.1 : float, optional
Diameter * length per gpm of feed [ft2/gpm].
top_chemical=None : str, optional
Identifier of chemical that will be favored in the low density phase.
efficiency=1.0 : float
Fraction of feed in liquid-liquid equilibrium
cache_tolerance=1e-6 : float, optional
Molar tolerance of cached partition coefficients.
"""
line = 'Settler'
def _init(self,
area_to_feed=0.1,
length_to_diameter=4,
vessel_material='Carbon steel',
vessel_type='Horizontal',
top_chemical=None,
efficiency=1.0,
cache_tolerance=1e-6,
):
LLEUnit._init(self, top_chemical, efficiency)
self.vessel_material = vessel_material
self.vessel_type = vessel_type
self.length_to_diameter = length_to_diameter
self.area_to_feed = area_to_feed
self.cache_tolerance = cache_tolerance
[docs]
class LiquidsSplitSettler(LiquidsSettler):
"""
Create a LLESettler object that rigorously simulates liquid-liquid extraction.
Parameters
----------
ins :
Inlet fluid with two liquid phases.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
split : Should be one of the following
* [float] The fraction of net feed in the 0th outlet stream
* [array_like] Componentwise split of feed to 0th outlet stream
* [dict] ID-split pairs of feed to 0th outlet stream
order=None : Iterable[str], defaults to biosteam.settings.chemicals.IDs
Chemical order of split.
vessel_material='Carbon steel' : str, optional
Vessel construction material.
vessel_type='Horizontal': 'Horizontal' or 'Vertical', optional
Vessel type.
length_to_diameter=4 : float, optional
Length to diameter ratio.
area_to_feed=0.1 : float, optional
Diameter * length per gpm of feed [ft2/gpm].
"""
line = 'Settler'
def _init(self,
split, order=None,
area_to_feed=0.1,
length_to_diameter=4,
vessel_material='Carbon steel',
vessel_type='Horizontal'
):
self.vessel_material = vessel_material
self.vessel_type = vessel_type
self.length_to_diameter = length_to_diameter
self.area_to_feed = area_to_feed
self._isplit = self.chemicals.isplit(split, order)
split = Splitter.split
isplit = Splitter.isplit
_run = Splitter._run
[docs]
class LiquidsPartitionSettler(LiquidsSettler):
"""
Create a LiquidsPartitionSettler object that simulates liquid-liquid
extraction by partition coefficients.
Parameters
----------
ins :
Inlet fluid with two liquid phases.
outs :
* [0] Low density fluid.
* [1] Heavy fluid.
vessel_material='Carbon steel' : str, optional
Vessel construction material.
vessel_type='Horizontal': 'Horizontal' or 'Vertical', optional
Vessel type.
length_to_diameter=4 : float, optional
Length to diameter ratio.
area_to_feed=0.1 : float, optional
Diameter * length per gpm of feed [ft2/gpm].
partition_coefficients : 1d array, optional
Partition coefficients of chemicals in equilibrium (molar
composition ratio of the top fluid over the bottom fluid).
partition_IDs: tuple[str], optional
IDs of chemicals in equilibrium.
"""
line = 'Settler'
def _init(self,
partition_coefficients, partion_IDs,
area_to_feed=0.1,
length_to_diameter=4,
vessel_material='Carbon steel',
vessel_type='Horizontal'
):
self.vessel_material = vessel_material
self.vessel_type = vessel_type
self.length_to_diameter = length_to_diameter
self.area_to_feed = area_to_feed
self.partition_coefficients = partition_coefficients
self.partion_IDs = partion_IDs
self.reset_cache()
def reset_cache(self, isdynamic=None):
self._phi = None
def _run(self):
self._phi = sep.partition(*self.ins, *self.outs,
self.partion_IDs, self.partition_coefficients,
self._phi)
# %% Mixer-settlers
[docs]
class MixerSettler(bst.Unit):
"""
Create a MixerSettler object that models liquid-liquid extraction using
a mixing tank and a settler tank.
Parameters
----------
ins :
* [0] feed.
* [1] solvent.
outs :
* [0] extract.
* [1] raffinate.
top_chemical : str, optional
Name of main chemical in the extract phase.
Defaults to chemical with highest molar fraction in the solvent.
mixer_data : dict, optional
Arguments to initialize the "mixer" attribute, a :class:`~biosteam.units.LiquidsMixingTank` object.
settler_data : dict, optional
Arguments to initialize the "settler" attribute, a :class:`~biosteam.units.LiquidsSettler` object.
Examples
--------
Simulate by rigorous LLE:
>>> import biosteam as bst
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Octanol'], cache=True)
>>> feed = bst.Stream('feed', Water=500, Methanol=50)
>>> solvent = bst.Stream('solvent', Octanol=500)
>>> MS1 = bst.MixerSettler('MS1', ins=(feed, solvent), outs=('extract', 'raffinate'))
>>> MS1.simulate()
>>> MS1.extract.imol['Methanol'] / MS1.feed.imol['Methanol']
0.63
>>> MS1.raffinate.imol['Water'] / MS1.feed.imol['Water']
0.85
>>> MS1.extract.imol['Octanol'] / MS1.solvent.imol['Octanol']
0.99
>>> MS1.results() # doctest: +SKIP
Mixer settler Units MS1
Power Rate kW 1.98
Cost USD/hr 0.155
Design Mixer - Volume m^3 1.98
Mixer - Power hp 2.65
Mixer - Vessel type Vertical
Mixer - Length ft 1.36
Mixer - Diameter ft 1.36
Mixer - Weight lb 91.2
Mixer - Wall thickness in 0.25
Settler - Vessel type Horizontal
Settler - Length ft 12.6
Settler - Diameter ft 3.15
Settler - Weight lb 1.44e+03
Settler - Wall thickness in 0.25
Purchase cost Mixer - Turbine agitator USD 6.32e+03
Mixer - Vertical pressure vessel USD 4.59e+03
Mixer - Platform and ladders USD 641
Settler - Horizontal pressure ve... USD 9.99e+03
Settler - Platform and ladders USD 2.66e+03
Total purchase cost USD 2.42e+04
Utility cost USD/hr 0.155
Simulate with user defined partition coefficients:
>>> import biosteam as bst
>>> import numpy as np
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Octanol'])
>>> feed = bst.Stream('feed', Water=500, Methanol=50)
>>> solvent = bst.Stream('solvent', Octanol=500)
>>> MS1 = bst.MixerSettler('MS1',
... ins=(feed, solvent), outs=('extract', 'raffinate'),
... model='partition coefficients',
... settler_data={
... 'partition_coefficients': np.array([1.451e-01, 1.380e+00, 2.958e+03]),
... 'partion_IDs': ('Water', 'Methanol', 'Octanol'),
... },
... )
>>> MS1.simulate()
>>> MS1.extract.imol['Methanol'] / MS1.feed.imol['Methanol']
0.66
>>> MS1.raffinate.imol['Water'] / MS1.feed.imol['Water']
0.82
>>> MS1.extract.imol['Octanol'] / MS1.solvent.imol['Octanol']
0.99
>>> MS1.results() # doctest: +SKIP
Mixer settler Units MS1
Electricity Power kW 1.98
Cost USD/hr 0.155
Design Mixer - Volume m^3 1.98
Mixer - Power hp 2.65
Mixer - Vessel type Vertical
Mixer - Length ft 1.36
Mixer - Diameter ft 1.36
Mixer - Weight lb 91.2
Mixer - Wall thickness in 0.25
Settler - Vessel type Horizontal
Settler - Length 12.6
Settler - Diameter 3.15
Settler - Weight 1.44e+03
Settler - Wall thickness 0.25
Purchase cost Mixer - Turbine agitator USD 6.32e+03
Mixer - Vertical pressure vessel USD 4.91e+03
Mixer - Platform and ladders USD 686
Settler - Horizontal pressure ve... USD 1.16e+04
Settler - Platform and ladders USD 3.08e+03
Total purchase cost USD 2.65e+04
Utility cost USD/hr 0.155
"""
_N_ins = 2
_ins_size_is_fixed = False
_N_outs = 2
auxiliary_unit_names = ('mixer', 'settler')
_graphics = mixer_settler_graphics
_units = {}
for i,j in LiquidsMixingTank._units.items():
_units['Mixer - ' + i] = j
for i,j in LiquidsSettler._units.items():
_units['Settler - ' + i] = j
def _init(self, top_chemical=None, mixer_data={}, settler_data={}, model="LLE"):
#: [LiquidsMixingTank] Mixer portion of the mixer-settler.
#: All data and settings for the design of the mixing tank are stored here.
self.mixer = mixer = LiquidsMixingTank(None, None, (None,),
self.thermo, **mixer_data)
self.multi_stream = multi_stream = mixer-0
mixer._ins = self._ins
model = model.lower()
if model == 'lle':
Settler = LLESettler
elif model == 'split':
Settler = LiquidsSplitSettler
elif model == 'partition coefficients':
Settler = LiquidsPartitionSettler
#: [LiquidsSettler] Settler portion of the mixer-settler.
#: All data and settings for the design of the settler are stored here.
self.settler = Settler(None, multi_stream, thermo=self.thermo, **settler_data)
#: [str] ID of carrier component in the feed.
self.top_chemical = top_chemical
@property
def feed(self):
"""[Stream] Feed with solute being extracted and carrier."""
return self._ins[0]
@feed.setter
def feed(self, stream):
self._ins[0] = stream
@property
def solvent(self):
"""[Stream] Solvent to extract solute."""
return self._ins[1]
@solvent.setter
def solvent(self, stream):
self._ins[1] = stream
@property
def raffinate(self):
"""[Stream] Raffinate after extraction."""
return self._outs[1]
@raffinate.setter
def raffinate(self, stream):
self._outs[1] = stream
@property
def extract(self):
"""[Stream] Extract with solvent."""
return self._outs[0]
@extract.setter
def extract(self, stream):
self._outs[0] = stream
def _run(self):
self.mixer._run()
self.settler.top_chemical = self.top_chemical or self.solvent.main_chemical
self.settler._run()
for i, j in zip([self.extract, self.raffinate], self.settler.outs): i.copy_like(j)
def _design(self):
mixer = self.mixer
mixer._design()
settler = self.settler
settler._design()
design_results = self.design_results
for i,j in mixer.design_results.items():
design_results['Mixer - ' + i] = j
for i,j in settler.design_results.items():
design_results['Settler - ' + i] = j
def _cost(self):
self.mixer._cost()
self.settler._cost()
[docs]
class MultiStageMixerSettlers(MultiStageEquilibrium):
"""
Create a MultiStageMixerSettlers object that models a counter-current system
of mixer-settlers for liquid-liquid extraction.
Parameters
----------
ins :
* [0] feed.
* [1] solvent.
outs :
* [0] extract
* [1] raffinate
N_stages : int
Number of stages.
partition_data : {'IDs': tuple[str], 'K': 1d array}, optional
IDs of chemicals in equilibrium and partition coefficients (molar
composition ratio of the extract over the raffinate). If given,
The mixer-settlers will be modeled with these constants. Otherwise,
partition coefficients are computed based on temperature and composition.
top_chemical : str
Name of main chemical in the top phase (extract phase).
mixer_data : dict
Arguments to initialize the "mixer" attribute, a :class:`~biosteam.units.LiquidsMixingTank` object.
settler_data : dict
Arguments to initialize the "settler" attribute, a :class:`~biosteam.units.LiquidsSettler` object.
Notes
-----
All mixer settlers are sized equally based on the assumption that the
volumetric flow rate of each phase does not change significantly across
stages.
Examples
--------
Simulate by rigorous LLE:
>>> import biosteam as bst
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Octanol'])
>>> feed = bst.Stream('feed', Water=500, Methanol=50)
>>> solvent = bst.Stream('solvent', Octanol=500)
>>> MSMS1 = bst.MultiStageMixerSettlers('MSMS1', ins=(feed, solvent), outs=('extract', 'raffinate'), N_stages=2)
>>> MSMS1.simulate()
>>> MSMS1.extract.imol['Methanol'] / MSMS1.feed.imol['Methanol']
0.83
>>> MSMS1.raffinate.imol['Water'] / MSMS1.feed.imol['Water']
0.82
>>> MSMS1.extract.imol['Octanol'] / MSMS1.solvent.imol['Octanol']
0.99
>>> MSMS1.results() # doctest: +SKIP
Multi stage mixer settlers Units MSMS1
Electricity Power kW 3.96
Cost USD/hr 0.309
Design Mixer - Volume m^3 1.98
Mixer - Power hp 2.65
Mixer - Vessel type Vertical
Mixer - Length ft 1.36
Mixer - Diameter ft 1.36
Mixer - Weight lb 91.2
Mixer - Wall thickness in 0.25
Settler - Vessel type Horizontal
Settler - Length 12.6
Settler - Diameter 3.15
Settler - Weight 1.44e+03
Settler - Wall thickness 0.25
Purchase cost Mixers and agitators USD 1.12e+04
Settlers USD 2.93e+04
Total purchase cost USD 4.05e+04
Utility cost USD/hr 0.309
Simulate with user defined partition coefficients:
>>> import biosteam as bst
>>> import numpy as np
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Octanol'])
>>> feed = bst.Stream('feed', Water=5000, Methanol=500)
>>> solvent = bst.Stream('solvent', Octanol=5000)
>>> MSMS1 = bst.MultiStageMixerSettlers('MSMS1', ins=(feed, solvent), outs=('extract', 'raffinate'), N_stages=10,
... partition_data={
... 'K': np.array([0.1450, 1.380, 2957.]),
... 'IDs': ('Water', 'Methanol', 'Octanol'),
... 'phi': 0.590 # Initial phase fraction guess. This is optional.
... }
... )
>>> MSMS1.simulate()
>>> MSMS1.extract.imol['Methanol'] / MSMS1.feed.imol['Methanol']
0.99
>>> MSMS1.raffinate.imol['Water'] / MSMS1.feed.imol['Water']
0.82
>>> MSMS1.extract.imol['Octanol'] / MSMS1.solvent.imol['Octanol']
0.99
>>> MSMS1.results() # doctest: +SKIP
Multi stage mixer settlers Units MSMS1
Electricity Power kW 198
Cost USD/hr 15.5
Design Mixer - Volume m^3 19.8
Mixer - Power hp 26.5
Mixer - Vessel type Vertical
Mixer - Length ft 2.93
Mixer - Diameter ft 2.93
Mixer - Weight lb 423
Mixer - Wall thickness in 0.25
Settler - Vessel type Horizontal
Settler - Length 39.8
Settler - Diameter 9.95
Settler - Weight 2.52e+04
Settler - Wall thickness 0.438
Purchase cost Mixers and agitators USD 1.15e+05
Settlers USD 6.15e+05
Total purchase cost USD 7.3e+05
Utility cost USD/hr 15.5
Because octanol and water do not mix well, it may be a good idea to assume
that these solvents do not mix at all:
>>> import biosteam as bst
>>> import numpy as np
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Octanol'])
>>> feed = bst.Stream('feed', Water=5000, Methanol=500)
>>> solvent = bst.Stream('solvent', Octanol=5000)
>>> MSMS1 = bst.MultiStageMixerSettlers('MSMS1', ins=(feed, solvent), outs=('extract', 'raffinate'), N_stages=20,
... partition_data={
... 'K': np.array([1.38]),
... 'IDs': ('Methanol',),
... 'raffinate_chemicals': ('Water',),
... 'extract_chemicals': ('Octanol',),
... }
... )
>>> MSMS1.simulate()
>>> MSMS1.extract.imol['Methanol'] / feed.imol['Methanol'] # Recovery
0.99
>>> MSMS1.extract.imol['Octanol'] / solvent.imol['Octanol'] # Solvent stays in extract
1.0
>>> MSMS1.raffinate.imol['Water'] / feed.imol['Water'] # Carrier remains in raffinate
1.0
Simulate with a feed at the 4th stage:
>>> import biosteam as bst
>>> import numpy as np
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Octanol'])
>>> feed = bst.Stream('feed', Water=5000, Methanol=500)
>>> solvent = bst.Stream('solvent', Octanol=5000)
>>> dilute_feed = bst.Stream('dilute_feed', Water=100, Methanol=2)
>>> MSMS1 = bst.MultiStageMixerSettlers('MSMS1', ins=(feed, dilute_feed, solvent), outs=('extract', 'raffinate'), N_stages=5,
... feed_stages=[0, 3, -1], # Stage at which each inlet enters, respectively
... partition_data={
... 'K': np.array([1.38]),
... 'IDs': ('Methanol',),
... 'raffinate_chemicals': ('Water',),
... 'extract_chemicals': ('Octanol',),
... }
... )
>>> MSMS1.simulate()
>>> MSMS1.extract.imol['Methanol'] / (feed.imol['Methanol'] + dilute_feed.imol['Methanol']) # Recovery
0.93
Simulate with a 60% extract side draw at the 2nd stage and 10% raffinate side draw at the 3rd stage:
>>> import biosteam as bst
>>> import numpy as np
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Octanol'])
>>> feed = bst.Stream('feed', Water=5000, Methanol=500)
>>> solvent = bst.Stream('solvent', Octanol=5000)
>>> MSMS1 = bst.MultiStageMixerSettlers('MSMS1', ins=(feed, solvent), N_stages=4,
... # Extract side draws always come first, then raffinate side draws
... outs=('extract', 'raffinate', 'extract_side_draw', 'raffinate_side_draw'),
... extract_side_draws=[(1, 0.6)], # Stage number and split fraction pairs
... raffinate_side_draws=[(2, 0.10)],
... partition_data={
... 'K': np.array([1.38]),
... 'IDs': ('Methanol',),
... 'raffinate_chemicals': ('Water',),
... 'extract_chemicals': ('Octanol',),
... }
... )
>>> MSMS1.simulate()
>>> (MSMS1.extract.imol['Methanol'] + MSMS1.outs[2].imol['Methanol']) / feed.imol['Methanol'] # Recovery
0.87
"""
_side_draw_names = ('extract_side_draws', 'raffinate_side_draws')
_units = MixerSettler._units
default_maxiter = 20
def _init(self, N_stages, feed_stages=None, extract_side_draws=None,
raffinate_side_draws=None, partition_data=None, top_chemical=None,
mixer_data={}, settler_data={}, use_cache=None, collapsed_init=None):
bst.MultiStageEquilibrium._init(
self, N_stages=N_stages, feed_stages=feed_stages, phases=('l', 'L'), P=101325,
top_side_draws=extract_side_draws, bottom_side_draws=raffinate_side_draws,
stage_specifications=None, partition_data=partition_data,
top_chemical=top_chemical, use_cache=use_cache, collapsed_init=collapsed_init,
)
#: [LiquidsMixingTank] Used to design all mixing tanks.
#: All data and settings for the design of mixing tanks are stored here.
self.mixer = mixer = LiquidsMixingTank(None, None, (None,),
self.thermo, **mixer_data)
mixer._ins = self._ins
#: [LiquidsSettler] Used to design all settlers.
#: All data and settings for the design of settlers are stored here.
self.settler = LiquidsSettler(None, mixer-0, None, self.thermo, **settler_data)
self.settler._outs = self._outs
self.use_cache = use_cache
self._last_args = (
self.N_stages, self.feed_stages, self.extract_side_draws, self.use_cache,
*self._ins, self.raffinate_side_draws, self.top_chemical, self.partition_data,
self.collapsed_init,
)
feed = MixerSettler.feed
solvent = MixerSettler.solvent
extract = MixerSettler.extract
raffinate = MixerSettler.raffinate
@property
def partition_data(self):
return self._partition_data
@partition_data.setter
def partition_data(self, partition_data):
self._partition_data = partition_data
self._last_args = None
def _setup(self):
super()._setup()
args = (self.N_stages, self.feed_stages, self.extract_side_draws, self.use_cache,
*self._ins, self.raffinate_side_draws, self.top_chemical, self.partition_data,
self.collapsed_init)
if args != self._last_args:
MultiStageEquilibrium._init(
self, N_stages=self.N_stages, feed_stages=self.feed_stages,
phases=('l', 'L'), P=self.P,
top_side_draws=self.extract_side_draws,
bottom_side_draws=self.raffinate_side_draws,
stage_specifications=None, partition_data=self.partition_data,
top_chemical=self.top_chemical, use_cache=self.use_cache,
collapsed_init=self.collapsed_init,
)
self.mixer._ins = self._ins
self.settler._outs = self._outs
self._last_args = args
def reset_cache(self, isdynamic=None):
self._last_args = None
def _design(self):
mixer = self.mixer
mixer._run()
mixer._design()
settler = self.settler
settler._design()
design_results = self.design_results
for i,j in mixer.design_results.items():
design_results['Mixer - ' + i] = j
for i,j in settler.design_results.items():
design_results['Settler - ' + i] = j
def _cost(self):
N_stages = self.N_stages
mixer = self.mixer
settler = self.settler
mixer._cost()
settler._cost()
self.power_utility.copy_like(mixer.power_utility)
self.power_utility.scale(N_stages)
purchase_costs = self.purchase_costs
purchase_costs['Mixers and agitators'] = N_stages * mixer.purchase_cost
purchase_costs['Settlers'] = N_stages * settler.purchase_cost
baseline_purchase_costs = self.baseline_purchase_costs
baseline_purchase_costs['Mixers and agitators'] = N_stages * mixer.purchase_cost
baseline_purchase_costs['Settlers'] = N_stages * settler.purchase_cost
installed_costs = self.installed_costs
installed_costs['Mixers and agitators'] = N_stages * mixer.purchase_cost
installed_costs['Settlers'] = N_stages * settler.purchase_cost
@property
def installed_cost(self):
N_stages = self.N_stages
return N_stages * (self.mixer.installed_cost + self.settler.installed_cost)