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
)
../_images/tutorial_Creating_a_System_5_0.png
[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')
../_images/tutorial_Creating_a_System_10_0.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')
../_images/tutorial_Creating_a_System_14_0.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')
../_images/tutorial_Creating_a_System_17_0.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')
../_images/tutorial_Creating_a_System_22_0.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')
../_images/tutorial_Creating_a_System_37_0.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