Basic Costing Features#

The purpose of this tutorial is to introduce the basic features and usage of the REE Costing Framework in PrOMMiS. This tutorial assumes that users are familiar with basic knowledge of building models in Pyomo, IDAES, and PrOMMiS.

Introduction#

This notebook demonstrates key features of the REE Costing Framework in PrOMMiS that enable users to add costing to their PrOMMiS flowsheets. The costing leverages the Quality Guidelines for Energy Systems Studies (QGESS) methodology with many costing assumptions modified for critical minerals and rare earth element recovery systems as presented by the University of Kentucky pilot study. Users may refer to these references for further details on the costing methodology and assumptions.

The REE Costing Framework supports capital costs leveraging reference data, as well as installation, fixed operating and variable operating costs calculated based on plant inputs. The reference data lives in a built-in cost account dictionary and follows a numbering convention set by QGESS standards for cost reference data.

Capital cost calculations follow the general principle of economies of scale: as production increases, the cost per production of a particular unit operation decreases. As such, capital costs vary nonlinearly with capacity and are calculated via power law surrogate models in the costing framework. The cost of a single unit operation or cost account is denoted as the equipment cost, or the “bare erected” cost prior to installation.

Installation costs are calculated as percentage factors of the total sum of Bare Erected Costs (BEC), which is the total capital equipment cost, in the plant, and fixed operating costs are largely taken as percentage factors of the total plant cost or total revenue. Variable operating costs reflect the overall usage of consumables, such as chemicals or electricity, or the required cost of waste disposal. Plant overhead and land/leasing costs are considered variable costs within the framework.

Learning Objectives#

The tutorial will take users through the following:

  1. Importing the required tools from PrOMMiS and related repositories

  2. Adding capital costing for unit models using the cost account library

  3. Building process costs, including annualized capital and operating costs

Useful Links:

Problem Statement#

For demonstrative purposes, this tutorial will introduce costing in the context of the University of Kentucky flowsheet, shown below:

uky_flowsheet.png

This tutorial will show adding costing for the leach train, leach tanks, and process-related costs. For more information on specific unit models in PrOMMiS, please refer to the UKy Flowsheet Tutorial which demonstrates the major unit models supported in PrOMMiS. A separate tutorial demonstrates a complete example of the UKy Flowsheet with Costing.

1 Import the necessary tools#

First, import the required Python, Pyomo, IDAES, and PrOMMiS packages. These will be implemented at various stages of the tutorial.

For installation instructions, please refer to the public GitHub repository linked above.

# import pytest
import pytest

# Pyomo packages
from pyomo.environ import (
    ComponentMap,
    ConcreteModel,
    SolverFactory,
    Suffix,
    Constraint,
    Var,
    Param,
    Expression,
    units as pyunits,
    assert_optimal_termination,
    value,
)

# IDAES packages
from idaes.core import FlowsheetBlock, UnitModelBlock, UnitModelCostingBlock
from idaes.core.solvers import get_solver
from idaes.core.util.model_diagnostics import DiagnosticsToolbox

# PrOMMiS packages
from prommis.uky.costing.ree_plant_capcost import QGESSCosting, QGESSCostingData

2 Adding Capital Costing#

2.1 Define a flowsheet-level costing block#

The REE Costing Framework attaches costing variables and constraints to an existing Pyomo block. To begin, a Pyomo ConcreteModel must be created with an appropriate FlowsheetBlock. The framework is compatible with steady-state flowsheets (dynamic=False) and dynamic flowsheets (dynamic=True); however, not every supported unit model is compatible with a time index. If a time index does not exist on the main flowsheet and variable operating costs are built, a time index of [0] will be added to the flowsheet block.

The first step is to create the model and flowsheet:

# Create a Concrete Model as the top level object
m = ConcreteModel()

# Add a flowsheet object to the model
m.fs = FlowsheetBlock(dynamic=False)

Then, attach a flowsheet costing block as shown below:

m.fs.costing_1 = QGESSCosting()

The costing block serves three purposes.

First, the QGESSCosting() method imports a ready-made dictionary of currency units from IDAES supporting cost years from 1990-2023. These cost-year factors are sourced from the public source Towering Skills, and enable conversion of economic results between different reference years. Currency units may be treated as any other Pyomo UOM (units of measurement), and instantiating the QGESSCosting() class adds the currency conversions to the Pyomo UOM library. The REE Costing module appends custom year indices from public case studies that are not available in IDAES. For the purposes of this example, the default currency of USD_2021 will be used.

Second, the QGESSCosting() method supports building cost equations with the IDAES UnitModelCostingBlock() method for each unit model block. The created costing block serves as a parent block for unit model costing, and the REE Costing Framework only needs to reference a single block to build process costing. For a usage example of IDAES costing, which utilizes unit model costing in a similar fashion, see the IDAES Flowsheet Costing example.

Third, the QGESSCosting() method contains all required capital and operating cost methods. After defining the unit model costing for each block in the flowsheet, a single build_process_costs() call builds all specified cost calculations. This will be discussed further in Step 3.

2.2 Build capital costing for unit models#

Next, we need to build and attach capital costing equations for each unit model block via the IDAES UnitModelCostingBlock() method. Costing may be attached to an imported, fully-defined unit model, or a dummy block such as Pyomo’s Block() or IDAES’s UnitModelBlock(). For this example, we will use the LeachingTrain model:

# Required imports for leaching train model
from prommis.leaching.leach_train import LeachingTrain, LeachingTrainInitializer
from prommis.leaching.leach_reactions import CoalRefuseLeachingReactionParameterBlock
from prommis.properties.coal_refuse_properties import CoalRefuseParameters
from prommis.properties.sulfuric_acid_leaching_properties import (
    SulfuricAcidLeachingParameters,
)

# Leaching property and unit models
m.fs.leach_soln = SulfuricAcidLeachingParameters()
m.fs.coal = CoalRefuseParameters()
m.fs.leach_rxns = CoalRefuseLeachingReactionParameterBlock()

m.fs.leach_tanks = LeachingTrain(
    number_of_tanks=1,
    liquid_phase={
        "property_package": m.fs.leach_soln,
        "has_energy_balance": False,
        "has_pressure_balance": False,
    },
    solid_phase={
        "property_package": m.fs.coal,
        "has_energy_balance": False,
        "has_pressure_balance": False,
    },
    reaction_package=m.fs.leach_rxns,
)

# Liquid feed state
m.fs.leach_tanks.liquid_inlet.flow_vol.fix(224.3 * 1e3 * pyunits.L / pyunits.hour)
m.fs.leach_tanks.liquid_inlet.conc_mass_comp.fix(1e-10 * pyunits.mg / pyunits.L)

m.fs.leach_tanks.liquid_inlet.conc_mass_comp[0, "H"].fix(
    2 * 0.05 * 1e3 * pyunits.mg / pyunits.L
)
m.fs.leach_tanks.liquid_inlet.conc_mass_comp[0, "HSO4"].fix(
    1e-8 * pyunits.mg / pyunits.L
)
m.fs.leach_tanks.liquid_inlet.conc_mass_comp[0, "SO4"].fix(
    0.05 * 96e3 * pyunits.mg / pyunits.L
)
# Solid feed state
m.fs.leach_tanks.solid_inlet.flow_mass.fix(22.68 * 1e3 * pyunits.kg / pyunits.hour)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "inerts"].fix(
    0.6952 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Al2O3"].fix(
    0.237 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Fe2O3"].fix(
    0.0642 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "CaO"].fix(
    3.31e-3 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Sc2O3"].fix(
    2.77966e-05 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Y2O3"].fix(
    3.28653e-05 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "La2O3"].fix(
    6.77769e-05 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Ce2O3"].fix(
    0.000156161 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Pr2O3"].fix(
    1.71438e-05 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Nd2O3"].fix(
    6.76618e-05 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Sm2O3"].fix(
    1.47926e-05 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Gd2O3"].fix(
    1.0405e-05 * pyunits.kg / pyunits.kg
)
m.fs.leach_tanks.solid_inlet.mass_frac_comp[0, "Dy2O3"].fix(
    7.54827e-06 * pyunits.kg / pyunits.kg
)

m.fs.leach_tanks.volume.fix(100 * 1e3 * pyunits.gallon)

# Apply scaling
solid_scaler = m.fs.leach_tanks.mscontactor.solid.default_scaler()
solid_scaler.default_scaling_factors["flow_mass"] = 1 / (22.68 * 2 * 1e3)

liquid_scaler = m.fs.leach_tanks.mscontactor.liquid.default_scaler()
liquid_scaler.default_scaling_factors["flow_vol"] = 1 / (224.3 * 2 * 1e3)

submodel_scalers = ComponentMap()
submodel_scalers[m.fs.leach_tanks.mscontactor.liquid_inlet_state] = liquid_scaler
submodel_scalers[m.fs.leach_tanks.mscontactor.liquid] = liquid_scaler
submodel_scalers[m.fs.leach_tanks.mscontactor.solid_inlet_state] = solid_scaler
submodel_scalers[m.fs.leach_tanks.mscontactor.solid] = solid_scaler

scaler_obj = m.fs.leach_tanks.default_scaler()
scaler_obj.scale_model(m.fs.leach_tanks, submodel_scalers=submodel_scalers)

# Diagnostics checks on model structure
dt = DiagnosticsToolbox(m)
dt.assert_no_structural_warnings()

# Create a scaled version of the model to solve

# Initialize model
initializer = LeachingTrainInitializer()
initializer.initialize(m.fs.leach_tanks)

solver = SolverFactory("ipopt_v2")
solver.solve(m, tee=True)

# Diagnostics checks on model results
dt.assert_no_numerical_warnings()
WARNING (W1002): Setting Var
'fs.leach_tanks.mscontactor.solid[0.0,1].conversion_comp[Y2O3]' to a numeric
value `-2.3391490520191677e-12` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach_tanks.mscontactor.solid[0.0,1].conversion_comp[Nd2O3]' to a numeric
value `-7.778855753596789e-13` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
2026-04-17 13:20:16 [INFO] idaes.init.fs.leach_tanks.mscontactor: Stream Initialization Completed.
2026-04-17 13:20:16 [INFO] idaes.init.fs.leach_tanks.mscontactor: Initialization Completed, optimal - <undefined>
Ipopt 3.13.2: 

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit http://projects.coin-or.org/Ipopt

This version of Ipopt was compiled from source code available at
    https://github.com/IDAES/Ipopt as part of the Institute for the Design of
    Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
    Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.

This version of Ipopt was compiled using HSL, a collection of Fortran codes
    for large-scale scientific computation.  All technical papers, sales and
    publicity material resulting from use of the HSL codes within IPOPT must
    contain the following acknowledgement:
        HSL, a collection of Fortran codes for large-scale scientific
        computation. See http://www.hsl.rl.ac.uk.
******************************************************************************

This is Ipopt version 3.13.2, running with linear solver ma27.

Number of nonzeros in equality constraint Jacobian...:      224
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:       95

Total number of variables............................:       57
                     variables with only lower bounds:       18
                variables with lower and upper bounds:       25
                     variables with only upper bounds:        0
Total number of equality constraints.................:       57
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  0.0000000e+00 5.00e-03 1.00e+00  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  0.0000000e+00 5.00e-05 2.25e-01  -1.0 1.00e-02    -  9.90e-01 9.90e-01h  1
   2  0.0000000e+00 4.95e-07 1.98e-01  -1.0 1.00e-04    -  9.90e-01 9.90e-01h  1
   3  0.0000000e+00 1.07e-14 9.99e+00  -1.0 9.90e-07    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 3

                                   (scaled)                 (unscaled)
Objective...............:   0.0000000000000000e+00    0.0000000000000000e+00
Dual infeasibility......:   0.0000000000000000e+00    0.0000000000000000e+00
Constraint violation....:   1.0658141036401503e-14    1.0658141036401503e-14
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   1.0658141036401503e-14    1.0658141036401503e-14


Number of objective function evaluations             = 4
Number of objective gradient evaluations             = 4
Number of equality constraint evaluations            = 4
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 4
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 3
Total CPU secs in IPOPT (w/o function evaluations)   =      0.000
Total CPU secs in NLP function evaluations           =      0.000

EXIT: Optimal Solution Found.
WARNING: model contains export suffix 'scaling_factor' that contains 37
component keys that are not exported as part of the NL file.  Skipping.

The model is solved prior to adding costing; this is a good modeling strategy to ensure that the model is structurally and numerically sound prior to adding costing.

The UnitModelCostingBlock requires a scaling parameter for the power law model; for the leach tanks, the capital cost scales with tank volume. Per the University of Kentucky pilot cost account dictionary, the leach tank cost_accounts name is “4.2”. If multiple cost accounts support the same scaling parameter, the account names may be added to cost_accounts as a list and the final costing block will be indexed by that list.

In the method below, scaled_param is the parameter or variable that capital costs scale with, source refers to reference data source which is “1” for the University of Kentucky public case study, n_equip is the number of parallel units, and CE_index_year is the cost year. The argument scale_down_parallel_equip allows users to specify how trains of parallel units are treated. Setting this argument to False assumes that the scaling parameter is the capacity of each parallel unit, e.g. each leach tank in the train is identical and has a volume equal to m.fs.leach.volume. Setting this argument to True assumes that the scaling parameter is the total capacity for the entire train, e.g. each leach tank in the train is identical and the total volume of all tanks in the train is equal to m.fs.leach.volume. For a single unit, the assumptions lead to the same result.

Generally, scaling down unit size for a parallel train will reduce the total cost of the train; however, in practice units often come in a predetermined size or specification. We will assume in this example that leach tanks come in a standard size and we cannot scale down multiple parallel tanks.

Capital costing is added below, assuming the train has 3 leach tanks where each tank equals the specification set by m.fs.leach.volume:

m.fs.leach_tanks.costing = UnitModelCostingBlock(
    flowsheet_costing_block=m.fs.costing_1,  # this is the flowsheet costing block
    costing_method=QGESSCostingData.get_REE_costing,  # REE capital costing method
    costing_method_arguments={
        "cost_accounts": [
            "4.2",
        ],  # leach tank account
        "scaled_param": m.fs.leach_tanks.volume[0, 1],  # scaling parameter
        "source": 1,  # tells framework to use University of Kentucky cost accounts
        "n_equip": 3,  # number of leach tanks in train
        "scale_down_parallel_equip": False,  # tanks are duplicates of the same size as the original train
        "CE_index_year": "2021",  # cost year, all variables will be returned in $ million 2021
    },
)

Displaying the costing block shows the new variables and constraints that we have just added:

m.fs.leach_tanks.costing.display()
Block fs.leach_tanks.costing

  Variables:
    bare_erected_cost : Scaled bare erected cost
        Size=1, Index={4.2}, Units=MUSD_2021
        Key : Lower : Value   : Upper : Fixed : Stale : Domain
        4.2 :     0 : 123.653 :  None : False : False :  Reals

  Objectives:
    None

  Constraints:
    bare_erected_cost_eq : Size=1
        Key : Lower : Body                : Upper
        4.2 :   0.0 : 0.12335124668756682 :   0.0

Note that the results above reflect the initial model state, not the solved state. The BEC of the leach tanks shown above is 123.653 million USD, which is much too high. The “body” of the added constraint is nonzero, which indicates that the constraint has not been solved. Solving the model again, we obtain the correct results:

solver.solve(m, tee=True)
Ipopt 3.13.2: 

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit http://projects.coin-or.org/Ipopt

This version of Ipopt was compiled from source code available at
    https://github.com/IDAES/Ipopt as part of the Institute for the Design of
    Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
    Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.

This version of Ipopt was compiled using HSL, a collection of Fortran codes
    for large-scale scientific computation.  All technical papers, sales and
    publicity material resulting from use of the HSL codes within IPOPT must
    contain the following acknowledgement:
        HSL, a collection of Fortran codes for large-scale scientific
        computation. See http://www.hsl.rl.ac.uk.
******************************************************************************

This is Ipopt version 3.13.2, running with linear solver ma27.

Number of nonzeros in equality constraint Jacobian...:      224
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:       95

Total number of variables............................:       57
                     variables with only lower bounds:       18
                variables with lower and upper bounds:       25
                     variables with only upper bounds:        0
Total number of equality constraints.................:       57
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  0.0000000e+00 5.00e-03 1.00e+00  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  0.0000000e+00 5.00e-05 2.25e-01  -1.0 1.00e-02    -  9.90e-01 9.90e-01h  1
   2  0.0000000e+00 4.95e-07 1.98e-01  -1.0 1.00e-04    -  9.90e-01 9.90e-01h  1
   3  0.0000000e+00 5.29e-15 9.99e+00  -1.0 9.90e-07    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 3

                                   (scaled)                 (unscaled)
Objective...............:   0.0000000000000000e+00    0.0000000000000000e+00
Dual infeasibility......:   0.0000000000000000e+00    0.0000000000000000e+00
Constraint violation....:   5.2877003327323470e-15    5.2877003327323470e-15
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   5.2877003327323470e-15    5.2877003327323470e-15


Number of objective function evaluations             = 4
Number of objective gradient evaluations             = 4
Number of equality constraint evaluations            = 4
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 4
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 3
Total CPU secs in IPOPT (w/o function evaluations)   =      0.000
Total CPU secs in NLP function evaluations           =      0.000

EXIT: Optimal Solution Found.
{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 0, 'Number of constraints': nan, 'Number of variables': nan, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Termination condition': 'optimal', 'Termination message': 'TerminationCondition.convergenceCriteriaSatisfied'}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}
m.fs.leach_tanks.costing.display()
assert m.fs.leach_tanks.costing.bare_erected_cost["4.2"].value == pytest.approx(
    0.301753, rel=1e-4
)
Block fs.leach_tanks.costing

  Variables:
    bare_erected_cost : Scaled bare erected cost
        Size=1, Index={4.2}, Units=MUSD_2021
        Key : Lower : Value              : Upper : Fixed : Stale : Domain
        4.2 :     0 : 0.3017533124331849 :  None : False : False :  Reals

  Objectives:
    None

  Constraints:
    bare_erected_cost_eq : Size=1
        Key : Lower : Body : Upper
        4.2 :   0.0 :  0.0 :   0.0

The actual BEC of the leach tanks is $301,753; the body of the constraint is zero indicating that the constraint is now solved. It is often the case that process models are pre-solved prior to adding costing, and it is important to ensure that the model is solved again after any costing calculations are added.

2.3 Add capital cost for a second unit model#

Inline Exercise: Next, let us add costing for the process pumps associated with the leaching tanks. The call will be the same as the method above, except that the cost account to use is "4.4" and the scaling parameter is the volumetric flow of leach liquid. Note that the pump costing depends on the stream properties, and not on a property of the pump itself such as power, so we don't actually need to add an IDAES Pump model here to add the costing. We just need the correct reference to the feed flow. Use the cell below to add the costing.
# Create a new UnitModelBlock called leach_pumps
m.fs.leach_pumps = UnitModelBlock()

# Add costing for the pumps scaled by m.fs.leach_liquid_feed.flow_vol[0]
m.fs.leach_pumps.costing = UnitModelCostingBlock(
    flowsheet_costing_block=m.fs.costing_1,
    costing_method=QGESSCostingData.get_REE_costing,
    costing_method_arguments={
        "cost_accounts": [
            "4.4",
        ],
        "scaled_param": m.fs.leach_tanks.liquid_inlet.flow_vol[0],
        "source": 1,
        "n_equip": 3,
        "scale_down_parallel_equip": False,
        "CE_index_year": "2021",
    },
)

# Solve the model and display results of the costing block
solver.solve(m, tee=False)
m.fs.leach_pumps.costing.display()
assert m.fs.leach_pumps.costing.bare_erected_cost["4.4"].value == pytest.approx(
    0.234096, rel=1e-4
)
Block fs.leach_pumps.costing

  Variables:
    bare_erected_cost : Scaled bare erected cost
        Size=1, Index={4.4}, Units=MUSD_2021
        Key : Lower : Value               : Upper : Fixed : Stale : Domain
        4.4 :     0 : 0.23409633101955057 :  None : False : False :  Reals

  Objectives:
    None

  Constraints:
    bare_erected_cost_eq : Size=1
        Key : Lower : Body : Upper
        4.4 :   0.0 :  0.0 :   0.0

2.4 Add capital costing using a custom cost account#

Now, let us consider adding capital costing for a custom account that doesn’t currently exist in PrOMMiS. Suppose we have regressed costing data offline for a generic additional component measured in units of length, and have obtained the following reference data:

additional_costing_params = {
    "1": {
        "newaccount": {
            "Account Name": "Leach train additional component",
            "BEC": 100000.0,  # equipment purchase cost prior to installation
            "BEC_units": "$2016",  # currency units associated with the BEC value
            "Exponent": 1.25,  # scaling exponent - New cost = Old cost * (new RP / old RP)**Exponent
            "Process Parameter": "Length of component",  # reference parameter
            "RP Value": 500.0,  # reference parameter value
            "Units": "m",  # physical units associated with the reference parameter
        },
    },
}

The UnitModelCostingBlock call supports an additional entry in “costing_method_arguments” named “additional_costing_params”. The passed object should be a dictionary in the form of the additional_costing_params object defined above, and the framework will add the new entry to the cost account dictionary in the session memory. Note that the dictionary file is not edited, rather the dictionary object in the memory is temporarily appended with the new entry. If the additional entry matches an existing account, an error will be thrown; to override this and instead use the new entry, pass another argument "use_additional_costing_params": True.

Inline Exercise: Use the cell below to add costing for the custom account, passing the object additional_costing_params to define the cost account. The name of the cost account is "newaccount". Because the unit model doesn't exist, it needs to be added with the correct variable (some of this has been done already below).
# Create a new UnitModelBlock called leach_additional_component
m.fs.leach_additional_component = UnitModelBlock()

# Add a new variable for the additional component scaling parameter
# Initialize the value to 1000 and the units to pyunits.m
# Remember to fix the variable after creating it
m.fs.leach_additional_component.length = Var(initialize=1000, units=pyunits.m)
m.fs.leach_additional_component.length.fix()

# Add costing for the pumps scaled by m.fs.leach_additional_component.length
# in the costing_method_arguments, set "additional_costing_params": additional_costing_params
m.fs.leach_additional_component.costing = UnitModelCostingBlock(
    flowsheet_costing_block=m.fs.costing_1,
    costing_method=QGESSCostingData.get_REE_costing,
    costing_method_arguments={
        "cost_accounts": [
            "newaccount",
        ],
        "scaled_param": m.fs.leach_additional_component.length,
        "source": 1,
        "n_equip": 1,
        "scale_down_parallel_equip": False,
        "CE_index_year": "2021",
        "additional_costing_params": additional_costing_params,
    },
)

# Solve the model and display results of the costing block
solver.solve(m, tee=False)
m.fs.leach_additional_component.costing.display()
assert m.fs.leach_additional_component.costing.bare_erected_cost[
    "newaccount"
].value == pytest.approx(0.310858, rel=1e-4)
Block fs.leach_additional_component.costing

  Variables:
    bare_erected_cost : Scaled bare erected cost
        Size=1, Index={newaccount}, Units=MUSD_2021
        Key        : Lower : Value              : Upper : Fixed : Stale : Domain
        newaccount :     0 : 0.3108579056385181 :  None : False : False :  Reals

  Objectives:
    None

  Constraints:
    bare_erected_cost_eq : Size=1
        Key        : Lower : Body : Upper
        newaccount :   0.0 :  0.0 :   0.0

3 Building Process Costs#

When the flowsheet contains all desired unit models with capital cost variables and constraints, we can call the main method of the costing module to build plant-wide costing for the flowsheet. This is conveniently done in a single method call, build_process_costs, that takes a number of arguments related to installation and operating costs.

For demonstrative purposes, the UKy Flowsheet will be used for plantwide costing.

The costing data dictionary contains information from the University of Kentucky pilot study “Pilot-Scale Testing of an Integrated Circuit for the Extraction of Rare Earth Minerals and Elements from Coal and Coal Byproducts Using Advanced Separation Technologies” (2021) and from the NETL Quality Guidelines for Energy Systems Studies (Feb 2021). Specifically it includes scaling exponents, valid ranges for the scaled parameter, and units for those ranges. It is important to note the units only apply to the ranges and are not necessarily the units that the reference parameter value will be given in.. It includes the total plant cost (TPC), reference parameter value, and units for that value.

This dictionary is nested with the following structure: source –> account –> property name –> property values. PrOMMiS currently supports a two sources, a large number of equipment accounts from the UKy flowsheet (“1”) and additional accounts for magnet recycling (“2”). The cost account dictionary may be imported from the following path:

from prommis.uky.costing.costing_dictionaries import load_REE_costing_dictionary

3.1 Build Process Costs Without Operating Costs#

The method build_process_costs automatically calculates the total BEC, and also calculates the total installation cost and fixed operating costs as percentages of various capital cost components. For this example, we will set fixed_OM = False so we can focus on only the equipment and installation costs.

Building the equipment-only costs is done in a single method call:

# Unit model
m.fs.unit = UnitModelBlock()
m.fs.unit.flow_vol = Var(initialize=100, units=pyunits.gal / pyunits.min)
m.fs.unit.flow_vol.fix()

# Add costing for unit - use 1 piece equip, no scaling parallel equip, cost year 2021
m.fs.unit.costing = UnitModelCostingBlock(
    flowsheet_costing_block=m.fs.costing_1,
    costing_method=QGESSCostingData.get_REE_costing,
    costing_method_arguments={
        "cost_accounts": [
            "4.4",
        ],
        "scaled_param": m.fs.unit.flow_vol,
        "source": 1,
        "n_equip": 1,
        "scale_down_parallel_equip": False,
        "CE_index_year": "2021",
    },
)

# Build the process cost calculations
m.fs.costing_1.build_process_costs(fixed_OM=False)
solver.solve(m, tee=False)
# m.fs.costing_1.display()  # uncomment to display
assert m.fs.costing_1.total_BEC.value == pytest.approx(0.879287, rel=1e-4)
assert m.fs.costing_1.total_installation_cost.value == pytest.approx(1.73220, rel=1e-4)
assert m.fs.costing_1.total_plant_cost.value == pytest.approx(2.61148, rel=1e-4)

As shown in the displayed block above, the build method created a large number of attributes, mainly factors related to the installation costs. The default cost units are millions of dollars using the reference year 2021.

The method build_process_costs supports a few capital cost-related arguments:

  • total_purchase_cost, a fixed value of the total BEC that is used instead of summing the unit model blocks BEC variables. If not set, the default is None and the framework will calculate the total BEC from the unit models in the flowsheet.

  • Lang_factor, a factor that determines the total installation cost as total_installation_cost = total_BEC * (Lang_factor - 1). If not set, the default is None and the framework uses the percentage factor arguments.

  • If Lang_factor is not set, there are a number of arguments breaking down the installation factor into plant components calculated as percentages of the total BEC. Effectively, the factors yield total_installation_cost = total_BEC * sum(percentage_factors)/100.

In the exercise below, a Lang factor of 2.97 is used per the University of Kentucky pilot study “Pilot-Scale Testing of an Integrated Circuit for the Extraction of Rare Earth Minerals and Elements from Coal and Coal Byproducts Using Advanced Separation Technologies” (2021).

Inline Exercise: The framework only builds what it needs to. For example, if we choose to use a Lang factor instead of using the percentage factors, only the relevant attributes will be built. Use the cell below to build costing (a new model object has been provided, as building costs for the same costing block twice is not allowed).
# Attach a new costing "costing_2" block to the flowsheet for this example
m.fs.costing_2 = QGESSCosting()

# Create a new UnitModelBlock "unit2" and add a Var flow_vol with value 100 and units gal/min
# Remember to fix the variable
m.fs.unit2 = UnitModelBlock()
m.fs.unit2.flow_vol = Var(initialize=100, units=pyunits.gal / pyunits.min)
m.fs.unit2.flow_vol.fix()

# Add costing for unit - use 1 piece equip, no scaling parallel equip, cost year 2021
m.fs.unit2.costing = UnitModelCostingBlock(
    flowsheet_costing_block=m.fs.costing_2,
    costing_method=QGESSCostingData.get_REE_costing,
    costing_method_arguments={
        "cost_accounts": [
            "4.4",
        ],
        "scaled_param": m.fs.unit2.flow_vol,
        "source": 1,
        "n_equip": 1,
        "scale_down_parallel_equip": False,
        "CE_index_year": "2021",
    },
)

# Build process costs using a Lang factor of 2.97
# We're still not including O&M costs, so fixed_OM should be False
m.fs.costing_2.build_process_costs(
    Lang_factor=2.97,
    fixed_OM=False,
)

# Solve and display results of the costing block
solver.solve(m, tee=False)
# m.fs.costing_2.display()  # uncomment to display
assert m.fs.costing_2.total_BEC.value == pytest.approx(0.0325796, rel=1e-4)
assert m.fs.costing_2.total_installation_cost.value == pytest.approx(
    0.0641819, rel=1e-4
)
assert m.fs.costing_2.total_plant_cost.value == pytest.approx(0.0967615, rel=1e-4)

The model is now considerably smaller and easier to navigate. Unless installation percentage factors are specified for case study being modeled, using a well-chosen or calculated Lang factor is recommended to reduce the size of the model.

3.2 Building Process Costs With Fixed Operating & Maintenance (O&M) Costs#

If the appropriate arguments are set, the build call will automatically create variables and constraints for fixed operating costs.

Fixed costs are calculated as percentages of various capital cost components; the percentages are constant values implemented by the University of Kentucky case study and cannot be changed by the user. The fixed operating cost components are:

  • annual_labor = operating_labor + technical_labor = labor_rate * operators_per_shift * shifts_per_day * operating_days_per_year * (1 + labor_burden)

  • maintenance_and_materials = 2% of TPC (total plant cost)

  • quality_assurance_and_control = 10% of operating labor

  • sales_patenting_and_research = 0.5% of total sales revenue

  • admin_and_support_labor = 20% of direct (operating) labor

  • property_taxes_and_insurance = 1% of TPC

  • membrane_materials = calculated by external WaterTAP package; see the tutorial on Advanced Costing Features for usage.

The total sales revenue is calculated from pure product and mixed product sale price dictionaries, which are required arguments to calculate fixed O&M costs. A built-in sales price dictionary provides per kg revenue for a list of potential critical mineral or REE oxide products. Mixed products assume a price realization factor which may be set by the user; for example, setting mixed_product_sale_price_realization_factor = 0.65 tells the framework that elements or oxides in the mixed basket are worth 65% of their pure component sale price. If not set, the framework assumes a 65% price realization for mixed products.

Inline Exercise: Use the cell below to build process costs with fixed O&M costs. The method should set the fixed O&M flag to True, and set the appropriate product dictionaries. The product rates are provided below; note that the dictionary entries need to have Pyomo unit containers, e.g. "component": 1 * pyunits.kg/pyunits.hr. Typically, the product rates would be obtained from a coupled flowsheet model.

For convenience, we can set the total_purchase_cost to 0.0325796 (this will be interpreted as the default cost units, MUSD_2021) so that we do not have to add a dummy unit model. Similarly, we’ll use a Lang factor of 2.97 so the model does not need to build every installation component.

# Attach a new costing block "costing_3" to the flowsheet for this example
m.fs.costing_3 = QGESSCosting()

# Pass appropriate arguments to calculate fixed O&M costs
# Create a pure_products_output_rates dictionary with
#     "SC2O3": 1.9 kg/hr,
#     "Dy2O3": 0.4 kg/hr
#     "Gd2O3": 0.5 kg/hr
# Create a mixed_product_output_rates dictionary with
#     "Sc2O3": 0.00143 kg/hr
#     "Y2O3": 0.05418 kg/hr
#     "La2O3": 0.13770 kg/hr
#     "CeO2": 0.37383 kg/hr
#     "Pr6O11": 0.03941 kg/hr
#     "Nd2O3": 0.17289 kg/hr
#     "Sm2O3": 0.02358 kg/hr
#     "Eu2O3": 0.00199 kg/hr
#     "Tb4O7": 0.00801 kg/hr
#     "Tm2O3": 0.00130 kg/hr
#     "Yb2O3": 0.00373 kg/hr
#     "Lu2O3": 0.00105 kg/hr

pure_product_output_rates = {
    "Sc2O3": 1.9 * pyunits.kg / pyunits.hr,
    "Dy2O3": 0.4 * pyunits.kg / pyunits.hr,
    "Gd2O3": 0.5 * pyunits.kg / pyunits.hr,
}

mixed_product_output_rates = {
    "Sc2O3": 0.00143 * pyunits.kg / pyunits.hr,
    "Y2O3": 0.05418 * pyunits.kg / pyunits.hr,
    "La2O3": 0.13770 * pyunits.kg / pyunits.hr,
    "CeO2": 0.37383 * pyunits.kg / pyunits.hr,
    "Pr6O11": 0.03941 * pyunits.kg / pyunits.hr,
    "Nd2O3": 0.17289 * pyunits.kg / pyunits.hr,
    "Sm2O3": 0.02358 * pyunits.kg / pyunits.hr,
    "Eu2O3": 0.00199 * pyunits.kg / pyunits.hr,
    "Tb4O7": 0.00801 * pyunits.kg / pyunits.hr,
    "Tm2O3": 0.00130 * pyunits.kg / pyunits.hr,
    "Yb2O3": 0.00373 * pyunits.kg / pyunits.hr,
    "Lu2O3": 0.00105 * pyunits.kg / pyunits.hr,
}


# Build process costs
# Use a total purchase cost of 0.0325796 and a Lang factor of 2.97 to simplify the model
m.fs.costing_3.build_process_costs(
    total_purchase_cost=0.0325796,
    Lang_factor=2.97,
    fixed_OM=True,
    pure_product_output_rates=pure_product_output_rates,
    mixed_product_output_rates=mixed_product_output_rates,
)

# Solve and display results
solver.solve(m, tee=False)
# m.fs.costing_3.display()  # uncomment to display
assert m.fs.costing_3.total_BEC.value == pytest.approx(0.0325796, rel=1e-4)
assert m.fs.costing_3.total_installation_cost.value == pytest.approx(
    0.0641819, rel=1e-4
)
assert m.fs.costing_3.total_plant_cost.value == pytest.approx(0.0967615, rel=1e-4)
assert m.fs.costing_3.total_fixed_OM_cost.value == pytest.approx(5.33847, rel=1e-4)
assert m.fs.costing_3.total_sales_revenue.value == pytest.approx(32.1231, rel=1e-4)

Triggering the fixed O&M cost calculations created some new variables and constraints and added them to the model. Each fixed cost component is calculated and reported separately, as well as the total fixed cost, total revenue, and the total fixed cost of membrane materials from WaterTAP models. Note how fixed costs such as fixed_operating_costs and total_fixed_OM_cost have units of cost per year, whereas capital costs have units of absolute cost.

3.3 Building Process Costs With Fixed and Variable Operating & Maintenance (O&M) Costs#

If the appropriate arguments are set, the build call will automatically create variables and constraints for variable operating costs as well. If calculating variable O&M costs, fixed O&M costs must also be calculated to create required variables and parameters.

Variable costs are calculated from resources rates and prices; the costing framework contains some pre-built resource prices, and users may pass their own or temporarily overwrite pre-built price values. The variable operating cost components are:

  • power_requirements = default $0.07 per kWh at 85% efficiency

  • waste_disposal = solid, precipitate, and dust/volatiles with default disposal costs

  • chemicals_cost = water, organics, reagent, fuel, etc at default or user-set prices

  • land_cost = leasing cost per year

  • plant overhead = 20% of (total fixed O&M + power_requirements + waste_disposal + chemicals_cost + land_cost)

The default costs for power, water, common REE processing chemicals and waste dispoal, as well as sale prices for saleable products, are located in the costing framework itself. Users must set three arguments on the build method: resources, a list of strings corresponding to the variable cost names; rates, a list of model variables corresponding to the resource flows; and prices, an optional argument to add prices for new resources or overwrite existing prices. To use the default prices, the resource name must match the names in the default price dictionary. The rates do not need to have the same units as the default dictionary, but they do need to be compatible, e.g. if using the default cost for “diesel”, the rate object may have any units of volume per time.

Inline Exercise: Use the cell below to build process costs with both fixed and variable O&M costs. The method should set the fixed O&M and variable O&M flags to True, set the appropriate product dictionaries, and set the resource name and rate.
# Attach a new costing block "costing_4" to the flowsheet for this example
m.fs.costing_4 = QGESSCosting()

# Set the water rate to 1000 gal/hr, remember to fix the variable after creating it
# The variable should be indexed by the time_set
# If the flowsheet block is not dynamic, indexing by [0] is sufficient
m.fs.water = Var([0], initialize=1000, units=pyunits.gallon / pyunits.hr)
m.fs.water.fix()

# Set the chemicals rate to 20 gal/hr, remember to fix the variable after creating it
# The variable should be indexed by the time_set
# If the flowsheet block is not dynamic, indexing by [0] is sufficient
m.fs.chemicals = Var([0], initialize=20, units=pyunits.gallon / pyunits.hr)
m.fs.chemicals.fix()

# Build process costs with fixed and variable O&M set to True
# As before, use a total purchase cost of 0.0325796, a Lang Factor of 2.97, and the product dictionaries from earlier
# To call the variable cost method, set variable_OM to True
# The resource is named "water" and the rate is the variable m.fs.water; they must be passed as lists
# Resource "water" exists in the built-in dictionary as 1.90e-3 * 1e-6 * CE_index_units / pyunits.gallon, or $0.0019/gal in the cost year
# Resource "chemicals" does not have a built-in price and needs a user-set price
# Pass a dictionary to the argument prices where "chemcials" has a price of $1/gal in 2021 USD
m.fs.costing_4.build_process_costs(
    total_purchase_cost=0.0325796,
    Lang_factor=2.97,
    fixed_OM=True,
    pure_product_output_rates=pure_product_output_rates,
    mixed_product_output_rates=mixed_product_output_rates,
    variable_OM=True,
    resources=[
        "water",
        "chemicals",
    ],
    rates=[m.fs.water, m.fs.chemicals],
    prices={"chemicals": 1.00 * pyunits.USD_2021 / pyunits.gallon},
)

# solve and display results
solver.solve(m, tee=False)
# m.fs.costing_4.display()  # uncomment to display
assert m.fs.costing_4.total_BEC.value == pytest.approx(0.0325796, rel=1e-4)
assert m.fs.costing_4.total_installation_cost.value == pytest.approx(
    0.0641819, rel=1e-4
)
assert m.fs.costing_4.total_plant_cost.value == pytest.approx(0.0967615, rel=1e-4)
assert m.fs.costing_4.total_fixed_OM_cost.value == pytest.approx(5.33847, rel=1e-4)
assert m.fs.costing_4.total_sales_revenue.value == pytest.approx(32.1231, rel=1e-4)
assert m.fs.costing_4.total_variable_OM_cost[0].value == pytest.approx(
    1.26010, rel=1e-4
)
assert m.fs.costing_4.plant_overhead_cost[0].value == pytest.approx(1.06769, rel=1e-4)

The variable O&M method created a large group of new variables and constraints for variable cost components. Scrolling through the list, one of the new variables is called variable_operating_costs. This is an indexed variable containing specific costs for the resources specified in the resources argument, which is the case above is “water” and “chemicals”. Note how variable costs such as variable_operating_costs and total_variable_OM_cost have units of cost per year, whereas capital costs have units of absolute cost.

3.4 Optional Arguments#

If both fixed and variable O&M costs are calculated, the framework supports a few additional objects based on the method arguments. Setting feed_input in units of mass per time adds a line to the report() method for the total annual operating cost per mass feed. Setting recovery_rate_per_year enables calculation of the overall recovery cost per year, created as an Expression named cost_of_recovery. Setting transport_cost_per_ton_product in units of cost per ton tells the framework to calculate a transport cost based on the recovery rate.

Further, users may set their own additional chemicals or waste costs which are added to the total resource costs. The passed expressions are considered separately from any costs defined in variable_operating_costs, and users may define chemical and waste costs in one or both ways. Users may set a land_cost, which will be added to the variable O&M cost calculations. Finally, all three cost types (capital, fixed O&M, variable O&M) support additional costs defined as “other” costs. After the build method is called, users may use these variable to define custom costs.

The code below demonstrates all supported arguments for capital, installation, fixed O&M, variable O&M, and cost of recovery calculations:

# Attach a new costing block "costing_5" to the flowsheet for this example
m.fs.costing_5 = QGESSCosting()

# Set a custom cost year
CE_index_year = "UKy_2019"

# Operation parameters to use later
hours_per_shift = 8
shifts_per_day = 3
operating_days_per_year = 336

m.fs.annual_operating_hours = Param(
    initialize=hours_per_shift * shifts_per_day * operating_days_per_year,
    mutable=True,
    units=pyunits.hours / pyunits.a,
)

# Define the feed input rate
m.fs.feed_input = Var(initialize=500, units=pyunits.ton / pyunits.hr)
m.fs.feed_input.fix()

# Define the recovery rate
m.fs.recovery_rate_per_year = Var(
    initialize=39.3
    * pyunits.kg
    / pyunits.hr
    * 0.8025  # TREO (total rare earth oxide), 80.25% REE in REO
    * m.fs.annual_operating_hours,
    units=pyunits.kg / pyunits.yr,
)
m.fs.recovery_rate_per_year.fix()

# Define transport cost per production
m.fs.transport_cost_per_ton_product = Var(
    initialize=10, units=pyunits.USD_2021 / pyunits.ton
)
m.fs.transport_cost_per_ton_product.fix()

# The land cost is the lease cost, or refining cost of REO produced
m.fs.land_cost = Expression(
    expr=0.303736
    * 1e-6
    * getattr(pyunits, "MUSD_" + CE_index_year)
    / pyunits.ton  # land cost in MUSD/ton
    * pyunits.convert(
        m.fs.feed_input, to_units=pyunits.ton / pyunits.hr
    )  # feed input in ton/hr
    * m.fs.annual_operating_hours
    * pyunits.a  # operation time in hr per year
)

# Define resources
reagent_costs = (
    (  # all USD/year
        302962  # component 1
        + 0  # component 2
        + 5767543  # component 3
        + 199053595  # component 4
        + 152303329  # Rcomponent 5
        + 43702016  # component 6
        + 7207168  # Scomponent 7
        + 1233763  # component 8
        + 18684816  # component 9
    )
    * pyunits.kg
    / pyunits.a
)

m.fs.reagents = Var(
    m.fs.time,
    initialize=reagent_costs / (m.fs.annual_operating_hours),
    units=pyunits.kg / pyunits.hr,
)
m.fs.reagents.fix()

# Additional chemicals cost
m.fs.additional_chemicals_cost = Expression(expr=0.01 * m.fs.reagents[0])

# Waste costs
m.fs.solid_waste = Var(m.fs.time, initialize=464, units=pyunits.ton / pyunits.hr)
m.fs.solid_waste.fix()

m.fs.precipitate = Var(m.fs.time, initialize=30.5, units=pyunits.ton / pyunits.hr)
m.fs.precipitate.fix()

m.fs.dust_and_volatiles = Var(m.fs.time, initialize=5, units=pyunits.ton / pyunits.hr)
m.fs.dust_and_volatiles.fix()

# Additional waste cost
m.fs.additional_waste_cost = Expression(expr=0.01 * m.fs.reagents[0])

# Power
m.fs.power = Var(m.fs.time, initialize=14716, units=pyunits.hp)
m.fs.power.fix()

resources = [
    "reagents",
    "nonhazardous_solid_waste",
    "nonhazardous_precipitate_waste",
    "dust_and_volatiles",
    "power",
]

rates = [
    m.fs.reagents,
    m.fs.solid_waste,
    m.fs.precipitate,
    m.fs.dust_and_volatiles,
    m.fs.power,
]

# Define product flowrates
pure_product_output_rates = {
    "Sc2O3": 1.9 * pyunits.kg / pyunits.hr,
    "Dy2O3": 0.4 * pyunits.kg / pyunits.hr,
    "Gd2O3": 0.5 * pyunits.kg / pyunits.hr,
}
mixed_product_output_rates = {
    "Sc2O3": 0.00143 * pyunits.kg / pyunits.hr,
    "Y2O3": 0.05418 * pyunits.kg / pyunits.hr,
    "La2O3": 0.13770 * pyunits.kg / pyunits.hr,
    "CeO2": 0.37383 * pyunits.kg / pyunits.hr,
    "Pr6O11": 0.03941 * pyunits.kg / pyunits.hr,
    "Nd2O3": 0.17289 * pyunits.kg / pyunits.hr,
    "Sm2O3": 0.02358 * pyunits.kg / pyunits.hr,
    "Eu2O3": 0.00199 * pyunits.kg / pyunits.hr,
    "Gd2O3": 0.00000 * pyunits.kg / pyunits.hr,
    "Tb4O7": 0.00801 * pyunits.kg / pyunits.hr,
    "Dy2O3": 0.00000 * pyunits.kg / pyunits.hr,
    "Ho2O3": 0.00000 * pyunits.kg / pyunits.hr,
    "Er2O3": 0.00000 * pyunits.kg / pyunits.hr,
    "Tm2O3": 0.00130 * pyunits.kg / pyunits.hr,
    "Yb2O3": 0.00373 * pyunits.kg / pyunits.hr,
    "Lu2O3": 0.00105 * pyunits.kg / pyunits.hr,
}

m.fs.costing_5.build_process_costs(
    total_purchase_cost=44.308,  # use this instead of unit model blocks
    # arguments related to installation costs
    piping_materials_and_labor_percentage=20,
    electrical_materials_and_labor_percentage=20,
    instrumentation_percentage=8,
    plants_services_percentage=10,
    process_buildings_percentage=40,
    auxiliary_buildings_percentage=15,
    site_improvements_percentage=10,
    equipment_installation_percentage=17,
    field_expenses_percentage=12,
    project_management_and_construction_percentage=30,
    process_contingency_percentage=15,
    # argument related to Fixed O&M costs
    fixed_OM=True,
    labor_types=[
        "skilled",
        "unskilled",
        "supervisor",
        "maintenance",
        "technician",
        "engineer",
    ],  # supported types of plant workers
    labor_rate=[
        24.98,
        19.08,
        30.39,
        22.73,
        21.97,
        45.85,
    ],  # USD/hr, pay rate for each type of worker
    labor_burden=25,  # % fringe benefits
    operators_per_shift=[4, 9, 2, 2, 2, 3],  # number of each type of worker per shift
    hours_per_shift=hours_per_shift,
    shifts_per_day=shifts_per_day,
    operating_days_per_year=operating_days_per_year,
    # arguments related to revenue
    pure_product_output_rates=pure_product_output_rates,
    mixed_product_output_rates=mixed_product_output_rates,
    mixed_product_sale_price_realization_factor=0.65,  # 65% price realization for mixed products
    # arguments related to Variable O&M costs
    variable_OM=True,
    resources=resources,
    rates=rates,
    prices={
        "reagents": 1 * getattr(pyunits, "USD_" + CE_index_year) / pyunits.kg,
    },
    efficiency=0.80,  # power usage efficiency, or fixed motor/distribution efficiency
    chemicals=["reagents"],
    waste=[
        "nonhazardous_solid_waste",
        "nonhazardous_precipitate_waste",
        "dust_and_volatiles",
    ],
    # arguments related to total owners costs
    feed_input=m.fs.feed_input,
    land_cost=m.fs.land_cost,
    recovery_rate_per_year=m.fs.recovery_rate_per_year,
    transport_cost_per_ton_product=m.fs.transport_cost_per_ton_product,
    CE_index_year=CE_index_year,
)

# Define reagent fill costs as an other plant cost so framework adds this to the TPC calculation
m.fs.costing_5.other_plant_costs.unfix()
m.fs.costing_5.other_plant_costs_rule = Constraint(
    expr=(
        m.fs.costing_5.other_plant_costs
        == pyunits.convert(
            1218073 * pyunits.USD_2016  # component 1
            + 48723 * pyunits.USD_2016  # component 2
            + 182711 * pyunits.USD_2016,  # component 3
            to_units=getattr(pyunits, "MUSD_" + CE_index_year),
        )
    )
)

# Define an additional fixed O&M cost, must use flowsheet units
m.fs.costing_5.other_fixed_costs.fix(
    1 * getattr(pyunits, "MUSD_" + CE_index_year) / pyunits.a
)

# Define an additional variable O&M cost, must use flowsheet units
m.fs.costing_5.other_variable_costs.fix(
    1 * getattr(pyunits, "MUSD_" + CE_index_year) / pyunits.a
)


# Check diagnostics of model structure
dt = DiagnosticsToolbox(m)
dt.assert_no_structural_warnings()

# Initialization methods for different cost components

QGESSCostingData.costing_initialization(m.fs.costing_5)  # capital/installation costs
QGESSCostingData.initialize_fixed_OM_costs(m.fs.costing_5)  # fixed O&M costs
QGESSCostingData.initialize_variable_OM_costs(m.fs.costing_5)  # variable O&M costs

# Solve the model
results = solver.solve(m, tee=True)
assert_optimal_termination(results)

# Check diagnostics of model results
dt.assert_no_numerical_warnings()
assert m.fs.costing_5.total_BEC.value == pytest.approx(44.308, rel=1e-4)
assert m.fs.costing_5.total_installation_cost.value == pytest.approx(87.287, rel=1e-4)
assert m.fs.costing_5.total_plant_cost.value == pytest.approx(133.23, rel=1e-4)
assert m.fs.costing_5.total_fixed_OM_cost.value == pytest.approx(11.916, rel=1e-4)
assert m.fs.costing_5.total_sales_revenue.value == pytest.approx(27.654, rel=1e-4)
assert m.fs.costing_5.total_variable_OM_cost[0].value == pytest.approx(532.95, rel=1e-4)
assert m.fs.costing_5.plant_overhead_cost[0].value == pytest.approx(90.644, rel=1e-4)
assert value(m.fs.costing_5.cost_of_recovery) == pytest.approx(2202.434, rel=1e-4)
Ipopt 3.13.2: 

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit http://projects.coin-or.org/Ipopt

This version of Ipopt was compiled from source code available at
    https://github.com/IDAES/Ipopt as part of the Institute for the Design of
    Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
    Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.

This version of Ipopt was compiled using HSL, a collection of Fortran codes
    for large-scale scientific computation.  All technical papers, sales and
    publicity material resulting from use of the HSL codes within IPOPT must
    contain the following acknowledgement:
        HSL, a collection of Fortran codes for large-scale scientific
        computation. See http://www.hsl.rl.ac.uk.
******************************************************************************

This is Ipopt version 3.13.2, running with linear solver ma27.

Number of nonzeros in equality constraint Jacobian...:      224
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:       95

Total number of variables............................:       57
                     variables with only lower bounds:       18
                variables with lower and upper bounds:       25
                     variables with only upper bounds:        0
Total number of equality constraints.................:       57
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  0.0000000e+00 5.00e-03 1.00e+00  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  0.0000000e+00 5.00e-05 2.25e-01  -1.0 1.00e-02    -  9.90e-01 9.90e-01h  1
   2  0.0000000e+00 4.95e-07 1.98e-01  -1.0 1.00e-04    -  9.90e-01 9.90e-01h  1
   3  0.0000000e+00 5.29e-15 9.99e+00  -1.0 9.90e-07    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 3

                                   (scaled)                 (unscaled)
Objective...............:   0.0000000000000000e+00    0.0000000000000000e+00
Dual infeasibility......:   0.0000000000000000e+00    0.0000000000000000e+00
Constraint violation....:   5.2877003327323470e-15    5.2877003327323470e-15
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   5.2877003327323470e-15    5.2877003327323470e-15


Number of objective function evaluations             = 4
Number of objective gradient evaluations             = 4
Number of equality constraint evaluations            = 4
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 4
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 3
Total CPU secs in IPOPT (w/o function evaluations)   =      0.002
Total CPU secs in NLP function evaluations           =      0.000

EXIT: Optimal Solution Found.
WARNING: model contains export suffix 'scaling_factor' that contains 37
component keys that are not exported as part of the NL file.  Skipping.
# Report the results
m.fs.costing_5.report()
====================================================================================
costing_5
------------------------------------------------------------------------------------
                                                                             Value     
    Plant Cost Units                                                      MUSD_UKy_2019
    Total Plant Cost                                                             133.23
    Total Bare Erected Cost                                                      44.308
    Total Installation Cost                                                      87.287
    Total Other Plant Costs                                                      1.6309
    Summation of Ancillary Installation Costs                                    25.699
    Total Ancillary Piping, Materials and Labor Installation Cost                8.8616
    Total Ancillary Electrical, Materials and Labor Installation Cost            8.8616
    Total Ancillary Instrumentation Installation Cost                            3.5446
    Total Ancillary Plant Services Installation Cost                             4.4308
    Summation of Buildings Installation Costs                                    28.800
    Total Process Buildings Installation Cost                                    17.723
    Total Auxiliary Buildings Installation Cost                                  6.6462
    Total Site Improvements Buildings Installation Cost                          4.4308
    Summation of EPCM Installation Costs                                         26.142
    Total Equipment Installation EPCM Installation Cost                          7.5324
    Total Field Expenses EPCM Cost                                               5.3170
    Total Project Management and Construction EPCM Installation Cost             13.292
    Total Process Contingency Installation Cost                                  6.6462
    Summation of Contingency Installation Costs                                  6.6462
    Total Fixed Operating & Maintenance Cost                                     11.916
    Total Annual Operating Labor Cost                                            3.8090
    Total Annual Technical Labor Cost                                            1.8294
    Summation of Annual Labor Costs                                              5.6384
    Total Maintenance and Material Cost                                          2.6645
    Total Quality Assurance and Control Cost                                    0.38090
    Total Sales, Patenting and Research Cost                                    0.13827
    Summation of Sales, Admin and Insurance Cost                                 2.2323
    Total Admin Support and Labor Cost                                          0.76181
    Total Property Taxes and Insurance Cost                                      1.3323
    Total Other Fixed Costs                                                      1.0000
    Total Variable Power Cost                                                    6.7935
    Total Variable Waste Cost                                                    5.0282
    Total Variable Chemicals Cost                                                428.26
    General Plant Overhead Cost                                                  90.644
    Total Plant Overhead Cost, Including Maintenance & Quality Assurance         93.689
    Total Variable Operating & Maintenance Cost                                  532.95
    Total Land Cost                                                              1.2247
    Total Transport Cost                                                      0.0024134
    Total Sales Revenue Cost                                                     27.654
    Cost of Recovery (USD/kg REE)                                                2202.4
====================================================================================

Summary#

The REE Costing Framework supports detailed plant-wide costing for critical minerals and rare earth element processing systems. The examples shown in this notebook demonstrate the basics of using the costing framework, and an application for full-flowsheet costing using a test example. The next notebook will demonstrate some Advanced Costing Features available through the costing framework.