WaterTAP Costing Framework
The WaterTAP Costing Base class and utility functions contain extensions, methods, variables, and constraints common to all WaterTAP Costing Packages, and which would be useful for creating custom costing packages for WaterTAP. An example of using WaterTAP costing in a flowsheet is provided in the How to use WaterTAP Costing guide.
Extensions Over IDAES Costing Framework
The WaterTAP Costing Framework extends the functionality of the IDAES Process Costing Framework in several ways:
Unit models can self-register a default costing method by specifying a
default_costing_methodattribute. This allows the costing method(s) to be specified with the unit model definition.
import pyomo.environ as pyo
import idaes.core as idc
from watertap.costing import WaterTAPCosting
def cost_unit_model(blk):
blk.capital_cost = pyo.Var(
initialize=1,
units=blk.config.flowsheet_costing_block.base_currency,
bounds=(0, None),
doc="Capital cost of unit operation",
)
@idc.declare_process_block_class("MyUnitModel")
class MyUnitModelData(idc.UnitModelBlockData):
@property
def default_costing_method(self):
# could point to a static method on
# this class, could be function in
# a different module even
return cost_unit_model
m = pyo.ConcreteModel()
m.fs = idc.FlowsheetBlock(dynamic=False)
m.fs.costing = WaterTAPCosting()
m.fs.my_unit = MyUnitModel()
# the `default_costing_method_attribute` on the
# unit model is checked, and the function
# `cost_unit_model` returned then build the costing block
m.fs.my_unit.costing = idc.UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
)
2. The method register_flow_type will create a new Expression if a costing component is not already defined and the costing component is not constant.
The default behavior in IDAES is to always create a new Var. This allows the user to specify intermediate values in register_flow_type.
import pyomo.environ as pyo
import idaes.core as idc
from watertap.costing import WaterTAPCosting
m = pyo.ConcreteModel()
m.fs = idc.FlowsheetBlock(dynamic=False)
m.fs.costing = WaterTAPCosting()
m.fs.naocl_bulk_cost = pyo.Param(
mutable=True,
initialize=0.23,
doc="NaOCl cost",
units=pyo.units.USD_2018 / pyo.units.kg,
)
m.fs.naocl_purity = pyo.Param(
mutable=True,
initialize=0.15,
doc="NaOCl purity",
units=pyo.units.dimensionless,
)
# This will create an Expression m.fs.costing.naocl_cost whose expr is the second argument
# so changes to m.fs.naocl_bulk_cost and m.fs.naocl_purity will affect the underlying
# new Expression m.fs.costing.naocl_cost.
m.fs.costing.register_flow_type("naocl", m.fs.naocl_bulk_cost / m.fs.naocl_purity)
# This, however, will create a Var called m.fs.costing.caoh2_cost whose *value* is the second argument
m.fs.costing.register_flow_type("caoh2", 0.12 * pyo.units.USD_2018 / pyo.units.kg)
3. Unit models specify one of the global indirect capital cost multipliers, either TIC or TPEC (defined below) when defining their capital costs. The costing package will then aggregate both direct and total capital costs.
import pyomo.environ as pyo
import idaes.core as idc
from watertap.costing import WaterTAPCosting
def cost_my_unit_model(blk):
blk.capital_cost = pyo.Var(
initialize=1,
units=blk.config.flowsheet_costing_block.base_currency,
bounds=(0, None),
doc="Capital cost of unit operation",
)
# Adds blk.cost_factor, an expression pointing
# to the appropriate indirect capital cost adder
# and blk.direct_capital_cost, which is a expression
# defined to be blk.capital_cost / blk.cost_factor.
# Valid strings are "TIC" and "TPEC", all others
# will result in an indirect capital cost factor
# of 1.
blk.costing_package.add_cost_factor(blk, "TIC")
blk.capital_cost_constraint = pyo.Constraint(
expr=blk.capital_cost
== blk.cost_factor * (42 * pyo.units.USD_2018)
)
@idc.declare_process_block_class("MyUnitModel")
class MyUnitModelData(idc.UnitModelBlockData):
pass
m = pyo.ConcreteModel()
m.fs = idc.FlowsheetBlock(dynamic=False)
m.fs.costing = WaterTAPCosting()
m.fs.my_unit = MyUnitModel()
m.fs.my_unit.costing = idc.UnitModelCostingBlock(
costing_method=cost_my_unit_model,
flowsheet_costing_block=m.fs.costing,
)
m.fs.my_unit.costing.initialize()
m.fs.my_unit.costing.cost_factor.pprint()
m.fs.my_unit.costing.capital_cost.pprint()
m.fs.my_unit.costing.direct_capital_cost.pprint()
cost_factor : Size=1, Index=None
Key : Expression
None : fs.costing.TIC
capital_cost : Capital cost of unit operation
Size=1, Index=None, Units=USD_2018
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 0 : 84.0 : None : False : False : Reals
direct_capital_cost : Size=1, Index=None
Key : Expression
None : fs.my_unit.costing.capital_cost/fs.costing.TIC
4. A helper utility for defining global-level parameters specific to a unit model without changing the base costing package implementation.
import pyomo.environ as pyo
import idaes.core as idc
from watertap.costing import (
WaterTAPCosting,
register_costing_parameter_block,
make_capital_cost_var,
)
def build_my_unit_model_param_block(blk):
"""
This function builds the global parameters for MyUnitModel.
This function should also register needed flows using the
blk.parent_block().register_flow_type method on the costing package.
"""
blk.fixed_capital_cost = pyo.Var(
initialize=42,
doc="Fixed capital cost for all of my units",
units=pyo.units.USD_2020,
)
# This decorator ensures that the function
# `build_my_unit_model_param_block` is only
# added to the costing package once.
# It registers it as a sub-block with the
# name `my_unit`.
@register_costing_parameter_block(
build_rule=build_my_unit_model_param_block,
parameter_block_name="my_unit",
)
def cost_my_unit_model(blk):
"""
Cost an instance of MyUnitModel
"""
# creates the `capital_cost` Var
make_capital_cost_var(blk)
blk.costing_package.add_cost_factor(blk, "TIC")
# here we reference the `fixed_capital_cost` parameter
# automatically added by the `register_costing_parameter_block`
# decorator.
blk.capital_cost_constraint = pyo.Constraint(
expr=blk.capital_cost
== blk.cost_factor * blk.costing_package.my_unit.fixed_capital_cost
)
@idc.declare_process_block_class("MyUnitModel")
class MyUnitModelData(idc.UnitModelBlockData):
@property
def default_costing_method(self):
# could point to a static method on
# this class, could be function in
# a different module even
return cost_my_unit_model
m = pyo.ConcreteModel()
m.fs = idc.FlowsheetBlock(dynamic=False)
m.fs.costing = WaterTAPCosting()
m.fs.my_unit_1 = MyUnitModel()
# The `default_costing_method_attribute` on the
# unit model is checked, and the function
# `cost_my_unit_model` returned then build the costing block.
# This method also adds the `my_unit` global parameter block,
# so the global costing parameter m.fs.costing.my_unit.fixed_capital_cost
# is the same for all instances of MyUnitModel.
m.fs.my_unit_1.costing = idc.UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
)
m.fs.my_unit_2 = MyUnitModel()
# Here everything as before, but the global parameter block
# m.fs.costing.my_unit is not re-built.
m.fs.my_unit_2.costing = idc.UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
)
Costing Index and Technoeconomic Factors
Default costing indices are provided with the WaterTAP Costing Framework, but the user is free to modify these for their needs. Costs from year A to year B are adjusted according to:
WaterTAP uses the Chemical Engineering Plant Cost Index (CEPCI)
to account for the time-value of investments. Aggregated capital and operating costs are
adjusted to the desired year for the model, accessible on the costing block as base_currency.
The default costing year is 2018, but the user can directly set the base_currency at
the flowsheet level (e.g., m.fs.costing.base_currency = pyo.units.USD_2020).
Common Global Costing Parameters
The build_global_params method builds common cost factor parameters necessary to calculate aggregated metrics such as levelized cost of water (LCOW).
Note that the default values can be overwritten in the derived class.
Cost factor |
Variable |
Name |
Default Value |
Description |
|---|---|---|---|---|
Plant capacity utilization factor |
\(f_{util}\) |
|
90% |
Percentage of year plant is operating |
Electricity price |
\(P\) |
|
$0.07/kWh |
Electricity price in 2018 USD |
Electricity carbon intensity |
\(f_{eci}\) |
|
0.475 kg/kWh |
Carbon intensity of electricity |
Capital recovery factor |
\(f_{crf}\) |
|
10% |
Capital annualization (fraction of investment cost/year) |
Plant lifetime |
\(L\) |
|
30 years |
Plant lifetime |
Weighted average cost of capital |
\(f_{wacc}\) |
|
9.30734% |
Average cost of capital over plant lifetime |
Total purchased equipment cost (TPEC) |
\(f_{TPEC}\) |
|
4.121212 |
Common indirect capital cost multiplier for unit models |
Total installed cost (TIC) |
\(f_{TIC}\) |
|
2.0 |
Common indirect capital cost multiplier for unit models |
The relationship between the \(f_{crf}\), \(L\), and \(f_{wacc}\) is as follows:
\[f_{crf} = \frac{ f_{wacc} (1 + f_{wacc}) ^ L}{ (1 + f_{wacc}) ^ L - 1}\]
Therefore, exactly two of the variables capital_recovery_factor, plant_lifetime and wacc must be fixed. By default, plant_lifetime and wacc are fixed
and capital_recovery_factor is calculated.
The process-wide costs described below rely on two other factors that must be supplied by the derived class: the total investment factor and the maintenance-labor-chemical factor.
Cost factor |
Variable |
Name |
Default Value |
Description |
|---|---|---|---|---|
Total investment factor |
\(f_{toti}\) |
|
None |
Total investment factor (investment cost / equipment cost) |
Maintenance-labor-chemical factor |
\(f_{mlc}\) |
|
None |
Maintenance, labor, and chemical factor (fraction of equipment cost / year) |
Costing Process-Wide Costs
The WaterTAPCostingBlockData class includes variables necessary to calculate process-wide costs:
Cost |
Variable |
Name |
Description |
|---|---|---|---|
Total capital cost |
\(C_{ca,tot}\) |
|
Total capital cost |
Unit capital cost |
\(C_{ca,u}\) |
|
Unit processes capital cost |
Total operating cost |
\(C_{op,tot}\) |
|
Total operating cost for unit process |
Total fixed operating cost |
\(C_{op,fix}\) |
|
Total fixed operating cost for unit process |
Total variable operating cost |
\(C_{op,var}\) |
|
Total variable operating cost for unit process |
Total annualized cost |
\(C_{annual}\) |
|
Total cost on an annualized basis |
Aggregate electricity cost |
\(C_{el,tot}\) |
|
Sum of all electricity costs |
Costing Calculations
Total annualized cost is a simple function of the annualized capital cost and the annualized operating cost:
\[C_{annual} = f_{crf} C_{ca,tot} + C_{op,tot}\]
The total capital cost is a simple factor of the sum of the unit model capital costs:
\[C_{ca,tot} = f_{toti} C_{ca,u}\]
The total operating cost is the sum of the fixed and variable operating costs:
\[C_{op,tot} = C_{op,fix} + C_{op,var}\]
The total fixed operating cost \(C_{op,fix}\) is the sum of the maintence, labor, and chemical operating costs, \(C_{mlc}\), and the total fixed operating costs from the unit models, \(C_{fop,u}\):
\[C_{op,fix} = C_{mlc} + C_{fop,u}\]
Where the maintenance-labor-chemical operating cost \(C_{mlc}\) is defined as:
\[C_{mlc} = f_{mlc} C_{ca,tot}\]
The total variable operating cost is the sum of the total variable operating cost from the unit models, \(C_{vop,u}\) plus the sum of the flow costs, \(C_{flow,tot}\) times the plant utilization factor \(f_{util}\):
\[C_{op,var} = C_{vop,u} + f_{util} C_{flow,tot}\]
Aggregate Metrics
Built-in methods can be used to add expressions for common aggregate metrics used in technoeconomic analyses of water systems. The following methods can be used to add different metrics to the costing block:
Method |
Default Expression Name |
Description |
|---|---|---|
|
|
Adds LCOW variable and constraint |
|
|
Adds specific energy consumption variable and constraint |
|
|
Adds specific electrical carbon intensity variable and constraint |
|
|
Adds annual water production variable and constraint |
|
|
Adds flow component breakdown variable and constraint |
Each of these methods requires the user pass a volumetric flow rate \(Q\) (with units of volume per time) to be used as the basis for the calculation.
Users can optionally provide custom names for the created expression via the name keyword argument. For example, creating an expression called SEC on m.fs.costing
based on flow_rate would be:
m.fs.costing.add_specific_energy_consumption(
flow_rate,
name="SEC",
)
Levelized Cost of Water (LCOW)
For a given volumetric flow \(Q\), an expression for the LCOW, \(LCOW_{Q}\) is added by the add_LCOW method as
\[LCOW_{Q} = \frac{f_{crf} C_{ca,tot} + C_{op,tot}}{f_{util} Q}\]
In addition to creating the LCOW expression at the system level, the add_LCOW method will create the following indexed expressions
to further break down the cost components contributing to the LCOW:
Description |
Default Expression Name 1 |
Index |
Equation 2 |
|---|---|---|---|
Direct capital expenditure by flowsheet component |
|
Unit model flowsheet name 3 |
\(\cfrac{f_{crf} C_{dir,i}}{f_{util} Q}\) |
Indirect capital expenditure by flowsheet component |
|
Unit model flowsheet name |
\(\cfrac{f_{crf} C_{indir,i}}{f_{util} Q}\) |
Fixed operating expenditure by flowsheet component |
|
Unit model flowsheet name |
\(\cfrac{f_{crf} C_{fop,i}}{f_{util} Q}\) |
Variable operating expenditure by flowsheet component |
|
Unit model flowsheet name or flow name 4 |
\(\cfrac{f_{crf} C_{vop,i}}{f_{util} Q}\) |
Aggregate direct capital expenditure by unit type |
|
Unit model class name 5 |
\(\cfrac{f_{crf} \sum C_{dir,u}}{f_{util} Q}\) |
Aggregate indirect capital expenditure by unit type |
|
Unit model class name |
\(\cfrac{f_{crf} \sum C_{indir,u}}{f_{util} Q}\) |
Aggregate fixed operating expenditure by unit type |
|
Unit model class name |
\(\cfrac{f_{crf} \sum C_{fop,u}}{f_{util} Q}\) |
Aggregate variable operating expenditure by unit type |
|
Unit model class name or flow name |
\(\cfrac{f_{crf} \sum C_{vop,u}}{f_{util} Q}\) |
Note
1 The default expression names prepend the method argument name to the extended variable name; e.g., add_LCOW(flow_rate, name="MyLCOW"), will result in MyLCOW_component_direct_capex.
2 The index \(i\) refers to individual unit model instances on the flowsheet, while \(u\) refers to unit model classes.
3 The unit model flowsheet name is the name assigned to the unit model when it is added to the flowsheet (e.g., m.fs.unit1 = MyUnitModel() would have a flowsheet name of “fs.unit1”).
4 The flow name is the name used when registering the flow with the costing package (e.g., m.fs.costing.register_flow_type("foobaz", foobaz_unit_cost) would have a flow name of “foobaz”).
5 The unit model class name is the string representation of the class used to define the unit model (e.g., “ReverseOsmosis0D”, “Pump”).
Note the difference between the “component” and “aggregate” expressions: the component expressions break down costs by individual unit model instances,
while the aggregate expressions sum costs by unit model class. So, if there are multiple pumps on the flowsheet, the individual contributions
to LCOW from each pump would be available in the LCOW_component_* expressions, while the total contribution from all pumps would be available as LCOW_aggregate_* expressions.
The LCOW_component_* expressions are indexed by the string representation of the unit model flowsheet name.
The indexes for the LCOW_aggregate_* expressions are the unit model class name.
Importantly, both LCOW_component_variable_opex and LCOW_aggregate_variable_opex expressions are also indexed by flow name for registered flows.
Energy (e.g., “electricity”) and material (e.g., “naocl”, “caustic”) flows registered with the costing package will have their variable operating costs
broken out in these expressions. This allows the user to see the contribution of individual flow costs to the overall LCOW.
For an example of the breakdowns presented by each of these expressions, see the how to use WaterTAP costing guide.
Specific Energy Consumption (SEC)
For a given volumetric flow Q, an expression for the specific energy consumption, \(\text{SEC}_Q\) is added by the add_specific_energy_consumption method as
\[\text{SEC}_Q = \frac{C_{el,tot}}{Q}\]
Additionally, the specific energy consumption will be broken down by unit model. An expression is created with _component appended to the name provided by the user (or specific_energy_consumption by default).
This expression is indexed by unit model flowsheet name and is calculated as
\[\text{SEC}^{\text{component}}_{Q,i} = \frac{C_{el,i}}{Q}\]
Specific Electrical Carbon Intensity (SECI)
For a given volumetric flow Q, an expression for the specific electrical carbon intensity, \(\text{SECI}_Q\) is added by the add_specific_electrical_carbon_intensity method as
\[\text{SECI}_Q = \frac{f_{eci} C_{el,tot}}{Q}\]
Additionally, the specific electrical carbon intensity will be broken down by unit model. An expression is created with _component appended to the name provided by the user (or specific_electrical_carbon_intensity by default).
This expression is indexed by unit model flowsheet name and is calculated as
\[\text{SECI}^{\text{component}}_{Q,i} = \frac{f_{eci} C_{el,i}}{Q}\]
Annual Water Production
For a given volumetric flow Q, an expression for the annual water production, \(\text{W}^{\text{A}}_{Q}\) is added by the add_annual_water_production method as
\[\text{W}^{\text{A}}_{Q} = f_{util} Q\]
Flow Breakdowns
An additional method on the WaterTAP costing block is add_flow_component_breakdown.
This allows the user to break down the costs associated with individual registered flows for a specific flow type (e.g., electricity, chemicals) per cubic meter of product flow \(Q_p\).
For a given registered flow type \(x\) the flow component breakdown \(\text{FCB}_{u}\) by flow source \(u\) is calculated as
\[\text{FCB}_{u} = \frac{F_{x,u} M_f}{Q_p}\]
Where \(F_{x,u}\) is the flow of \(x\) from source \(u\), \(M_f\) is an optional multiplier, and \(Q_p\) is a specified volumetric flow rate. \(M_f\) must have units that, when multipled with the units for \(F_{x,u}\), result in a rate (i.e., units per time). For example, if the flow rate was electricity (units of kW), the multiplier could be a electrical carbon intensity (units of kg/kWh) and the resulting units would be kg/hr.
The method has two required arguments and three optional arguments:
flow_name(required): string for a registered flow typeflow_rate(required): flow rate of water (volumetric) to be used for normalizationname(optional): base name appended with_componentfor expression name (default is to useflow_name)period(optional): time period for normalization (default isbase_period)multiplier(optional): multiplier for the flow (default is 1.0)
To create a breakdown of costs for bazchem used per hour per cubic meter of water, the following will create
an expression m.fs.costing.bazchem_flow_component indexed to every unit that is associated with a flow of bazchem.
m.fs.costing.add_flow_component_breakdown(
"bazchem", flow_rate, name="bazchem_flow", period=pyunits.hour
)
Note
The add_flow_component_breakdown method will try to find the unit associated with each registered flow automatically. If it can’t, the logger will print a warning and the index for the unidentified flow will be the name of the flow expression.
Default Costing Methods
While the expectation is that unit models use the self-registration process noted above, for interoperability with IDAES unit models the WaterTAPCostingBlockData class defines default costing methods for IDAES unit models:
Class Documentation
WaterTAPCostingBlockData