9. Inheriting from Unit#
9.1. Layout#

A Unit subclass has class attributes that dictate how an instance is initialized:
_F_BM_default
: dict[str, float] Default bare module factors for each purchase cost item._units
: [dict] Units of measure for thedesign_results
items._N_ins
=1 : [int] Expected number of input streams._N_outs
=1 : [int] Expected number of output streams._ins_size_is_fixed
=True : [bool] Whether the number of streams in ins is fixed._outs_size_is_fixed
=True : [bool] Whether the number of streams in outs is fixed.auxiliary_unit_names
=() : tuple[str] Name of attributes that are auxiliary units._graphics
: [biosteam Graphics] A Graphics object for diagram representation. Defaults to a box diagram._default_equipment_lifetime
: [int] or dict[str, int] Default lifetime of equipment. Defaults to lifetime of production venture.line
: [str] Label for the unit operation in a diagram. Defaults to the class name.
Abstract methods are used to run heat and mass balances, find design requirements, and cost the unit:
_run()
: Called during System convergece to specifyouts
streams._design()
: Called after System convergence to find design requirements._cost()
: Called after_design
to find cost requirements.
These abstract methods will rely on the following instance attributes:
ins
: Inlets[Stream] Input streams.outs
: Outlets[Stream] Output streams.power_utility
: [PowerUtility] Used to add electricity rate requirement.heat_utilities
: list[HeatUtility] Used to add cooling and heating requirements.design_results
: [dict] All design requirements.baseline_purchase_costs
: [dict] Itemized purchase costs (without accounting for design, pressure or material factors).parallel
: [dict] Name-number pairs of baseline purchase costs and auxiliary unit operations in parallel.F_BM
: [dict] Bare-module factors.F_D
: [dict] Design factors.F_P
: [dict] Pressure factors.F_M
: [dict] Material factors.equipment_lifetime
: [dict] Lifetime of each equiment.thermo
: [Thermo] The thermodynamic property package used by the unit.
9.1.1. Subclass example#
The following example depicts inheritance from Unit by creating a new Boiler class:
[1]:
import biosteam as bst
from math import ceil
bst.nbtutorial()
class Boiler(bst.Unit):
"""
Create a Boiler object that partially boils the feed.
Parameters
----------
ins :
Inlet fluid.
outs :
* [0] vapor product
* [1] liquid product
V : float
Molar vapor fraction.
P : float
Operating pressure [Pa].
"""
# Note that the documentation does not include `ID` or `thermo` in the parameters.
# This is OK, and most subclasses in BioSTEAM are documented this way too.
# Documentation for all unit operations should include the inlet and outlet streams
# listed by index. If there is only one stream in the inlets (or outlets), there is no
# need to list out by index. There is no need to specify the types for the `ins` and `outs` either;
# BioSTEAM automatically adds the correct types for these when creating the documentation.
# Any additional arguments to the unit should also be listed (e.g. V, and P).
_N_ins = 1 # Number in inlets
_N_outs = 2 # Number in outlets
_units = {'Area': 'm^2'}
def _init(self, V, P):
# The _init methods adds input parameters for unit creation
self.V = V #: Molar vapor fraction.
self.P = P #: Operating pressure [Pa].
def _run(self):
# Equivalent to self.ins[0] when the number of inlets is one
feed = self.feed
vap, liq = self.outs
# Perform vapor-liquid equilibrium
stream = feed.copy()
stream.vle(V=self.V, P=self.P)
# Update outlet streams
vap.copy_like(stream['g'])
liq.copy_like(stream['l'])
def _design(self):
# Add heat utility requirement
T_operation = self.outs[0].T
duty = self.H_out - self.H_in
if duty < 0: raise RuntimeError(f'{repr(self)} is cooling.')
heat_utility = self.add_heat_utility(duty, T_operation) # New utility is also in self.heat_utilities
# Temperature of utility at entrance
T_utility = heat_utility.inlet_utility_stream.T
# Temeperature gradient
dT = T_utility - T_operation
# Heat transfer coefficient kJ/(hr*m2*K)
U = 8176.699
# Area requirement (m^2)
A = duty /(U * dT)
# Maximum area per unit
A_max = 743.224
# Number of units
N = ceil(A / A_max)
# Design requirements (excluding utilities) are stored here
self.design_results['Area'] = A / N
# The capital cost of all boilers will be equal to the cost of a single
# boiler by the number of boilers
self.parallel['Boiler'] = N
def _cost(self):
A = self.design_results['Area']
# Long-tube vertical boiler cost correlation from
# "Product process and design". Warren et. al. (2016) Table 22.32, pg 592
purchase_cost = bst.settings.CEPCI * 3.086 * A **0.55
# Itemized purchase costs are stored here
self.baseline_purchase_costs['Boiler'] = purchase_cost # Not accounting for material factor
# Assume design, pressure, and material factors are 1.
self.F_D['Boiler'] = self.F_P['Boiler'] = self.F_M['Boiler'] = 1.
# Set bare-module factor for boilers
self.F_BM['Boiler'] = 2.45
9.1.2. Simulation test#
[2]:
import biosteam as bst
bst.settings.set_thermo(['Water'])
water = bst.Stream('water', Water=300)
B1 = Boiler('B1', ins=water, outs=('gas', 'liq'),
V=0.5, P=101325)
B1.diagram()
B1.show()
Boiler: B1
ins...
[0] water
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 300
outs...
[0] gas
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
[1] liq
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
[3]:
B1.simulate()
B1.show()
Boiler: B1
ins...
[0] water
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 300
outs...
[0] gas
phase: 'g', T: 373.12 K, P: 101325 Pa
flow (kmol/hr): Water 150
[1] liq
phase: 'l', T: 373.12 K, P: 101325 Pa
flow (kmol/hr): Water 150
[4]:
# Note that utility requirements, items in the `design_results` dictionary,
# and purchase costs are automatically included in the results dataframe.
B1.results()
[4]:
Boiler | Units | B1 | |
---|---|---|---|
Low pressure steam | Duty | kJ/hr | 8.21e+06 |
Flow | kmol/hr | 212 | |
Cost | USD/hr | 50.4 | |
Design | Area | m^2 | 24.4 |
Purchase cost | Boiler | USD | 1.02e+04 |
Total purchase cost | USD | 1.02e+04 | |
Utility cost | USD/hr | 50.4 |
[5]:
# Note that the number of boilers are automatically accounted for
# through the `parallel` dictionany.
B1.feed.scale(100) # Rescale flow rate x100
B1.simulate()
B1.results()
[5]:
Boiler | Units | B1 | |
---|---|---|---|
Low pressure steam | Duty | kJ/hr | 8.21e+08 |
Flow | kmol/hr | 2.12e+04 | |
Cost | USD/hr | 5.04e+03 | |
Design | Area | m^2 | 610 |
Purchase cost | Boiler (x4) | USD | 2.38e+05 |
Total purchase cost | USD | 2.38e+05 | |
Utility cost | USD/hr | 5.04e+03 |
9.1.3. Graphviz attributes#
All graphviz attributes for generating a diagram are stored in _graphics
as a Graphics object. One Graphics object is generated for each Unit subclass:
[6]:
graphics = Boiler._graphics
edge_in = graphics.edge_in
edge_out = graphics.edge_out
node = graphics.node
[7]:
# Attributes correspond to each inlet stream respectively
# For example: Attributes for B1.ins[0] would correspond to edge_in[0]
edge_in
[7]:
[{'headport': 'c'}]
[8]:
# Attributes correspond to each outlet stream respectively
# For example: Attributes for B1.outs[0] would correspond to edge_out[0]
edge_out
[8]:
[{'tailport': 'c'}, {'tailport': 'c'}]
[9]:
node # The node represents the actual unit
[9]:
{'shape': 'box',
'style': 'filled',
'gradientangle': '0',
'width': '0.6',
'height': '0.6',
'orientation': '0.0',
'peripheries': '1',
'margin': 'default',
'fontname': 'Arial'}
These attributes can be changed to the user’s liking:
[10]:
edge_out[0]['tailport'] = 'n'
edge_out[1]['tailport'] = 's'
node['width'] = '1'
node['height'] = '1.2'
[11]:
B1.diagram()
It is also possible to dynamically adjust node and edge attributes by setting the tailor_node_to_unit
attribute:
[12]:
def tailor_node_to_unit(node, unit):
if unit.feed.isempty(): node['label'] += '\n-empty-'
graphics.tailor_node_to_unit = tailor_node_to_unit
B1.diagram()
[13]:
B1.ins[0].empty()
B1.diagram()
NOTE: The example implementation of the tailor_node_to_unit
function is not suggested; best to keep diagrams simple.
9.1.4. Cost decorator#
The cost decorator adds a free on board purchase cost based on the exponential scale up equation:
\(New\ cost = N \cdot cost \bigg(\frac{CE_{new}}{CE}\bigg) \bigg(\frac{S_{new}}{N \cdot S}\bigg)^{n}\)
\(Electricity\ rate = kW \bigg(\frac{S_{new}}{S}\bigg)\)
\(N = ceil \bigg( \frac{S_{new}}{ub} \bigg)\)
\(N\text{: Number of units}\)
\(ub\text{: Upper bound of size}\)
\(CE_{new}\text{: New Chemical Engineering Plant Cost Index}\)
\(CE\text{: Chemical Engineering Plant Cost Index}\)
\(S_{new}\text{: New size value}\)
\(S\text{: Original size value}\)
\(cost\text{: Free on board purchase cost at size S}\)
\(kW\text{: Electricity rate in kW at size S}\)
\(n\text{: Exponential factor}\)
\(lifetime\text{: Number of operating years until equipment needs to be replaced}\)
Create a Shredder Unit subclass for sugar cane with the following exponential scale up factors [1]:
Chemical Engineering Plant Cost Index: 567.3
Size: 500,000 kg/hr flow rate
Purchase cost: $2,500,000
Electricity rate: 3,000 kW
Exponential factor: 0.6
Bare module factor: 1.39
Equipment lifetime: 30
Additionally, include a bare module factor of 1.39 as an approximation for this example [2].
[14]:
import biosteam as bst
from biosteam.units.decorators import cost
# Set up thermo property package
chemicals = bst.Chemicals(['Water', 'Ethanol'])
SugarCane = bst.Chemical.blank('SugarCane', phase_ref='s')
SugarCane.default()
chemicals.append(SugarCane)
bst.settings.set_thermo(chemicals)
# For year 2018
bst.CE = 603.1
# basis will be the total flow rate in kg/hr
@cost('Flow rate', units='kg/hr', cost=2.5e6, CE=567.3,
n=0.6, S=500e3, kW=3000, BM=1.39, lifetime=30)
class Shredder(bst.Unit): pass
# Units without a `_run` method assume one input and output stream
# that share the same flow rates and conditions
### Test a Shreadder object ###
# Display flow rate in kg/hr
bst.Stream.display_units.flow = 'kg/hr'
feed = bst.Stream(SugarCane=1e6, units='kg/hr')
shredder = Shredder(ins=feed)
shredder.simulate()
shredder.show()
shredder.results()
Shredder: U1
ins...
[0] s1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): SugarCane 1e+06
outs...
[0] s2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): SugarCane 1e+06
[14]:
Shredder | Units | U1 | |
---|---|---|---|
Electricity | Power | kW | 6e+03 |
Cost | USD/hr | 469 | |
Design | Flow rate | kg/hr | 1e+06 |
Purchase cost | Shredder | USD | 4.03e+06 |
Total purchase cost | USD | 4.03e+06 | |
Utility cost | USD/hr | 469 |
Cost data is stored as CostItem objects in the cost_items
dictionary:
[15]:
Shredder.cost_items
[15]:
{'Shredder': <CostItem: Flow rate (kg/hr)>}
[16]:
Shredder.cost_items['Shredder']
CostItem: Flow rate (kg/hr)
S 5e+05
CE 567
cost 2.5e+06
n 0.6
kW 3e+03
Bare module factors and equipment lifetimes are stored separately:
[17]:
Shredder._default_equipment_lifetime
[17]:
{'Shredder': 30}
Any option can be changed:
[18]:
Shredder.cost_items['Shredder'].cost = 3e6 # Change base cost
# This also works:
# Shredder.cost_items['Shredder']['cost'] = 3e6
shredder.simulate()
shredder.results()
[18]:
Shredder | Units | U1 | |
---|---|---|---|
Electricity | Power | kW | 6e+03 |
Cost | USD/hr | 469 | |
Design | Flow rate | kg/hr | 1e+06 |
Purchase cost | Shredder | USD | 4.83e+06 |
Total purchase cost | USD | 4.83e+06 | |
Utility cost | USD/hr | 469 |
[19]:
Shredder.cost_items['Shredder'].ub = 6e5 # Change size upper bound
shredder.simulate()
shredder.results()
[19]:
Shredder | Units | U1 | |
---|---|---|---|
Electricity | Power | kW | 6e+03 |
Cost | USD/hr | 469 | |
Design | Flow rate | kg/hr | 1e+06 |
Purchase cost | Shredder (x2) | USD | 6.38e+06 |
Total purchase cost | USD | 6.38e+06 | |
Utility cost | USD/hr | 469 |
Note: It is also possible to decorate a Unit subclass multiple times to add multiple cost items.
It is also possible to extend the decorated cost. In this next example we will add an agitator to a flash vessel with the following exponential scale up factors [3]:
Chemical Engineering Plant Cost Index: 522
Size: 252,891 kg/hr flow rate
Purchase cost: $90,000
Electricity rate: 170 kW
Exponential factor: 0.5
Bare module factor: 1.5
Lifetime: Number of biorefinery operating years
[20]:
@cost('Flow rate', 'Agitator', units='kg/hr',
cost=90e3, S=252891, kW=170, CE=522, n=0.5, BM=1.5)
class FlashWithAgitator(bst.Flash):
def _design(self):
super()._design()
self._decorated_design()
def _cost(self):
# Run flash cost algorithm
super()._cost()
# Run decorated cost algorithm
self._decorated_cost()
# Test
F1 = FlashWithAgitator('F1', bst.Stream('feed', Water=800, Ethanol=500, T=350),
V=0.5, P=101325)
F1.simulate()
F1.results()
[20]:
Flash with agitator | Units | F1 | |
---|---|---|---|
Electricity | Power | kW | 25.2 |
Cost | USD/hr | 1.97 | |
Low pressure steam | Duty | kJ/hr | 2.82e+07 |
Flow | kmol/hr | 728 | |
Cost | USD/hr | 173 | |
Design | Vessel type | Vertical | |
Length | ft | 12.5 | |
Diameter | ft | 8.5 | |
Weight | lb | 8.14e+03 | |
Wall thickness | in | 0.438 | |
Flow rate | kg/hr | 3.74e+04 | |
Vessel material | Carbon steel | ||
Purchase cost | Vertical pressure vessel | USD | 4.2e+04 |
Platform and ladders | USD | 1.19e+04 | |
Agitator | USD | 4e+04 | |
Heat exchanger - Floating head | USD | 3.65e+04 | |
Total purchase cost | USD | 1.3e+05 | |
Utility cost | USD/hr | 175 |
9.1.5. Auxiliary units#
A unit operation may be composed of several auxiliary unit operations. To implement auxiliary units, define their names in auxiliary_unit_names
and instantiate them using the auxiliary
method. In the following example, we create a new unit class composed of a mix tank, a pump, and a heat exchanger:
[21]:
class HeatedTank(bst.Unit):
# Define names of auxiliary units
auxiliary_unit_names = ('mix_tank', 'pump', 'heat_exchanger')
def _init(self, T):
pump = self.auxiliary(
'pump', # name of auxiliary unit
bst.Pump, # class of auxiliary unit
ins=self.ins, # inlets to auxiliary unit (can be the same as parent unit)
)
heat_exchanger = self.auxiliary(
'heat_exchanger', # name
bst.HXutility, # class
ins=pump.outlet, # outlet from pump is the inlet to heat exchanger
T=T, #: additional arguments to HXutility (Temperature [K])
)
self.auxiliary(
'mix_tank', # name
bst.MixTank, # class
ins=heat_exchanger.outlet, # connect
outs=self.outs, # outlet from auxiliary unit (can be the same as parent unit)
)
def _run(self):
# Run auxiliary unit operations internally
self.pump._run()
self.heat_exchanger._run()
self.mix_tank._run()
def _design(self):
# Explicitly design and cost auxiliaries
self.pump._design()
self.heat_exchanger._design()
self.mix_tank._design()
def _cost(self):
self.pump._cost()
self.heat_exchanger._cost()
self.mix_tank._cost()
feed = bst.Stream('feed', Water=200)
HT1 = HeatedTank('HT1', ins=feed, T=310)
HT1.simulate()
HT1.diagram(format='svg')
HT1.show()
HT1.results()
HeatedTank: HT1
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kg/hr): Water 3.6e+03
outs...
[0] s5
phase: 'l', T: 310 K, P: 101325 Pa
flow (kg/hr): Water 3.6e+03
[21]:
Heated tank | Units | HT1 | |
---|---|---|---|
Electricity | Power | kW | 0.736 |
Cost | USD/hr | 0.0575 | |
Low pressure steam | Duty | kJ/hr | 1.88e+05 |
Flow | kmol/hr | 4.86 | |
Cost | USD/hr | 1.15 | |
Purchase cost | Mix tank - Tank | USD | 3.07e+04 |
Pump - Pump | USD | 4.64e+03 | |
Pump - Motor | USD | 273 | |
Heat exchanger - Double pipe | USD | 3.84e+03 | |
Total purchase cost | USD | 3.94e+04 | |
Utility cost | USD/hr | 1.21 |
Note that BioSTEAM takes care of adding utilities and costs from auxiliary units to the parent unit. Auxiliary units are displayed by default in unit diagrams, but it is possible to supress this behavior by passing auxiliaries=0
:
[22]:
HT1.diagram(auxiliaries=0, format='png')

9.1.6. References#
Huang, H., Long, S., & Singh, V. (2016) “Techno-economic analysis of biodiesel and ethanol co-production from lipid-producing sugarcane” Biofuels, Bioproducts and Biorefining, 10(3), 299–315. https://doi.org/10.1002/bbb.1640
Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, R., & Ng, M. K. (2017). Product and Process Design Principles. Wiley. Cost Accounting and Capital Cost Estimation (Chapter 16)
Humbird, D., Davis, R., Tao, L., Kinchin, C., Hsu, D., Aden, A., Dudgeon, D. (2011). Process Design and Economics for Biochemical Conversion of Lignocellulosic Biomass to Ethanol: Dilute-Acid Pretreatment and Enzymatic Hydrolysis of Corn Stover (No. NREL/TP-5100-47764, 1013269). https://doi.org/10.2172/1013269