10. Creating a System#
10.1. Conventional methods#
Systems are defined by a recycle stream (i.e. a tear stream; if any), and a path of unit operations and nested systems. A System object takes care of solving recycle streams by iteratively running its path of units and subsystems until the recycle converges to steady state. Systems can be manually created or automatically generated via the flowsheet or by context management.
10.1.1. Manually generated#
Manually creating a system is not recommended as it requires an exponential amount of time and effort for an individual to layout an accurate path. Here we create a trivial system manually as a simple exercise:
[1]:
import biosteam as bst
bst.nbtutorial() # Ignore warnings and reset local BioSTEAM preferences
bst.settings.set_thermo(['Water'])
feed = bst.Stream('feed', Water=100)
recycle = bst.Stream('recycle')
effluent = bst.Stream('effluent')
T1 = bst.MixTank('T1', ins=[feed, recycle])
P1 = bst.Pump('P1', T1-0)
S1 = bst.Splitter('S1', P1-0, [effluent, recycle], split=0.5)
manual_sys = bst.System('manual_sys', path=[T1, P1, S1], recycle=recycle)
manual_sys.simulate()
manual_sys.diagram(
kind='cluster', # Cluster diagrams highlight recycle streams and nested systems.
number=True, # This numbers each unit according to their path simulation order
format='png', # Easier to view the whole diagram
)
[2]:
manual_sys.show()
System: manual_sys
Highest convergence error among components in recycle
stream S1-1 after 3 loops:
- flow rate 0.00e+00 kmol/hr (0%)
- temperature 0.00e+00 K (0%)
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 100
outs...
[0] effluent
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 100
Note that the inlets and outlets to a system are inherently connected to the unit operations within the system, but we can still connect systems just like unit operations, as depicted future examples.
10.1.2. Autogenerated from the flowsheet#
The recommended way of creating systems is to use the flowsheet. Here we expand on the existing process and create a new system using the flowsheet:
[3]:
water = bst.Stream('water', Water=10)
P2 = bst.Pump('P2', manual_sys-0) # -pipe- notation equivalent to manual_sys.outs[0]
M2 = bst.Mixer('M2', [P2-0, water])
flowsheet_sys = bst.main_flowsheet.create_system('flowsheet_sys')
flowsheet_sys.simulate()
# Units in subsystems are numbered with after a "." to represent
# the subsystem depth (checkout SYS1 in the diagram)
flowsheet_sys.diagram(kind='cluster', number=True, format='png')
[4]:
flowsheet_sys.show()
System: flowsheet_sys
Highest convergence error among components in recycle
stream S1-1 after 1 loops:
- flow rate 0.00e+00 kmol/hr (0%)
- temperature 0.00e+00 K (0%)
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 100
[1] water
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 10
outs...
[0] s4
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 110
10.1.3. Autogenerated by context management#
System objects’ context management feature allows for creating systems of only the units created within the given context:
[5]:
downstream_recycle = bst.Stream('downstream_recycle')
product = bst.Stream('product')
with bst.System('context_sys') as context_sys:
T2 = bst.MixTank('T2', ins=['', downstream_recycle])
P3 = bst.Pump('P3', T2-0)
S2 = bst.Splitter('S2', P3-0, [product, downstream_recycle], split=0.5)
# The feed is empty, no need to run system (yet)
context_sys.diagram('cluster', format='png')
[6]:
context_sys.show()
System: context_sys
ins...
[0] s5
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
outs...
[0] product
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
Let’s connect two systems together and create a new system from the flowsheet:
[7]:
# -pipe- notation equivalent to context_sys.ins[:] = [flowsheet_sys.outs[0]]
flowsheet_sys-0-context_sys
complete_sys = bst.main_flowsheet.create_system('complete_sys')
complete_sys.simulate()
complete_sys.diagram('cluster', number=True, format='png')
[8]:
complete_sys.show()
System: complete_sys
Highest convergence error among components in recycle
stream S1-1 after 1 loops:
- flow rate 0.00e+00 kmol/hr (0%)
- temperature 0.00e+00 K (0%)
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 100
[1] water
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 10
outs...
[0] product
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 110
10.2. Drop-in systems#
10.2.1. A simple example#
When a system is created by a function, it’s called a drop-in system. Here, we create a sugarcane to ethanol production system without facilities (e.g., cooling tower, boiler) by using drop-in systems:
[9]:
from biosteam import Stream, System, settings, main_flowsheet
from biorefineries.cane import (
create_sugarcane_chemicals,
create_juicing_system,
create_sucrose_to_ethanol_system
)
chemicals = create_sugarcane_chemicals()
main_flowsheet.clear() # Remove previous unit operations to prevent ID-conflict warnings
settings.set_thermo(chemicals)
denaturant = Stream('denaturant',
Octane=230.69,
units='kg/hr',
price=0.756)
sucrose_solution = Stream('sucrose_solution')
juicing_sys = create_juicing_system(
ID='juicing_sys', # ID of system
outs=[sucrose_solution], # Place sucrose_solution at the 0th outlet (all other streams are defaulted)
)
sucrose_to_ethanol_sys = create_sucrose_to_ethanol_system(ins=[sucrose_solution, denaturant])
# Here are a couple of other ways to connect systems:
# Manually:
# >>> sucrose_to_ethanol_sys.ins[0] = juicing_sys.outs[0]
# With -pipe- notation:
# >>> juicing_sys-0-0-sucrose_to_ethanol_sys
# Manually create a new system to view a diagram of systems
sugarcane_to_ethanol_sys = System('sugarcane_to_ethanol_sys',
path=[juicing_sys, sucrose_to_ethanol_sys])
sugarcane_to_ethanol_sys.simulate() # Simulate before showing results
sugarcane_to_ethanol_sys.diagram(kind='surface', format='png')
[10]:
sugarcane_to_ethanol_sys.show(data=False)
System: sugarcane_to_ethanol_sys
Highest convergence error among components in recycle
stream M201-0 after 3 loops:
- flow rate 1.08e+00 kmol/hr (0.03%)
- temperature 9.93e-02 K (0.03%)
ins...
[0] imbibition_water
[1] sugarcane
[2] rvf_wash_water
[3] H3PO4
[4] lime
[5] polymer
[6] stripping_water
[7] dilution_water
[8] denaturant
outs...
[0] filter_cake
[1] fiber_fines
[2] bagasse
[3] vent
[4] evaporator_condensate
[5] stillage
[6] yeast
[7] recycle_process_water
[8] ethanol
The number of inlets and outlets are rather large. It may be helpful to specify what inlets and outlets do we want to expose:
[11]:
s = main_flowsheet.stream
sugarcane_to_ethanol_sys.load_inlet_ports([s.sugarcane], names={'feedstock': 0})
sugarcane_to_ethanol_sys.load_outlet_ports([s.ethanol, s.bagasse], names={'product': 0})
sugarcane_to_ethanol_sys.show(data=False)
System: sugarcane_to_ethanol_sys
Highest convergence error among components in recycle
stream M201-0 after 3 loops:
- flow rate 1.08e+00 kmol/hr (0.03%)
- temperature 9.93e-02 K (0.03%)
ins...
[0] sugarcane
outs...
[0] ethanol
[1] bagasse
The ethanol product is now the 0th stream
[12]:
sugarcane_to_ethanol_sys.outs[0].show()
Stream: ethanol from <StorageTank: T304>
phase: 'l', T: 339.26 K, P: 101325 Pa
flow (kmol/hr): Water 10.4
Ethanol 491
Octane 4.39
We can also use the names we gave earlier for getting and setting streams:
[13]:
print([sugarcane_to_ethanol_sys.get_inlet('feedstock'),
sugarcane_to_ethanol_sys.get_outlet('product')])
# You can also use <System>.set_inlet(name, stream)
[<Stream: sugarcane>, <Stream: ethanol>]
10.2.2. System factories#
Both create_juicing_system
and create_sucrose_to_ethanol_system
are SystemFactory objects, which accept the system ID
, ins
, and outs
(similar to unit operations) and return a new system. Let’s first have a look at some of the system factories in the biorefineries.sugarcane library:
[14]:
create_juicing_system.show()
print()
create_sucrose_to_ethanol_system.show()
SystemFactory(
f=<create_juicing_system(ins, outs, pellet_bagasse=None, dry_bagasse=None)>,
ID='juicing_sys',
ins=[dict(Water=0.7,
Glucose=0.01208,
Sucrose=0.1369,
Ash=0.006,
Cellulose=0.06115,
Hemicellulose=0.03608,
Lignin=0.03276,
Solids=0.015,
ID='sugarcane',
units='kg/hr',
price=0.03455,
total_flow=333334.2),
dict(H3PO4=74.23,
Water=13.1,
ID='H3PO4',
units='kg/hr',
price=0),
dict(CaO=333.0,
Water=2200.0,
ID='lime',
units='kg/hr',
price=0.077),
dict(Flocculant=0.83,
ID='polymer',
units='kg/hr',
price=0)],
outs=[dict(Glucose=3802,
Sucrose=43090.0,
Water=259000.0,
H3PO4=83.33,
ID='screened_juice',
T=372,
units='kg/hr'),
dict(ID='bagasse'),
dict(ID='fiber_fines')]
)
SystemFactory(
f=<create_sucrose_to_ethanol_system(ins, outs, add_urea=False)>,
ID='sucrose_to_ethanol_sys',
ins=[dict(Glucose=3802,
Sucrose=43090.0,
Water=259000.0,
H3PO4=83.33,
ID='screened_juice',
T=372,
units='kg/hr'),
dict(Octane=230.69,
ID='denaturant',
units='kg/hr',
price=0.756)],
outs=[dict(ID='ethanol',
price=0.789),
dict(ID='stillage'),
dict(ID='recycle_process_water'),
dict(ID='evaporator_condensate')]
)
SystemFactory objects are composed of a function f
which creates the unit operations, a predefined system ID
, and ins
and outs
dictionaries that serve as keyword arguments to initialize the system’s default inlets and outlets.
The signature of a SystemFactory is f(ID=None, ins=None, outs=None, mockup=False, area=None, udct=None, ...)
. The additional parameters (i.e. mockup, area, and udct) will be discussed in the next section.
10.2.3. Mock systems#
When creating a biorefinery, we may not be interested in all the subsystems we created with SystemFactory objects. We can save a few milliseconds in computational time (per system) by using mock systems:
[15]:
main_flowsheet.clear() # Remove previous unit operations to prevent ID-conflict warnings
juicing_sys = create_juicing_system(
outs=[sucrose_solution],
mockup=True
)
sucrose_to_ethanol_sys = create_sucrose_to_ethanol_system(
ins=[sucrose_solution, denaturant],
mockup=True
)
# Note that mock systems don't have anything other than `ins`, `outs`, and `units`
juicing_sys.show()
sucrose_to_ethanol_sys.show()
MockSystem(
ins=[0-U201, 1-T203, 0-T204, 1-T206],
outs=[S202-0, U202-0, S202-1],
units=[U201, U202, M201, S201, T202,
H201, T203, P201, T204, T205,
P202, M202, H202, T206, C201,
C202, P203, S202]
)
MockSystem(
ins=[0-F301, 0-T303],
outs=[T304-0, H302-1, P303-0, F301-1],
units=[F301, P306, M301, H301, R301,
T301, C301, S302, D301, M302,
P301, D302, P302, H302, M303,
D303, P303, H303, U301, H304,
T302, P304, T303, P305, M304,
T304]
)
[16]:
# We can create the system using the flowsheet
sugarcane_to_ethanol_sys = main_flowsheet.create_system('sugarcane_to_ethanol_sys')
sugarcane_to_ethanol_sys.simulate()
sugarcane_to_ethanol_sys.diagram(format='png')
[17]:
sucrose_to_ethanol_sys.outs[0].show()
Stream: ethanol from <StorageTank: T304>
phase: 'l', T: 339.26 K, P: 101325 Pa
flow (kmol/hr): Water 10.4
Ethanol 491
Octane 4.39
10.2.4. Using the area naming convention#
The area naming convention follows {letter}{area + number} where the letter depends on the unit operation as follows:
C: Centrifuge
D: Distillation column
E: Evaporator
F: Flash tank
H: Heat exchange
K: Compressor
Ʞ: Turbine
M: Mixer
P: Pump (including conveying belt)
R: Reactor
S: Splitter (including solid/liquid separator)
T: Tank or bin for storage
U: Other units
V: Valve
J: Junction, not a physical unit (serves to adjust streams)
PS: Process specificiation, not a physical unit (serves to adjust streams)
For example, the first mixer in area 100 would be named M101. When calling a SystemFactory object, we can pass the area
to name unit operations according to the area convention. In the following example, we name all unit operations in the juicing system under area 300:
[18]:
main_flowsheet.clear() # Remove previous unit operations
juicing_sys = create_juicing_system(area=300, mockup=True)
juicing_sys.show()
MockSystem(
ins=[0-U301, 1-T302, 0-T303, 1-T305],
outs=[U306-0, U302-0, U306-1],
units=[U301, U302, M301, U303, T301,
H301, T302, P301, T303, T304,
P302, M302, H302, T305, U304,
U305, P303, U306]
)
To access unit operations by their default ID (as originally defined in SystemFactory code), you can request a unit dictionary by passing udct
=True:
[19]:
main_flowsheet.clear() # Remove previous unit operations
# When udct is True, both the system and the unit dictionary are returned
juicing_sys, udct = create_juicing_system(mockup=True, area=300, udct=True)
unit = udct['U201']
print(repr(unit)) # Originally, this unit was named U201
<CrushingMill: U301>
10.2.5. Creating system factories#
Create a SystemFactory object for mixing in gasoline to ethanol:
[20]:
from biosteam import System, SystemFactory
@SystemFactory(
ID='denature_ethanol_sys',
ins=[dict(ID='dehydrated_ethanol',
Water=0.001,
Ethanol=0.999,
total_flow=9.05e7,
units='kg/hr'),
dict(ID='denaturant',
price=0.756)],
outs=[dict(ID='denatured_ethanol',
price=0.789)]
)
def create_denature_ethanol_sys(ins, outs):
# ins and outs will be stream objects
dehydrated_ethanol, denaturant = ins
denatured_ethanol, = outs
M1 = bst.Mixer('M1', ins=(dehydrated_ethanol, denaturant))
S1 = bst.StorageTank('S1', ins=M1-0, outs=denatured_ethanol)
# Create the specification function.
@M1.add_specification
def adjust_denaturant_flow():
denaturant_over_ethanol_flow = 0.02 / 0.98 # A mass ratio
denaturant.imass['Octane'] = denaturant_over_ethanol_flow * dehydrated_ethanol.F_mass
M1.run() # Run mass and energy balance
# The system factory builds a system from units created by the function
create_denature_ethanol_sys.show()
SystemFactory(
f=<create_denature_ethanol_sys(ins, outs)>,
ID='denature_ethanol_sys',
ins=[dict(ID='dehydrated_ethanol',
Water=0.001,
Ethanol=0.999,
total_flow=90500000.0,
units='kg/hr'),
dict(ID='denaturant',
price=0.756)],
outs=[dict(ID='denatured_ethanol',
price=0.789)]
)
Create the system and simulate:
[21]:
main_flowsheet.clear() # Remove previous unit operations
denature_ethanol_sys = create_denature_ethanol_sys()
denature_ethanol_sys.simulate()
denature_ethanol_sys.show('cwt')
System: denature_ethanol_sys
ins...
[0] dehydrated_ethanol
phase: 'l', T: 298.15 K, P: 101325 Pa
composition (%): Water 0.1
Ethanol 99.9
------- 9.05e+07 kg/hr
[1] denaturant
phase: 'l', T: 298.15 K, P: 101325 Pa
composition (%): Octane 100
------ 1.85e+06 kg/hr
outs...
[0] denatured_ethanol
phase: 'l', T: 298.15 K, P: 101325 Pa
composition (%): Water 0.098
Ethanol 97.9
Octane 2
------- 9.23e+07 kg/hr
Biorefinery systems can be created by connecting smaller systems, allowing us to create alternative configurations with ease. The biorefineries library has yet to fully implement SystemFactory objects across all functions that create systems, but that is the goal.
10.2.6. System meshes#
A system mesh represents a group of connected units, systems, or system factories. Their plug-and-play interface makes them more readable and flexible than system factories. In the following example, we will recreate the sugarcane_to_ethanol_system
using a system mesh object.
[22]:
from biosteam import SystemMesh
# Add system factories to the mesh.
# Inlet and outlet streams with the same names are automatically
# connected unless autoconnect=False is passed.
SM = SystemMesh()
SM.add('juicing', create_juicing_system) # Can add a Unit, System, or SystemFactory object
SM.add('ethanol_production', create_sucrose_to_ethanol_system)
# Alternatively:
# SM = SystemMesh()
# SM.add('juicing', create_juicing_system, autoconnect=False)
# SM.add('ethanol_production', create_sucrose_to_ethanol_system, autoconnect=False)
# SM.connect(outlet='screened_juice', inlet='screened_juice')
# Note how all inlets and outlets of each area are named
SM.show()
SystemMesh:
juicing (create_juicing_system)
ins [0] sugarcane
[1] H3PO4
[2] lime
[3] polymer
outs [0] screened_juice to 0-ethanol_production
[1] bagasse
[2] fiber_fines
ethanol_production (create_sucrose_to_ethanol_system)
ins [0] screened_juice from juicing-0
[1] denaturant
outs [0] ethanol
[1] stillage
[2] recycle_process_water
[3] evaporator_condensate
[23]:
main_flowsheet.clear() # Remove previous unit operations
sugarcane_to_ethanol_sys = SM(ID='sugarcane_to_ethanol_sys')
sugarcane_to_ethanol_sys.simulate()
ethanol = sugarcane_to_ethanol_sys.flowsheet('ethanol')
ethanol.show('cwt')
Stream: ethanol from <StorageTank: T304>
phase: 'l', T: 339.26 K, P: 101325 Pa
composition (%): Water 0.807
Ethanol 97
Octane 2.15
------- 2.33e+04 kg/hr