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          310
Total purchase cost                 USD     4.63e+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