14. Gotchas!#
BioSTEAM reduces the time needed to develop and evaluate a production process by simplifying pressure handling and by providing optional modeling assumptions and a range of alternative specifications. New users may not expect some of these “BioSTEAM” behaviours. This chapter lists common “gotchas” so that you won’t have to scratch your brain too hard.
14.1. Mixer outlet pressure#
When streams at different pressures are mixed, BioSTEAM assumes valves reduce the pressure of inlet streams to prevent backflow:
[1]:
import biosteam as bst
from biorefineries.sugarcane import chemicals
bst.nbtutorial()
bst.settings.set_thermo(chemicals)
# Mix streams at different pressures
s_in1 = bst.Stream('s_in1', Water=1, units='kg/hr',P=2*101325)
s_in2 = bst.Stream('s_in2', Water=1, units='kg/hr', P=101325)
M1 = bst.Mixer('M1', ins=[s_in1, s_in2], outs='s_out')
M1.simulate()
M1.outs[0].show() # Note how the outlet stream pressure is the minimum inlet stream pressure
Stream: s_out from <Mixer: M1>
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 0.111
14.2. Optional phase equilibrium.#
Mixing streams with different phases does not perform vapor-liquid equilibrium (VLE) by default:
[2]:
s_in2.phase = 'g'
s_in2.T = 380
M1.simulate()
M1.outs[0].show() # Notice how the outlet is liquid and above the boiling point (energy balance is conserved)
Stream: s_out from <Mixer: M1>
phase: 'l', T: 581.71 K, P: 101325 Pa
flow (kmol/hr): Water 0.111
For rigorous VLE during mixing, set rigorous=True
:
[3]:
M1.rigorous = True
M1.simulate()
M1.outs[0].show()
MultiStream: s_out from <Mixer: M1>
phases: ('g', 'l'), T: 373.12 K, P: 101325 Pa
flow (kmol/hr): (g) Water 0.0481
(l) Water 0.0629
Heat exchangers also don’t perform VLE by default:
[4]:
H1 = bst.HXutility(
'H1', ins=bst.Stream('feed', Water=1000, units='kg/hr'), outs='outlet', T=400
)
H1.simulate()
H1.outs[0].show()
Stream: outlet from <HXutility: H1>
phase: 'l', T: 400 K, P: 101325 Pa
flow (kmol/hr): Water 55.5
Set rigorous=True
for rigorous VLE during:
[5]:
H1.rigorous = True
H1.simulate()
H1.outs[0].show()
MultiStream: outlet from <HXutility: H1>
phases: ('g', 'l'), T: 400 K, P: 101325 Pa
flow (kmol/hr): (g) Water 55.5
Streams do not perform phase equlibrium when initialized:
[6]:
s1 = bst.Stream('s1', Water=1, Ethanol=1, T=400, P=101325)
s1.show() # It should be a gas, but the phase defaults to liquid
Stream: s1
phase: 'l', T: 400 K, P: 101325 Pa
flow (kmol/hr): Water 1
Ethanol 1
To perform phase equilibrium during initialization, pass vlle=True
:
[7]:
s2 = bst.Stream('s2', Water=1, Ethanol=1, T=400, P=101325, vlle=True)
s2.show() # Now it is gas, as it should be
Stream: s2
phase: 'g', T: 400 K, P: 101325 Pa
flow (kmol/hr): Water 1
Ethanol 1
14.3. Simplified pressure handling#
Pressure drops of unit operations are not accounted for due to the tremendous amount of detail required to do so (e.g., specifying pipe fittings and dimensions). Ultimately, we just want to estimate pump sizes and motor requirements. As a preliminary assumption, we can specify the design pressure gain that a pump must operate, dP_design
, to estimate these requirements:
[8]:
s3 = bst.Stream('s3', Water=1000, units='kg/hr')
P1 = bst.Pump('P1', ins=s3, outs='s4', P=101325, dP_design=4 * 101325)
P1.simulate()
# Note how, although inlet and outlet pressures are the same, the pump is assummed to work at a 4 atm pressure gain
P1.outs[0].show()
print()
print(P1.results())
Stream: s4 from <Pump: P1>
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 55.5
Pump Units P1
Electricity Power kW 0.321
Cost USD/hr 0.0251
Design Type Centrifugal
Ideal power hp 0.151
Flow rate gpm 4.42
Efficiency 0.352
Power hp 0.43
Head ft 386
Motor size hp 0.5
Purchase cost Pump USD 4.32e+03
Motor USD 289
Total purchase cost USD 4.61e+03
Utility cost USD/hr 0.0251
14.4. Indexers for everything!#
Indexers are very powerful tools for faster development in BioSTEAM, but may lead to some confusion. Here we look into the many use cases for clarity. In BioSTEAM, all flow rate data is stored in mol, but we can easily access mass flow rate data through indexers which reference molar data:
[9]:
s5 = bst.Stream('s5', Water=1000, units='kg/hr')
print(s5.imol['Water'], s5.imass['Water'])
55.508435061791985 1000.0
[10]:
s5.imass['Water', 'Ethanol']
[10]:
array([1000., 0.])
Chemical groups (e.g., the “fiber” group defined below) can be used to get and set “lumped” chemical flow rates in a stream. However, they are not linked to the stream’s flow rate data:
[11]:
# Define a lignocellulose chemical group with composition
# 25, 47, and 28 wt. % lignin, cellulose, and hemicellulose, respectively
chemicals.define_group(
name='fiber',
IDs=['Lignin', 'Cellulose', 'Hemicellulose'],
composition=[0.25, 0.47, 0.28],
wt=True,
)
s6 = bst.Stream('s6', Water=1, fiber=1, units='kg/hr')
s6.show(flow='kg/hr')
Stream: s6
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 1
Cellulose 0.47
Hemicellulose 0.28
Lignin 0.25
[12]:
s6.imass['fiber']
[12]:
1.0
[13]:
s6.imass['fiber', 'Water'] # All data indexed with chemical groups are also floats
[13]:
array([1., 1.])
[14]:
s6.imass['fiber', 'Water'] = [100, 100]
s6.show() # Note how we can still set flow rates this way
Stream: s6
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 5.55
Cellulose 0.29
Hemicellulose 0.212
Lignin 0.164
When chemical groups are used to define chemicals splits (e.g., at a splitter), they work as nested indices with broadcasting:
[15]:
s7 = bst.Stream('s7', Water=1, fiber=1, units='kg/hr')
S1 = bst.Splitter('S1', ins=s7, split=dict(Water=0.1, fiber=0.5))
S1.isplit.show() # Note how the split is broadcasted to all components
SplitIndexer:
Water 0.1
Cellulose 0.5
Hemicellulose 0.5
Lignin 0.5
[16]:
S1.isplit['fiber'] # Note how the splits for each component is returned (not the sum)
[16]:
array([0.5, 0.5, 0.5])
[17]:
S1.isplit['fiber', 'Water'] # Note how the splits are nested
[17]:
array([array([0.5, 0.5, 0.5]), 0.1], dtype=object)
[18]:
S1.isplit['fiber'] = [0.2, 0.5, 0.7] # This is also valid
S1.isplit.show()
SplitIndexer:
Water 0.1
Cellulose 0.5
Hemicellulose 0.7
Lignin 0.2