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.

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

# 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
m.scaling_factor = Suffix(direction=Suffix.EXPORT)

for j in m.fs.coal.component_list:
    if j not in ["Al2O3", "Fe2O3", "CaO", "inerts"]:
        m.scaling_factor[
            m.fs.leach_tanks.mscontactor.solid[0.0, 1].mass_frac_comp[j]
        ] = 1e5
        m.scaling_factor[
            m.fs.leach_tanks.mscontactor.solid_inlet_state[0.0].mass_frac_comp[j]
        ] = 1e5
        m.scaling_factor[
            m.fs.leach_tanks.mscontactor.heterogeneous_reactions[0.0, 1].reaction_rate[
                j
            ]
        ] = 1e5
        m.scaling_factor[
            m.fs.leach_tanks.mscontactor.solid[0.0, 1].conversion_comp_eqn[j]
        ] = 1e3
        m.scaling_factor[
            m.fs.leach_tanks.mscontactor.solid_inlet_state[0.0].conversion_comp_eqn[j]
        ] = 1e3

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

# Create a scaled version of the model to solve
scaling = TransformationFactory("core.scale_model")
scaled_model = scaling.create_using(m, rename=False)

# Initialize model
# This is likely to fail to converge, but gives a good enough starting point
initializer = LeachingTrainInitializer()
try:
    initializer.initialize(scaled_model.fs.leach_tanks)
except:
    pass

# Solve scaled model
solver = SolverFactory("ipopt")
solver.solve(scaled_model, tee=True)

# Propagate results back to unscaled model
scaling.propagate_solution(scaled_model, m)

# Diagnostics checks on model results
dt.assert_no_numerical_warnings()

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()

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)
m.fs.leach_tanks.costing.display()

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


# Add costing for the pumps scaled by m.fs.leach_liquid_feed.flow_vol[0]


# Solve the model and display results of the costing block

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


# 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


# 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


# Solve the model and display results of the costing block

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_1 = 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

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 block "costing_2" to the flowsheet for this example


# Create a new UnitModelBlock "unit2" and add a Var flow_vol with value 100 and units gal/min
# Remember to fix the variable


# Add costing for unit - use 1 piece equip, no scaling parallel equip, cost 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


# Solve and display results of the costing block

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


# 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


# Build process costs
# Use a total purchase cost of 0.0325796 and a Lang factor of 2.97 to simplify the model


# Solve and display results

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 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 costing block "costing_4" to the flowsheet for this example


# 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


# 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


# 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


# Solve and display results

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 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
solver = get_solver()
results = solver.solve(m, tee=True)
assert_optimal_termination(results)

# Check diagnostics of model results
dt.assert_no_numerical_warnings()
m.fs.costing_5.report()

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.