# -*- 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.
"""
This module contains functions for modeling separations in unit operations.
"""
from warnings import warn
import thermosteam as tmo
import numpy as np
from .exceptions import InfeasibleRegion
from .equilibrium import phase_fraction as compute_phase_fraction
__all__ = (
'mix_and_split',
'adjust_moisture_content',
'mix_and_split_with_moisture_content',
'handle_infeasible_flow_rates',
'partition_coefficients',
'vle_partition_coefficients',
'lle_partition_coefficients',
'partition', 'lle', 'vle',
'material_balance',
'chemical_splits',
'phase_fraction',
)
def check_partition_infeasibility(infeasible_index, strict, stacklevel=1):
if infeasible_index.any():
if strict:
raise InfeasibleRegion('negative flow rates in equilibrium '
'solution; partition data')
else:
warning = RuntimeWarning(
'phase equilibrium solution results in negative flow rates; '
'negative flows have been removed from solution'
)
warn(warning, stacklevel=stacklevel+1)
def handle_infeasible_flow_rates(mol, maxmol, strict, stacklevel=1):
mol = mol
maxmol = maxmol
infeasible_index, = np.where(mol < 0.)
check_partition_infeasibility(infeasible_index, strict, stacklevel+1)
mol[infeasible_index] = 0.
infeasible_index, = np.where(mol > maxmol)
check_partition_infeasibility(infeasible_index, strict, stacklevel+1)
mol[infeasible_index] = maxmol[infeasible_index]
# %% Mixing, splitting, and moisture content
CAS_water = '7732-18-5'
[docs]
def mix_and_split_with_moisture_content(ins, retentate, permeate,
split, moisture_content, ID=None,
strict=None):
"""
Run splitter mass and energy balance with mixing all input streams and
and ensuring retentate moisture content.
Parameters
----------
ins : Iterable[Stream]
Inlet fluids with solids.
retentate : Stream
permeate : Stream
split : array_like
Component splits to the retentate.
moisture_content : float
Fraction of water in retentate.
Examples
--------
>>> import thermosteam as tmo
>>> Solids = tmo.Chemical('Solids', default=True, search_db=False, phase='s')
>>> tmo.settings.set_thermo(['Water', Solids])
>>> feed = tmo.Stream('feed', Water=100, Solids=10, units='kg/hr')
>>> wash_water = tmo.Stream('wash_water', Water=10, units='kg/hr')
>>> retentate = tmo.Stream('retentate')
>>> permeate = tmo.Stream('permeate')
>>> split = [0., 1.]
>>> moisture_content = 0.5
>>> tmo.separations.mix_and_split_with_moisture_content(
... [feed, wash_water], retentate, permeate, split, moisture_content
... )
>>> retentate.show(flow='kg/hr')
Stream: retentate
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 10
Solids 10
>>> permeate.show(flow='kg/hr')
Stream: permeate
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 100
"""
mix_and_split(ins, retentate, permeate, split)
adjust_moisture_content(retentate, permeate, moisture_content, ID, strict)
[docs]
def adjust_moisture_content(retentate, permeate, moisture_content, ID=None, strict=None):
"""
Remove water from permate to adjust retentate moisture content.
Parameters
----------
retentate : Stream
permeate : Stream
moisture_content : float
Fraction of water in retentate.
Examples
--------
>>> import thermosteam as tmo
>>> Solids = tmo.Chemical('Solids', default=True, search_db=False, phase='s')
>>> tmo.settings.set_thermo(['Water', Solids])
>>> retentate = tmo.Stream('retentate', Solids=20, units='kg/hr')
>>> permeate = tmo.Stream('permeate', Water=50, Solids=0.1, units='kg/hr')
>>> moisture_content = 0.5
>>> tmo.separations.adjust_moisture_content(retentate, permeate, moisture_content)
>>> retentate.show(flow='kg/hr')
Stream: retentate
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 20
Solids 20
>>> permeate.show(flow='kg/hr')
Stream: permeate
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 30
Solids 0.1
Note that if not enough water is available, an InfeasibleRegion error is raised:
>>> retentate.imol['Water'] = permeate.imol['Water'] = 0
>>> tmo.separations.adjust_moisture_content(retentate, permeate, moisture_content)
Traceback (most recent call last):
InfeasibleRegion: not enough water; permeate moisture content is infeasible
"""
F_mass = retentate.F_mass
mc = moisture_content
if ID is None:
ID = CAS_water
MW = 18.01528
retentate_water = retentate.imol[ID]
dry_mass = F_mass - MW * retentate_water
key = ('l', ID) if isinstance(retentate, tmo.MultiStream) else ID
retentate.imol[key] = water = (dry_mass * mc/(1-mc)) / MW
key = ('l', ID) if isinstance(retentate, tmo.MultiStream) else ID
permeate.imol[key] -= water - retentate_water
else:
retentate_moisture = retentate.imass[ID]
dry_mass = F_mass - retentate_moisture
key = ('l', ID) if isinstance(retentate, tmo.MultiStream) else ID
retentate.imass[key] = moisture = dry_mass * mc/(1-mc)
key = ('l', ID) if isinstance(retentate, tmo.MultiStream) else ID
permeate.imass[key] -= moisture - retentate_moisture
if permeate.imol[key] < 0:
if strict is None: strict = True
if strict:
raise InfeasibleRegion(f'not enough {ID}; permeate moisture content')
else:
retentate.imol[key] -= permeate.imol[key]
permeate.imol[key] = 0.
[docs]
def mix_and_split(ins, top, bottom, split):
"""
Run splitter mass and energy balance with mixing all input streams.
Parameters
----------
ins : Iterable[Stream]
All inlet fluids.
top : Stream
Top inlet fluid.
bottom : Stream
Bottom inlet fluid
split : array_like
Component-wise split of feed to the top stream.
Examples
--------
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> feed_a = tmo.Stream(Water=20, Ethanol=5)
>>> feed_b = tmo.Stream(Water=15, Ethanol=5)
>>> split = 0.8
>>> effluent_a = tmo.Stream('effluent_a')
>>> effluent_b = tmo.Stream('effluent_b')
>>> tmo.separations.mix_and_split([feed_a, feed_b], effluent_a, effluent_b, split)
>>> effluent_a.show()
Stream: effluent_a
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 28
Ethanol 8
>>> effluent_b.show()
Stream: effluent_b
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 7
Ethanol 2
"""
top.mix_from(ins)
top.split_to(top, bottom, split, energy_balance=True)
def phase_split(feed, outlets):
"""
Split the feed to outlets by phase.
Parameters
----------
feed : stream
outlets : streams
Notes
-----
Phases allocate to outlets in alphabetical order. For example,
if the feed.phases is 'gls' (i.e. gas, liquid, and solid), the phases
of the outlets will be 'g', 'l', and 's'.
Examples
--------
Split gas and liquid phases to streams:
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> feed = tmo.Stream('feed', Water=10, Ethanol=10)
>>> feed.vle(V=0.5, P=101325)
>>> vapor = tmo.Stream('vapor')
>>> liquid = tmo.Stream('liquid')
>>> outlets = [vapor, liquid]
>>> tmo.separations.phase_split(feed, outlets)
>>> vapor.show()
Stream: vapor
phase: 'g', T: 353.94 K, P: 101325 Pa
flow (kmol/hr): Water 3.87
Ethanol 6.13
>>> liquid.show()
Stream: liquid
phase: 'l', T: 353.94 K, P: 101325 Pa
flow (kmol/hr): Water 6.13
Ethanol 3.87
Note that the number of phases in the feed should be equal to the number of
outlets:
>>> tmo.separations.phase_split(feed, [vapor])
Traceback (most recent call last):
RuntimeError: number of phases in feed must be equal to the number of outlets
"""
phases = feed.phases
if len(outlets) != len(phases):
raise RuntimeError('number of phases in feed must be equal to the number of outlets')
for i,j in zip(feed, outlets): j.copy_like(i)
# %% Single stage equilibrium
[docs]
def partition_coefficients(IDs, top, bottom):
"""
Return partition coefficients given streams in equilibrium.
Parameters
----------
top : Stream
Vapor fluid.
bottom : Stream
Liquid fluid.
IDs : tuple[str]
IDs of chemicals in equilibrium.
Returns
-------
K : 1d array
Patition coefficients in mol fraction in top stream over mol fraction in bottom stream.
Examples
--------
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol', tmo.Chemical('O2', phase='g')], cache=True)
>>> s = tmo.Stream('s', Water=20, Ethanol=20, O2=0.1)
>>> s.vle(V=0.5, P=101325)
>>> tmo.separations.partition_coefficients(('Water', 'Ethanol'), s['g'], s['l'])
array([0.632, 1.582])
"""
numerator = top.get_normalized_mol(IDs)
denominator = bottom.get_normalized_mol(IDs)
denominator[denominator < 1e-24] = 1e-24
return numerator / denominator
[docs]
def chemical_splits(a, b=None, mixed=None):
"""
Return a ChemicalIndexer with splits for all chemicals to stream `a`.
Examples
--------
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> stream = tmo.Stream('stream', Water=10, Ethanol=10)
>>> stream.vle(V=0.5, P=101325)
>>> isplits = tmo.separations.chemical_splits(stream['g'], stream['l'])
>>> isplits.show()
ChemicalIndexer:
Water 0.387
Ethanol 0.613
>>> isplits = tmo.separations.chemical_splits(stream['g'], mixed=stream)
>>> isplits.show()
ChemicalIndexer:
Water 0.387
Ethanol 0.613
"""
mixed_mol = mixed.mol.copy() if mixed else a.mol + b.mol
return tmo.indexer.ChemicalIndexer.from_data(a.mol / mixed_mol)
[docs]
def vle_partition_coefficients(top, bottom):
"""
Return VLE partition coefficients given vapor and liquid streams in equilibrium.
Parameters
----------
top : Stream
Vapor fluid.
bottom : Stream
Liquid fluid.
Returns
-------
IDs : tuple[str]
IDs for chemicals in vapor-liquid equilibrium.
K : 1d array
Patition coefficients in mol fraction in vapor over mol fraction in liquid.
Examples
--------
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol', tmo.Chemical('O2', phase='g')], cache=True)
>>> s = tmo.Stream('s', Water=20, Ethanol=20, O2=0.1)
>>> s.vle(V=0.5, P=101325)
>>> IDs, K = tmo.separations.vle_partition_coefficients(s['g'], s['l'])
>>> IDs
('Water', 'Ethanol')
>>> K
array([0.632, 1.582])
"""
IDs = tuple([i.ID for i in bottom.vle_chemicals])
return IDs, partition_coefficients(IDs, top, bottom)
[docs]
def lle_partition_coefficients(top, bottom):
"""
Return LLE partition coefficients given two liquid streams in equilibrium.
Parameters
----------
top : Stream
Liquid fluid.
bottom : Stream
Other liquid fluid.
Returns
-------
IDs : tuple[str]
IDs for chemicals in liquid-liquid equilibrium.
K : 1d array
Patition coefficients in mol fraction in top liquid over mol fraction in bottom liquid.
Examples
--------
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol', 'Octanol'], cache=True)
>>> s = tmo.Stream('s', Water=20, Octanol=20, Ethanol=1)
>>> s.lle(T=298.15, P=101325, top_chemical='Octanol') # Top phase is L
>>> IDs, K = tmo.separations.lle_partition_coefficients(s['L'], s['l'])
>>> IDs
('Water', 'Ethanol', 'Octanol')
>>> round(K[2], -1) # Octanol
2390.0
"""
IDs = tuple([i.ID for i in bottom.lle_chemicals])
return IDs, partition_coefficients(IDs, top, bottom)
[docs]
def phase_fraction(feed, IDs, K, phi=None, top_chemicals=None,
bottom_chemicals=None, strict=False, stacklevel=1):
"""
Return the phase fraction given a stream and partition coeffiecients.
Parameters
----------
feed : Stream
Mixed feed.
IDs : tuple[str]
IDs of chemicals in equilibrium.
K : 1d array
Partition coefficeints corresponding to IDs.
phi : float, optional
Guess phase fraction in top phase.
top_chemicals : tuple[str], optional
Chemicals that remain in the top fluid.
bottom_chemicals : tuple[str], optional
Chemicals that remain in the bottom fluid.
strict : bool, optional
Whether to raise an InfeasibleRegion exception when solution results
in negative flow rates or to remove negative flows and issue a warning.
Defaults to False.
Returns
-------
phi : float
Phase fraction in top phase.
Notes
-----
Chemicals not in equilibrium end up in the top phase.
Examples
--------
>>> import numpy as np
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol', tmo.Chemical('NaCl', default=True),
... tmo.Chemical('O2', phase='g')], cache=True)
>>> IDs = ('Water', 'Ethanol')
>>> K = np.array([0.629, 1.59])
>>> feed = tmo.Stream('feed', Water=20, Ethanol=20, O2=0.1)
>>> tmo.separations.phase_fraction(feed, IDs, K)
0.500
>>> feed = tmo.Stream('feed', Water=20, Ethanol=20, NaCl=0.1, O2=0.1)
>>> tmo.separations.phase_fraction(feed, IDs, K,
... top_chemicals=('O2',),
... bottom_chemicals=('NaCl'))
0.500
"""
mol = feed.imol[IDs]
F_mol = mol.sum()
Fa = feed.imol[top_chemicals].sum() if top_chemicals else 0.
if bottom_chemicals:
bottom_flows = feed.imol[bottom_chemicals]
Fb = bottom_flows.sum() if hasattr(bottom_flows, 'sum') else bottom_flows
else:
Fb = 0.
F_mol += Fa + Fb
z_mol = mol / F_mol
phi = compute_phase_fraction(z_mol, K, phi, Fa/F_mol, Fb/F_mol)
if phi <= 0.:
phi = 0.
elif phi < 1.:
x = z_mol / (phi * K + (1. - phi))
bottom_mol = x * (1. - phi) * F_mol
handle_infeasible_flow_rates(bottom_mol, mol, strict, stacklevel+1)
else:
phi = 1.
return phi
[docs]
def partition(feed, top, bottom, IDs, K, phi=None, top_chemicals=None,
bottom_chemicals=None, strict=False, stacklevel=1):
"""
Run equilibrium of feed to top and bottom streams given partition
coeffiecients and return the phase fraction.
Parameters
----------
feed : Stream
Mixed feed.
top : Stream
Top fluid.
bottom : Stream
Bottom fluid.
IDs : tuple[str]
IDs of chemicals in equilibrium.
K : 1d array
Partition coefficeints corresponding to IDs.
phi : float, optional
Guess phase fraction in top phase.
top_chemicals : tuple[str], optional
Chemicals that remain in the top fluid.
bottom_chemicals : tuple[str], optional
Chemicals that remain in the bottom fluid.
strict : bool, optional
Whether to raise an InfeasibleRegion exception when solution results
in negative flow rates or to remove negative flows and issue a warning.
Defaults to False.
Returns
-------
phi : float
Phase fraction in top phase.
Notes
-----
Chemicals not in equilibrium end up in the top phase.
Examples
--------
>>> import numpy as np
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol', tmo.Chemical('NaCl', default=True),
... tmo.Chemical('O2', phase='g')], cache=True)
>>> IDs = ('Water', 'Ethanol')
>>> K = np.array([0.629, 1.59])
>>> feed = tmo.Stream('feed', Water=20, Ethanol=20, O2=0.1)
>>> top = tmo.Stream('top')
>>> bottom = tmo.Stream('bottom')
>>> tmo.separations.partition(feed, top, bottom, IDs, K)
0.500
>>> top.show()
Stream: top
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 7.73
Ethanol 12.3
O2 0.1
>>> bottom.show()
Stream: bottom
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 12.3
Ethanol 7.72
>>> feed = tmo.Stream('feed', Water=20, Ethanol=20, NaCl=0.1, O2=0.1)
>>> tmo.separations.partition(feed, top, bottom, IDs, K,
... top_chemicals=('O2',),
... bottom_chemicals=('NaCl'))
0.500
>>> top.show()
Stream: top
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 7.73
Ethanol 12.3
O2 0.1
>>> bottom.show()
Stream: bottom
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 12.3
Ethanol 7.72
NaCl 0.1
"""
feed_mol = feed.mol
mol = feed.imol[IDs]
F_mol = mol.sum()
if top_chemicals:
top.imol[top_chemicals] = top_flows = feed.imol[top_chemicals]
bottom.imol[top_chemicals] = 0
Fa = top_flows.sum() if hasattr(top_flows, 'sum') else top_flows
else:
Fa = 0.
if bottom_chemicals:
bottom.imol[bottom_chemicals] = bottom_flows = feed.imol[bottom_chemicals]
top.imol[bottom_chemicals] = 0
Fb = bottom_flows.sum() if hasattr(bottom_flows, 'sum') else bottom_flows
else:
Fb = 0.
F_mol += Fa + Fb
z_mol = mol / F_mol
phi = compute_phase_fraction(z_mol, K, phi, Fa/F_mol, Fb/F_mol)
if phi <= 0.:
bottom.imol[IDs] = mol
phi = 0.
elif phi < 1.:
x = z_mol / (phi * K + (1. - phi))
bottom_mol = x * (1. - phi) * F_mol
handle_infeasible_flow_rates(bottom_mol, mol, strict, stacklevel+1)
bottom.imol[IDs] = bottom_mol
else:
phi = 1.
top.mol[:] = feed_mol - bottom.mol
return phi
[docs]
def lle(feed, top, bottom, top_chemical=None, efficiency=1.0, multi_stream=None):
"""
Run LLE mass and energy balance.
Parameters
----------
feed : Stream
Mixed feed.
top : Stream
Top fluid.
bottom : Stream
Bottom fluid.
top_chemical : str, optional
Identifier of chemical that will be favored in the top fluid.
efficiency=1. : float,
Fraction of feed in liquid-liquid equilibrium.
The rest of the feed is divided equally between phases
multi_stream : MultiStream, optional
Data from feed is passed to this stream to perform liquid-liquid equilibrium.
Examples
--------
Perform liquid-liquid equilibrium around water and octanol and split the phases:
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol', 'Octanol'], cache=True)
>>> feed = tmo.Stream('feed', Water=20, Octanol=20, Ethanol=1)
>>> top = tmo.Stream('top')
>>> bottom = tmo.Stream('bottom')
>>> tmo.separations.lle(feed, top, bottom, top_chemical='Octanol')
>>> top.show()
Stream: top
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 2.87
Ethanol 0.828
Octanol 20
>>> bottom.show()
Stream: bottom
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 17.1
Ethanol 0.172
Octanol 0.00612
Assume that 1% of the feed is not in equilibrium (possibly due to poor mixing):
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol', 'Octanol'], cache=True)
>>> feed = tmo.Stream('feed', Water=20, Octanol=20, Ethanol=1)
>>> top = tmo.Stream('top')
>>> bottom = tmo.Stream('bottom')
>>> ms = tmo.MultiStream('ms', phases='lL') # Store flow rate data here as well
>>> tmo.separations.lle(feed, top, bottom, efficiency=0.99, multi_stream=ms, top_chemical='Octanol')
>>> ms.show()
MultiStream: ms
phases: ('L', 'l'), T: 298.15 K, P: 101325 Pa
flow (kmol/hr): (L) Water 2.87
Ethanol 0.828
Octanol 20
(l) Water 17.1
Ethanol 0.172
Octanol 0.00612
"""
if multi_stream:
ms = multi_stream
ms.copy_like(feed)
else:
ms = feed.copy()
ms.lle(feed.T, top_chemical=top_chemical)
top_phase, bottom_phase = ms.phases
if not top_chemical:
rho_l = ms['l'].rho
rho_L = ms['L'].rho
if rho_L is None or rho_l is not None and rho_l < rho_L:
top_phase = 'l'
bottom_phase = 'L'
top.mol[:] = ms.imol[top_phase]
bottom.mol[:] = ms.imol[bottom_phase]
top.T = bottom.T = feed.T
top.P = bottom.P = feed.P
if efficiency < 1.:
top.mol *= efficiency
bottom.mol *= efficiency
mixing = (1. - efficiency) / 2. * feed.mol
top.mol += mixing
bottom.mol += mixing
[docs]
def vle(feed, vap, liq, T=None, P=None, V=None, Q=None, x=None, y=None,
multi_stream=None):
"""
Run VLE mass and energy balance.
Parameters
----------
feed : Stream
Mixed feed.
vap : Stream
Vapor fluid.
liq : Stream
Liquid fluid.
P=None : float
Operating pressure [Pa].
Q=None : float
Duty [kJ/hr].
T=None : float
Operating temperature [K].
V=None : float
Molar vapor fraction.
x=None : float
Molar composition of liquid (for binary mixtures).
y=None : float
Molar composition of vapor (for binary mixtures).
multi_stream : MultiStream, optional
Data from feed is passed to this stream to perform vapor-liquid equilibrium.
Examples
--------
Perform vapor-liquid equilibrium on water and ethanol and split phases
to vapor and liquid streams:
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> feed = tmo.Stream('feed', Water=20, Ethanol=20)
>>> vapor = tmo.Stream('top')
>>> liquid = tmo.Stream('bottom')
>>> tmo.separations.vle(feed, vapor, liquid, V=0.5, P=101325)
>>> vapor.show()
Stream: top
phase: 'g', T: 353.94 K, P: 101325 Pa
flow (kmol/hr): Water 7.75
Ethanol 12.3
>>> liquid.show()
Stream: bottom
phase: 'l', T: 353.94 K, P: 101325 Pa
flow (kmol/hr): Water 12.3
Ethanol 7.75
It is also possible to save flow rate data in a multi-stream as well:
>>> ms = tmo.MultiStream('ms', phases='lg')
>>> tmo.separations.vle(feed, vapor, liquid, V=0.5, P=101325, multi_stream=ms)
>>> ms.show()
MultiStream: ms
phases: ('g', 'l'), T: 353.94 K, P: 101325 Pa
flow (kmol/hr): (g) Water 7.75
Ethanol 12.3
(l) Water 12.3
Ethanol 7.75
"""
if multi_stream:
ms = multi_stream
ms.copy_like(feed)
else:
ms = feed.copy()
H = feed.H + Q if Q is not None else None
ms.vle(P=P, H=H, T=T, V=V, x=x, y=y)
# Set Values
vap.phase = 'g'
liq.phase = 'l'
vap.mol[:] = ms.imol['g']
liq.mol[:] = ms.imol['l']
vap.T = liq.T = ms.T
vap.P = liq.P = ms.P
[docs]
def material_balance(chemical_IDs, variable_inlets, constant_inlets=(),
constant_outlets=(), is_exact=True, balance='flow'):
"""
Solve stream mass balance by iteration.
Parameters
----------
chemical_IDs : tuple[str]
Chemicals that will be used to solve mass balance linear equations.
The number of chemicals must be same as the number of input streams varied.
variable_inlets : Iterable[Stream]
Inlet streams that can vary in net flow rate to accomodate for the
mass balance.
constant_inlets: Iterable[Stream], optional
Inlet streams that cannot vary in flow rates.
constant_outlets: Iterable[Stream], optional
Outlet streams that cannot vary in flow rates.
is_exact=True : bool, optional
True if exact flow rate solution is required for the specified IDs.
balance='flow' : {'flow', 'composition'}, optional
* 'flow': Satisfy output flow rates
* 'composition': Satisfy net output molar composition
Examples
--------
Vary inlet flow rates to satisfy outlet flow rates:
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> in_a = tmo.Stream('in_a', Water=1)
>>> in_b = tmo.Stream('in_b', Ethanol=1)
>>> variable_inlets = [in_a, in_b]
>>> in_c = tmo.Stream('in_c', Water=100)
>>> constant_inlets = [in_c]
>>> out_a = tmo.Stream('out_a', Water=200, Ethanol=2)
>>> out_b = tmo.Stream('out_b', Ethanol=100)
>>> constant_outlets = [out_a, out_b]
>>> chemical_IDs = ('Water', 'Ethanol')
>>> tmo.separations.material_balance(chemical_IDs, variable_inlets, constant_inlets, constant_outlets)
>>> tmo.Stream.sum([in_a, in_b, in_c]).mol - tmo.Stream.sum([out_a, out_b]).mol # Molar flow rates entering and leaving are equal
sparse([0., 0.])
Vary inlet flow rates to satisfy outlet composition:
>>> import thermosteam as tmo
>>> tmo.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> in_a = tmo.Stream('in_a', Water=1)
>>> in_b = tmo.Stream('in_b', Ethanol=1)
>>> variable_inlets = [in_a, in_b]
>>> in_c = tmo.Stream('in_c', Water=100)
>>> constant_inlets = [in_c]
>>> out_a = tmo.Stream('out_a', Water=200, Ethanol=2)
>>> out_b = tmo.Stream('out_b', Ethanol=100)
>>> constant_outlets = [out_a, out_b]
>>> chemical_IDs = ('Water', 'Ethanol')
>>> tmo.separations.material_balance(chemical_IDs, variable_inlets, constant_inlets, constant_outlets, balance='composition')
>>> tmo.Stream.sum([in_a, in_b, in_c]).z_mol - tmo.Stream.sum([out_a, out_b]).z_mol # Molar composition entering and leaving are equal
array([0., 0.])
"""
# SOLVING BY ITERATION TAKES 15 LOOPS FOR 2 STREAMS
# SOLVING BY LEAST-SQUARES TAKES 40 LOOPS
solver = np.linalg.solve if is_exact else np.linalg.lstsq
# Set up constant and variable streams
if not variable_inlets:
raise ValueError('variable_inlets must contain at least one stream')
index = variable_inlets[0].chemicals.get_index(chemical_IDs)
mol_out = sum([s.mol for s in constant_outlets]).to_array()
inlet_mols = np.array([s.mol.to_array() for s in variable_inlets]).transpose()
if balance == 'flow':
# Perform the following calculation: Ax = b = f - g
# Where:
# A = flow rate array
# x = factors
# b = target flow rates
# f = output flow rates
# g = constant inlet flow rates
# Solve linear equations for mass balance
A = inlet_mols[index, :]
f = mol_out[index]
g = sum([s.mol[index] for s in constant_inlets])
b = f - g
x = solver(A, b)
# Set flow rates for input streams
for factor, s in zip(x, variable_inlets):
s.mol[:] = s.mol * factor
elif balance == 'composition':
# Perform the following calculation:
# Ax = b
# = sum( A_ * x_guess + g_ )f - g
# = A_ * x_guess * f - O
# O = sum(g_)*f - g
# Where:
# A_ is flow array for all species
# g_ is constant flows for all species
# Same variable definitions as in 'flow'
# Set all variables
A_ = inlet_mols.copy()
A = inlet_mols[index, :]
F_mol_out = mol_out.sum()
z_mol_out = mol_out / F_mol_out if F_mol_out else mol_out
f = z_mol_out[index]
g_ = sum([s.mol for s in constant_inlets])
g = g_[index]
O = sum(g_) * f - g
# Solve by iteration
x_guess = np.ones_like(index)
not_converged = True
while not_converged:
# Solve linear equations for mass balance
b = (A_ * x_guess).sum()*f + O
x_new = solver(A, b)
infeasibles = x_new < 0.
if infeasibles.any(): x_new -= x_new[infeasibles].min()
denominator = x_guess.copy()
denominator[denominator == 0.] = 1.
not_converged = sum(((x_new - x_guess)/denominator)**2) > 1e-6
x_guess = x_new
# Set flow rates for input streams
for factor, s in zip(x_new, variable_inlets):
s.mol = s.mol * factor
else:
raise ValueError( "balance must be one of the following: 'flow', 'composition'")