UKy Flowsheet with Costing#
The purpose of this tutorial is to demonstrate defining, adding, and solving costing for the University of Kentucky flowsheet. It is assumed that the user is already familiar with the flowsheet, which is introduced in detail in the UKy Flowsheet Tutorial. The user should have an understanding of the basic features and structure of the costing framework in PrOMMiS, which is introduced in detail in the Basic Costing Features tutorial.
Problem Statement#
This tutorial will show how to add costing to the West Kentucky No.13 Coal Refuse flowsheet, referred to in this tutorial as the University of Kentucky (UKy) flowsheet. As is the case for the flowsheet model, the inputs for the costing are case study-specific, so the flowsheet is not guaranteed to solve if the values are significantly altered.

The tutorial will take users through the following:
Importing the required tools from PrOMMiS and related repositories
Importing and building the UKy flowsheet
Adding costing to the UKy flowsheet
Initializing, solving, and displaying key cost results
Useful Links:
Public GitHub Repository: prommis/prommis
REE Costing Module Code: prommis/prommis
1 Import the necessary tools#
First, import the required Python, Pyomo, IDAES, and PrOMMiS packages. These will be implemented at various stages of the demonstration.
For installation instructions, please refer to the public GitHub repository linked above.
# import pytest
import pytest
# Pyomo packages
from pyomo.environ import (
assert_optimal_termination,
ConcreteModel,
SolverFactory,
Suffix,
TransformationFactory,
Constraint,
Var,
Param,
Expression,
units as pyunits,
check_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
from idaes.core.util.model_statistics import degrees_of_freedom
# PrOMMiS packages
from prommis.uky.uky_flowsheet import (
add_result_expressions,
build,
set_operating_conditions,
set_scaling,
initialize_system,
solve_system,
fix_organic_recycle,
display_results,
)
from prommis.uky.costing.ree_plant_capcost import QGESSCosting, QGESSCostingData
from prommis.uky.costing.costing_dictionaries import load_REE_costing_dictionary
2 Build the UKy Flowsheet#
2.1 Process Description#
The University of Kentucky (UKy) Flowsheet in PrOMMiS simulates a West Kentucky No. 13 Coal Refuse processing plant in which rare earth element (REE) components are recovered via a series of mechanical and chemical treatment processes. A diagram of the process is shown below:

2.2 Build the Flowsheet#
In this demonstration, we will call the flowsheet methods to build the flowsheet and will not show the model structure in detail. For more information, see the UKy Flowsheet Tutorial.
The flowsheet methods are called below:
# Call the flowsheet methods to build and connect the unit operations
m = build()
set_operating_conditions(m)
set_scaling(m)
add_result_expressions(m)
if degrees_of_freedom(m) != 0:
raise AssertionError(
"The degrees of freedom are not equal to 0."
"Check that the expected variables are fixed and unfixed."
"For more guidance, run assert_no_structural_warnings from the IDAES DiagnosticToolbox "
)
initialize_system(m)
solve_system(m)
# Fixes the volumetric flow rate of the organic recycle streams and unfixes the flow of the make-up streams
# We want to be able to adjust the total recycle flow rate, not just the make-up portion of it
fix_organic_recycle(m)
results = solve_system(m)
if not check_optimal_termination(results):
raise RuntimeError(
"Solver failed to terminate with an optimal solution. Please check the solver logs for more details"
)
# Adjust inputs to commercial scale
scaleup_factor = value(
pyunits.convert(
# UKy solid feed rate to leaching circuit
495 * pyunits.ton / pyunits.hr,
to_units=pyunits.kg / pyunits.hr,
)
/ (m.fs.leach_solid_feed.flow_mass[0])
)
for var in [
m.fs.leach_liquid_feed.flow_vol,
m.fs.leach_solid_feed.flow_mass,
m.fs.leach.volume,
m.fs.rougher_org_make_up.flow_vol,
m.fs.acid_feed1.flow_vol,
m.fs.acid_feed2.flow_vol,
m.fs.acid_feed3.flow_vol,
m.fs.cleaner_org_make_up.flow_vol,
m.fs.roaster.gas_inlet.flow_mol,
]:
for k in var.keys():
var[k].fix(
value(var[k]) * 1
) # TODO try reverting to use scaleup_factor once UKy flowsheet is fixed
results = get_solver("ipopt_v2").solve(m, tee=True)
display_results(m)
Scaling fs.leach
Scaling fs.sl_sep1
Scaling fs.scrubber_HCl_leach_translator
Scaling fs.leach_mixer
Scaling fs.leach_liquid_feed
Scaling fs.leach_solid_feed
Scaling fs.leach_filter_cake
Scaling fs.leach_filter_cake_liquid
Scaling fs.rougher_org_make_up
Scaling fs.solex_rougher_load
Scaling fs.acid_feed1
Scaling fs.solex_rougher_scrub
Scaling fs.acid_feed2
Scaling fs.solex_rougher_strip
Scaling fs.rougher_sep
Scaling fs.rougher_mixer
Scaling fs.load_sep
Scaling fs.scrub_sep
Scaling fs.rougher_organic_purge
Scaling fs.solex_cleaner_load
Scaling fs.solex_cleaner_strip
Scaling fs.cleaner_org_make_up
Scaling fs.cleaner_mixer
Scaling fs.cleaner_sep
Scaling fs.cleaner_HCl_leach_translator
Scaling fs.leach_sx_mixer
Scaling fs.acid_feed3
Scaling fs.cleaner_organic_purge
Scaling fs.precipitator
Scaling fs.sl_sep2
Scaling fs.precip_sep
Scaling fs.precip_sx_mixer
Scaling fs.precip_purge
Scaling fs.roaster
Scaling fs.leaching_sol_feed_expanded
Scaling fs.leaching_liq_feed_expanded
Scaling fs.leaching_feed_mixture_expanded
Scaling fs.leaching_solid_outlet_expanded
Scaling fs.leaching_liquid_outlet_expanded
Scaling fs.sl_sep1_solid_outlet_expanded
Scaling fs.sl_sep1_retained_liquid_outlet_expanded
Scaling fs.sl_sep1_liquid_outlet_expanded
Scaling fs.sx_rougher_load_aq_feed_expanded
Scaling fs.sx_rougher_org_feed_expanded
Scaling fs.sx_rougher_mixed_org_recycle_expanded
Scaling fs.sx_rougher_load_aq_outlet_expanded
Scaling fs.sx_rougher_load_aq_recycle_expanded
Scaling fs.sx_rougher_load_org_outlet_expanded
Scaling fs.sx_rougher_scrub_acid_feed_expanded
Scaling fs.sx_rougher_scrub_aq_outlet_expanded
Scaling fs.sx_rougher_scrub_aq_translator_expanded
Scaling fs.translator_scrub_recycle_expanded
Scaling fs.sx_rougher_scrub_org_outlet_expanded
Scaling fs.sx_rougher_strip_acid_feed_expanded
Scaling fs.sx_rougher_strip_org_outlet_expanded
Scaling fs.sx_rougher_strip_org_purge_expanded
Scaling fs.sx_rougher_strip_org_recycle_expanded
Scaling fs.sx_rougher_strip_aq_outlet_expanded
Scaling fs.sx_cleaner_load_aq_feed_expanded
Scaling fs.sx_cleaner_org_feed_expanded
Scaling fs.sx_cleaner_mixed_org_recycle_expanded
Scaling fs.sx_cleaner_load_aq_outlet_translator_expanded
Scaling fs.sx_cleaner_load_translator_leach_sx_mixer_expanded
Scaling fs.sx_cleaner_strip_acid_feed_expanded
Scaling fs.sx_cleaner_load_org_outlet_expanded
Scaling fs.sx_cleaner_strip_org_outlet_expanded
Scaling fs.sx_cleaner_strip_org_purge_expanded
Scaling fs.sx_cleaner_strip_org_recycle_expanded
Scaling fs.sx_cleaner_strip_aq_precip_expanded
Scaling fs.precip_solid_outlet_expanded
Scaling fs.precip_aq_sl_sep2_expanded
Scaling fs.sl_sep2_solid_outlet_expanded
Scaling fs.sl_sep2_liquid_outlet_expanded
Scaling fs.sl_sep2_retained_liquid_roaster_expanded
Scaling fs.sl_sep2_aq_purge_expanded
Scaling fs.sl_sep2_aq_recycle_expanded
2026-01-26 13:34:44 [INFO] idaes.prommis.uky.uky_flowsheet: Initialization Order: {_init_ord}
2026-01-26 13:34:44 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_solid_feed
2026-01-26 13:34:44 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_liquid_feed
2026-01-26 13:34:44 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_rougher_load
2026-01-26 13:34:45 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:45 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:45 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_org_make_up
2026-01-26 13:34:45 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.acid_feed1
2026-01-26 13:34:45 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.acid_feed2
2026-01-26 13:34:45 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_cleaner_load
2026-01-26 13:34:46 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:46 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:46 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_org_make_up
2026-01-26 13:34:46 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.acid_feed3
2026-01-26 13:34:46 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach
2026-01-26 13:34:47 [INFO] idaes.init.fs.leach.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:47 [INFO] idaes.init.fs.leach.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:47 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.load_sep
2026-01-26 13:34:48 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_rougher_scrub
2026-01-26 13:34:48 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:48 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:48 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_HCl_leach_translator
2026-01-26 13:34:49 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_cleaner_strip
2026-01-26 13:34:49 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:49 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:49 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.sl_sep1
2026-01-26 13:34:50 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.scrub_sep
2026-01-26 13:34:51 [INFO] idaes.init.fs.scrub_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-01-26 13:34:51 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_rougher_strip
2026-01-26 13:34:51 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:51 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:51 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_sep
2026-01-26 13:34:52 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precipitator
2026-01-26 13:34:52 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_filter_cake
2026-01-26 13:34:52 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_filter_cake_liquid
2026-01-26 13:34:52 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_sx_mixer
2026-01-26 13:34:52 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.scrubber_HCl_leach_translator
2026-01-26 13:34:52 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_sep
2026-01-26 13:34:53 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_mixer
2026-01-26 13:34:53 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_organic_purge
2026-01-26 13:34:53 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.sl_sep2
2026-01-26 13:34:53 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precip_sep
2026-01-26 13:34:54 [INFO] idaes.init.fs.precip_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-01-26 13:34:54 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_mixer
2026-01-26 13:34:54 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_mixer
2026-01-26 13:34:55 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_organic_purge
2026-01-26 13:34:55 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.roaster
2026-01-26 13:34:55 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precip_purge
2026-01-26 13:34:55 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precip_sx_mixer
2026-01-26 13:34:56 [INFO] idaes.init.fs.precip_sx_mixer: Initialization Complete: optimal - <undefined>
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_solid_feed
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_liquid_feed
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_org_make_up
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.acid_feed1
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.acid_feed2
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_org_make_up
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.acid_feed3
2026-01-26 13:34:56 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_cleaner_load
2026-01-26 13:34:56 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:57 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:57 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_rougher_load
2026-01-26 13:34:58 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:58 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:58 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Sc2O3]' to a numeric value
`-4.626416617352418e-09` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Y2O3]' to a numeric value
`-2.3672243090666785e-09` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Pr2O3]' to a numeric value
`-1.3732216770802377e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Sm2O3]' to a numeric value
`-1.7537902707478002e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Gd2O3]' to a numeric value
`-2.9141664370036106e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Dy2O3]' to a numeric value
`-4.397400324499399e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Y2O3]' to a numeric value
`-3.86051984302772e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[La2O3]' to a numeric value
`-1.3488266310338477e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Ce2O3]' to a numeric value
`-1.0298300697123324e-10` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Pr2O3]' to a numeric value
`-8.333246057828515e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Nd2O3]' to a numeric value
`-1.3528486101605311e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Sm2O3]' to a numeric value
`-9.820992419966043e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Gd2O3]' to a numeric value
`-1.4389926027723214e-07` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Dy2O3]' to a numeric value
`-2.022038233042291e-07` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Sc2O3]' to a numeric value
`-4.7508365881559147e-08` outside the bounds (0, 0.99999999).
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
2026-01-26 13:34:58 [INFO] idaes.init.fs.leach.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:59 [INFO] idaes.init.fs.leach.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:59 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_cleaner_strip
2026-01-26 13:34:59 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Stream Initialization Completed.
2026-01-26 13:34:59 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:34:59 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_HCl_leach_translator
2026-01-26 13:35:00 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_rougher_scrub
2026-01-26 13:35:00 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Stream Initialization Completed.
2026-01-26 13:35:00 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:35:00 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.load_sep
2026-01-26 13:35:01 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.sl_sep1
2026-01-26 13:35:01 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precipitator
2026-01-26 13:35:01 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_sx_mixer
2026-01-26 13:35:02 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_sep
2026-01-26 13:35:02 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.solex_rougher_strip
2026-01-26 13:35:02 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Stream Initialization Completed.
2026-01-26 13:35:03 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-01-26 13:35:03 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.scrub_sep
2026-01-26 13:35:03 [INFO] idaes.init.fs.scrub_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-01-26 13:35:03 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.sl_sep2
2026-01-26 13:35:03 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_mixer
2026-01-26 13:35:04 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_sep
2026-01-26 13:35:04 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.scrubber_HCl_leach_translator
2026-01-26 13:35:04 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precip_sep
2026-01-26 13:35:04 [INFO] idaes.init.fs.precip_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-01-26 13:35:04 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_mixer
2026-01-26 13:35:05 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_mixer
2026-01-26 13:35:05 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precip_sx_mixer
2026-01-26 13:35:05 [INFO] idaes.init.fs.precip_sx_mixer: Initialization Complete: optimal - <undefined>
WARNING: Direct failed to converge in 1 iterations
2026-01-26 13:35:05 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_filter_cake
2026-01-26 13:35:05 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.leach_filter_cake_liquid
2026-01-26 13:35:05 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.rougher_organic_purge
2026-01-26 13:35:06 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.cleaner_organic_purge
2026-01-26 13:35:06 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.roaster
2026-01-26 13:35:06 [INFO] idaes.prommis.uky.uky_flowsheet: Initializing fs.precip_purge
Ipopt 3.13.2: linear_solver="ma57"
max_iter=200
nlp_scaling_method="gradient-based"
tol=1e-06
******************************************************************************
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 ma57.
Number of nonzeros in equality constraint Jacobian...: 4772
Number of nonzeros in inequality constraint Jacobian.: 0
Number of nonzeros in Lagrangian Hessian.............: 650
Total number of variables............................: 1510
variables with only lower bounds: 1139
variables with lower and upper bounds: 55
variables with only upper bounds: 0
Total number of equality constraints.................: 1510
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 1.77e+00 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0
Reallocating memory for MA57: lfact (86530)
Reallocating memory for MA57: lfact (95798)
1 0.0000000e+00 1.77e-02 2.09e+02 -1.0 1.00e-02 - 9.90e-01 9.90e-01h 1
2 0.0000000e+00 1.74e-04 1.09e+01 -1.0 1.00e-04 - 9.90e-01 9.90e-01h 1
Reallocating memory for MA57: lfact (101805)
3 0.0000000e+00 1.42e-13 9.84e+02 -1.0 9.81e-07 - 9.90e-01 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....: 6.0396132539608516e-14 1.4210854715202004e-13
Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00
Overall NLP error.......: 6.0396132539608516e-14 1.4210854715202004e-13
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.039
Total CPU secs in NLP function evaluations = 0.000
EXIT: Optimal Solution Found.
====================================================================================
Unit : fs.roaster Time: 0.0
------------------------------------------------------------------------------------
Unit Performance
Expressions:
Key : Value : Units
Product Al Mass Fraction : 0.0060644 : dimensionless
Product Ca Mass Fraction : 0.034877 : dimensionless
Product Ce Mass Fraction : 0.052904 : dimensionless
Product Dy Mass Fraction : 0.074091 : dimensionless
Product Fe Mass Fraction : 0.23230 : dimensionless
Product Gd Mass Fraction : 0.54163 : dimensionless
Product La Mass Fraction : 0.013945 : dimensionless
Product Nd Mass Fraction : 0.021171 : dimensionless
Product Pr Mass Fraction : 0.0041915 : dimensionless
Product Sc Mass Fraction : 1.3884e-06 : dimensionless
Product Sm Mass Fraction : 0.013422 : dimensionless
Product Y Mass Fraction : 0.0054037 : dimensionless
------------------------------------------------------------------------------------
Stream Table
Empty DataFrame
Columns: [Units]
Index: []
====================================================================================
REE product mass flow is 1.3110505979403861e-05 kg/hr
REE feed mass flow is 0.007623590660980152 kg/hr
Total REE recovery is 0.1719728479980884 %
Product purity is 62.90944953818737 % REE
REE element recovery:
Leaching scandium recovery is 3.389431614123762 %
Overall scandium recovery is 4.5899396221863945e-06 %
Scandium rejected in leach filter cake solids is 95.57126625962546 %
Scandium rejected in leach filter cake entrained liquid is 1.4589266306743924 %
Scandium rejected in rougher load aqueous purge is 0.041599444533323586 %
Scandium rejected in rougher scrub aqueous purge is 0.006689621823970186 %
Scandium rejected in rougher organic purge is 2.9146069968064365 %
Scandium rejected in cleaner aqueous purge is 9.405835296788149e-07 %
Scandium rejected in cleaner organic purge is 0.00665795468702667 %
Scandium accounted for is 99.99975243867377 %
Leaching yttrium recovery is 8.048680516038901 %
Overall yttrium recovery is 0.01510851798579413 %
Yttrium rejected in leach filter cake solids is 88.60701209224437 %
Yttrium rejected in leach filter cake entrained liquid is 3.4535270857285942 %
Yttrium rejected in rougher load aqueous purge is 0.007200824070023154 %
Yttrium rejected in rougher scrub aqueous purge is 0.0059819445348419245 %
Yttrium rejected in rougher organic purge is 7.544233858306028 %
Yttrium rejected in cleaner aqueous purge is 0.0004909460229073468 %
Yttrium rejected in cleaner organic purge is 0.36522856161538064 %
Yttrium accounted for is 99.99878383050795 %
Leaching lanthanum recovery is 37.33111452100455 %
Overall lanthanum recovery is 0.018905965658229364 %
Lanthanum rejected in leach filter cake solids is 70.26755760963272 %
Lanthanum rejected in leach filter cake entrained liquid is 24.088626233977926 %
Lanthanum rejected in rougher load aqueous purge is 5.480308564232273 %
Lanthanum rejected in rougher scrub aqueous purge is 0.13778724509131185 %
Lanthanum rejected in rougher organic purge is 0.0021543456251946007 %
Lanthanum rejected in cleaner aqueous purge is 0.0016860201884069746 %
Lanthanum rejected in cleaner organic purge is 0.0002225228387954223 %
Lanthanum accounted for is 99.99724850724486 %
Leaching cerium recovery is 41.827428815595155 %
Overall cerium recovery is 0.03112998094098154 %
Cerium rejected in leach filter cake solids is 64.52148416005821 %
Cerium rejected in leach filter cake entrained liquid is 28.738169474607403 %
Cerium rejected in rougher load aqueous purge is 6.535392793556418 %
Cerium rejected in rougher scrub aqueous purge is 0.16629678950605442 %
Cerium rejected in rougher organic purge is 0.0028453586565232816 %
Cerium rejected in cleaner aqueous purge is 0.0013833387065902022 %
Cerium rejected in cleaner organic purge is 0.00029977735448932146 %
Cerium accounted for is 99.99700167338665 %
Leaching praseodymium recovery is 45.227849874980855 %
Overall praseodymium recovery is 0.022466234775436186 %
Praseodymium rejected in leach filter cake solids is 59.66817910265356 %
Praseodymium rejected in leach filter cake entrained liquid is 32.68169084414525 %
Praseodymium rejected in rougher load aqueous purge is 7.498121423218083 %
Praseodymium rejected in rougher scrub aqueous purge is 0.12486820818929434 %
Praseodymium rejected in rougher organic purge is 0.002160170557340657 %
Praseodymium rejected in cleaner aqueous purge is 0.0006002995654651861 %
Praseodymium rejected in cleaner organic purge is 0.0003070503254364978 %
Praseodymium accounted for is 99.99839333342986 %
Leaching neodynium recovery is 45.58928745468378 %
Overall neodynium recovery is 0.02875125310203011 %
Neodynium rejected in leach filter cake solids is 59.11867631329024 %
Neodynium rejected in leach filter cake entrained liquid is 33.12264670753368 %
Neodynium rejected in rougher load aqueous purge is 7.563742390352754 %
Neodynium rejected in rougher scrub aqueous purge is 0.16148575597874987 %
Neodynium rejected in rougher organic purge is 0.0021331978856033436 %
Neodynium rejected in cleaner aqueous purge is 0.0006162459937191675 %
Neodynium rejected in cleaner organic purge is 0.00019522087131928971 %
Neodynium accounted for is 99.9982470850081 %
Leaching samarium recovery is 26.14800854879715 %
Overall samarium recovery is 0.08337798974476171 %
Samarium rejected in leach filter cake solids is 81.85198217103438 %
Samarium rejected in leach filter cake entrained liquid is 14.641950180452609 %
Samarium rejected in rougher load aqueous purge is 3.2373425563665097 %
Samarium rejected in rougher scrub aqueous purge is 0.16913818119301202 %
Samarium rejected in rougher organic purge is 0.008222819582271383 %
Samarium rejected in cleaner aqueous purge is 0.0011439260244558937 %
Samarium rejected in cleaner organic purge is 0.0013393821038924895 %
Samarium accounted for is 99.99449720650189 %
Leaching gadolinium recovery is 57.38569721795633 %
Overall gadolinium recovery is 4.783278287820991 %
Gadolinium rejected in leach filter cake solids is 34.992714618349815 %
Gadolinium rejected in leach filter cake entrained liquid is 47.756514664889046 %
Gadolinium rejected in rougher load aqueous purge is 8.3041390725735 %
Gadolinium rejected in rougher scrub aqueous purge is 2.1603674638611112 %
Gadolinium rejected in rougher organic purge is 1.2080439642498682 %
Gadolinium rejected in cleaner aqueous purge is 0.06173503729106891 %
Gadolinium rejected in cleaner organic purge is 0.413399511067394 %
Gadolinium accounted for is 99.6801926201028 %
Leaching dysprosium recovery is 12.710585091156567 %
Overall dysprosium recovery is 0.9019485061054804 %
Dysprosium rejected in leach filter cake solids is 83.90742525412882 %
Dysprosium rejected in leach filter cake entrained liquid is 5.584857996964025 %
Dysprosium rejected in rougher load aqueous purge is 0.1612768993121641 %
Dysprosium rejected in rougher scrub aqueous purge is 0.11911078183983825 %
Dysprosium rejected in rougher organic purge is 7.497253861961234 %
Dysprosium rejected in cleaner aqueous purge is 0.01258775988086633 %
Dysprosium rejected in cleaner organic purge is 1.754211820085449 %
Dysprosium accounted for is 99.93867288027789 %
Gangue recovery:
Leaching aluminum recovery is 6.595118890309193 %
Overall aluminum recovery is 2.3512219944934572e-06 %
Aluminum rejected in leach filter cake solids is 96.29421762139852 %
Aluminum rejected in leach filter cake entrained liquid is 3.0048328955800216 %
Aluminum rejected in rougher load aqueous purge is 0.6975396542468848 %
Aluminum rejected in rougher scrub aqueous purge is 0.003581336335065468 %
Aluminum rejected in rougher organic purge is 1.169441579067471e-05 %
Aluminum rejected in cleaner aqueous purge is 2.4528776016824764e-05 %
Aluminum rejected in cleaner organic purge is 8.98034833476379e-07 %
Aluminum accounted for is 100.00021098000911 %
Leaching iron recovery is 22.13719535085123 %
Overall iron recovery is 0.00033249060149180017 %
Iron rejected in leach filter cake solids is 85.38417062835481 %
Iron rejected in leach filter cake entrained liquid is 11.847362479531935 %
Iron rejected in rougher load aqueous purge is 2.726493337640036 %
Iron rejected in rougher scrub aqueous purge is 0.03740714119368132 %
Iron rejected in rougher organic purge is 0.0013243481402802114 %
Iron rejected in cleaner aqueous purge is 0.0012594480057543716 %
Iron rejected in cleaner organic purge is 0.0005078244296334413 %
Iron accounted for is 99.99885769789763 %
Leaching calcium recovery is 43.763761425052905 %
Overall calcium recovery is 0.0009682277711279309 %
Calcium rejected in leach filter cake solids is 61.8354134748125 %
Calcium rejected in leach filter cake entrained liquid is 30.943313280515596 %
Calcium rejected in rougher load aqueous purge is 7.174580198813556 %
Calcium rejected in rougher scrub aqueous purge is 0.04532096805375407 %
Calcium rejected in rougher organic purge is 0.000254574303011557 %
Calcium rejected in cleaner aqueous purge is 0.00035572116572119933 %
Calcium rejected in cleaner organic purge is 2.792643808843805e-05 %
Calcium accounted for is 100.00023437187336 %
3 Add Costing#
To begin, we need to create a flowsheet-level costing block. This serves as a central location for plant-wide costing, as well as a reference to let equipment costing import the costing library and equations from the specified source. We also need to define the cost year; Pyomo supports conversion of cost years from 1990-2023 and setting this string will automatically convert flowsheet cost results to the specified cost year.
In this demonstration, we will use the QGESSCosting() method imported from the module ree_plant_capcost.py which support the economic formulation used in the UKy study. The Quality Guidelines for Energy Systems Studies (QGESS) form the backbone of the costing framework, incorporating economies of scale for equipment costing and assumptions for operating, maintenance, installation, and overnight cost estimation based on prior knowledge of industrial processes. The QGESS methods and implementation are introduced for PrOMMiS modeling in the Basic Costing Features and more generally in the IDAES Power Plant Costing documentation.
Now, let’s add the flowsheet-level costing block and set the cost year:
m.fs.costing = QGESSCosting()
CE_index_year = "2023"
3.1 Build capital costing for a unit model#
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(). The latter option is useful to cost balance-of-plant equipment that is not explicitly modeled in the flowsheet.
To begin, let’s add costing for the leaching tanks:
# 4.2 is UKy Leaching - Polyethylene Tanks
L_pe_tanks_accounts = ["4.2"]
m.fs.leach.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": L_pe_tanks_accounts,
"scaled_param": m.fs.leach.volume[0, 1],
"source": 1,
"n_equip": 3,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
The UnitModelCostingBlock takes several arguments:
flowsheet_costing_block- a flowsheet-level costing block to attach the cost model to, in this case the one we created.costing_method- the method to use for equipment cost calculations, which exists in theQGESSCostingDataclass.
There are a few arguments for the QGESSCosting method:
cost_accounts- the list of reference cost accounts from the PrOMMiS library to use; we are only using one here, but the method supports multiple cost accounts if they scale with the same scaling parameter, use the same reference source, and have the same number of parallel units.scaled_param- the variable used to calculate the scaled cost per the following power law for economies of scale:
\( scaledCost = referenceCost * (\frac{scaledParam}{referenceParam})^{EXP}\)
where the values of referencecost, referenceparam, and EXP are sourced from the PrOMMiS cost account library.
source- the library account list to draw from; PrOMMiS currently supports two sources: a large number of equipment accounts from the UKy flowsheet (“1”) and additional accounts for magnet recycling (“2”).n_equip- the number of parallel equipment, for example we have 3 leach tanks in parallelscale_down_parallel_equip- whether the scaling parameter is the capacity of the entire train (“True”) or each individual unit in the train (“False”).
For example, if set to “False” the scaled parameter (flowsheet variable for tank volume) is taken as the volume of each individual leach tank:
\( trainCost = nEquip * referenceCost * (\frac{scaledParam}{referenceParam})^{EXP} = nEquip * scaledCost\)
If set to “True”, the volume is taken as the total volume of all tanks in the train, and the individual volumes are scaled down:
\( trainCost = nEquip * referenceCost * (\frac{scaledParam}{nEquip * referenceParam})^{EXP} = nEquip * scaledCost * (\frac{1}{nEquip})^{EXP} = nEquip^{1-EXP} * scaledCost\)
Due to economies of scale (cost per capacity becomes cheaper as capacity increases), the exponents for most cost accounts are less than 1. This option is useful when equipment is not constrained by a discrete set of available capacities, and the total capacity of a train is more readily available than the size of each individual unit.
CE_index_year- the basis year for flowsheet cost calculations, which we set as “2023” earlier.
3.2 Build capital costing for the rest of the flowsheet#
Let’s do this for all other equipment. Each equipment requires its own costing block, and the syntax is exactly the same as how we costed the leach tank.
However, many of the balance-of-plant equipment (pumps, tanks, filters) either do not have explicitly unit models in the flowsheet, or the process parameter to estimate the scaled capital cost is not known. These unit operations need to be included in the cost, so blocks will be created for them. For example, additional cost components for leaching are attached to UnitModelBlock() objects, since a unit model can only have one costing block attached. An empirical scaling is applied using the flowrate into the leaching, rougher solvent extraction cleaner solvent extraction, and precipitation sections to estimate non-flow parameters using data from the University of Kentucky report.
The rest of the equipment costing is added below:
# Define reference values for empirical scaling to estimate balance of
# Plant unit operation process parameters
# scaled_parameter = reference_parameter * (scaled_basis_flow/reference_basis_flow)
# Reference values from UKy study - Table 4-7 p. 351
REE_costing_params = load_REE_costing_dictionary()
reference_basis_flow = {
"leach_sol_flow_mass": 495 * pyunits.ton / pyunits.hr, # p. 273, 351
"rougher_solex_aqueous_flow_vol": 23131 * pyunits.L / pyunits.min,
"cleaner_solex_aqueous_flow_vol": 925 * pyunits.L / pyunits.min,
"precipitator_solex_aqueous_flow_vol": 231 * pyunits.L / pyunits.min,
}
# Leaching costs
# 4.3 is UKy Leaching - Tank Mixer
L_tank_mixer_accounts = ["4.3"]
m.fs.leach_mixer.power = Var(initialize=4.74, units=pyunits.hp, bounds=(0, None))
@m.fs.leach_mixer.Constraint(L_tank_mixer_accounts)
def power_scaling_constraint(c, k):
return m.fs.leach_mixer.power == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.hp
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=pyunits.hp,
)
m.fs.leach_mixer.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": L_tank_mixer_accounts,
"scaled_param": m.fs.leach_mixer.power,
"source": 1,
"n_equip": 3,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 4.4 is UKy Leaching - Process Pump
L_pump_accounts = ["4.4"]
m.fs.leach_pump = UnitModelBlock()
m.fs.leach_pump.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": L_pump_accounts,
"scaled_param": m.fs.leach_liquid_feed.flow_vol[0],
"source": 1,
"n_equip": 3,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 4.5 is UKy Leaching - Thickener
L_thickener_accounts = ["4.5"]
m.fs.leach_sx_mixer.area = Var(initialize=225.90, units=pyunits.ft**2, bounds=(0, None))
@m.fs.leach_sx_mixer.Constraint(L_thickener_accounts)
def area_scaling_constraint(c, k):
return m.fs.leach_sx_mixer.area == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.ft**2
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=pyunits.ft**2,
)
m.fs.leach_sx_mixer.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": L_thickener_accounts,
"scaled_param": m.fs.leach_sx_mixer.area,
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 4.6 is UKy Leaching - Solid Waste Filter Press
L_filter_press_accounts = ["4.6"]
m.fs.sl_sep1.volume = Var(initialize=36.00, units=pyunits.ft**3, bounds=(0, None))
@m.fs.sl_sep1.Constraint(L_filter_press_accounts)
def volume_scaling_constraint(c, k):
return m.fs.sl_sep1.volume == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.ft**3
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=pyunits.ft**3,
)
m.fs.sl_sep1.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": L_filter_press_accounts,
"scaled_param": m.fs.sl_sep1.volume,
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 4.8 is UKy Leaching - Solution Heater
L_solution_heater_accounts = ["4.8"]
m.fs.leach_solution_heater = UnitModelBlock()
m.fs.leach_solution_heater.duty = Var(
initialize=0.24, units=pyunits.MBTU / pyunits.hr, bounds=(0, None)
)
@m.fs.leach_solution_heater.Constraint(L_solution_heater_accounts)
def duty_scaling_constraint(c, k):
return m.fs.leach_solution_heater.duty == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.MBTU
/ pyunits.hr
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=pyunits.MBTU / pyunits.hr,
)
m.fs.leach_solution_heater.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": L_solution_heater_accounts,
"scaled_param": m.fs.leach_solution_heater.duty,
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# Solvent extraction costs
# 5.1 is UKy Rougher Solvent Extraction - Polyethylene Tanks
RSX_pe_tanks_accounts = ["5.1"]
m.fs.rougher_solex_tank = UnitModelBlock()
m.fs.rougher_solex_tank.volume = Var(
initialize=35.136, units=pyunits.gal, bounds=(0, None)
)
@m.fs.rougher_solex_tank.Constraint(RSX_pe_tanks_accounts)
def volume_scaling_constraint(c, k):
return m.fs.rougher_solex_tank.volume == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.gal
* (
m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["rougher_solex_aqueous_flow_vol"]
),
to_units=pyunits.gal,
)
m.fs.rougher_solex_tank.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": RSX_pe_tanks_accounts,
"scaled_param": m.fs.rougher_solex_tank.volume,
"source": 1,
"n_equip": 6,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 5.2 is UKy Rougher Solvent Extraction - Tank Mixer
RSX_tank_mixer_accounts = ["5.2"]
m.fs.rougher_mixer.power = Var(initialize=2.0, units=pyunits.hp, bounds=(0, None))
@m.fs.rougher_mixer.Constraint(RSX_tank_mixer_accounts)
def power_scaling_constraint(c, k):
return m.fs.rougher_mixer.power == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.hp
* (
m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["rougher_solex_aqueous_flow_vol"]
),
to_units=pyunits.hp,
)
m.fs.rougher_mixer.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": RSX_tank_mixer_accounts,
"scaled_param": m.fs.rougher_mixer.power,
"source": 1,
"n_equip": 2,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 5.3 is UKy Rougher Solvent Extraction - Process Pump
RSX_pump_accounts = ["5.3"]
m.fs.rougher_pump = UnitModelBlock()
m.fs.rougher_pump.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": RSX_pump_accounts,
"scaled_param": m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0],
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 5.4 is UKy Rougher Solvent Extraction - Mixer Settler
RSX_mixer_settler_accounts = ["5.4"]
m.fs.rougher_solex_settler = UnitModelBlock()
m.fs.rougher_solex_settler.volume = Var(
initialize=61.107, units=pyunits.gal, bounds=(0, None)
)
@m.fs.rougher_solex_settler.Constraint(RSX_mixer_settler_accounts)
def volume_scaling_constraint(c, k):
return m.fs.rougher_solex_settler.volume == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.gal
* (
m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["rougher_solex_aqueous_flow_vol"]
),
to_units=pyunits.gal,
)
m.fs.rougher_solex_settler.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": RSX_mixer_settler_accounts,
"scaled_param": m.fs.rougher_solex_settler.volume,
"source": 1,
"n_equip": 6,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 6.1 is UKy Cleaner Solvent Extraction - Polyethylene Tanks
CSX_pe_tanks_accounts = ["6.1"]
m.fs.cleaner_solex_tank = UnitModelBlock()
m.fs.cleaner_solex_tank.volume = Var(
initialize=14.05, units=pyunits.gal, bounds=(0, None)
)
@m.fs.cleaner_solex_tank.Constraint(CSX_pe_tanks_accounts)
def volume_scaling_constraint(c, k):
return m.fs.cleaner_solex_tank.volume == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.gal
* (
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["cleaner_solex_aqueous_flow_vol"]
),
to_units=pyunits.gal,
)
m.fs.cleaner_solex_tank.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": CSX_pe_tanks_accounts,
"scaled_param": m.fs.cleaner_solex_tank.volume,
"source": 1,
"n_equip": 5,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 6.2 is UKy Cleaner Solvent Extraction - Tank Mixer
CSX_tank_mixer_accounts = ["6.2"]
m.fs.cleaner_mixer.power = Var(initialize=0.08, units=pyunits.hp, bounds=(0, None))
@m.fs.cleaner_mixer.Constraint(CSX_tank_mixer_accounts)
def power_scaling_constraint(c, k):
return m.fs.cleaner_mixer.power == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.hp
* (
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["cleaner_solex_aqueous_flow_vol"]
),
to_units=pyunits.hp,
)
m.fs.cleaner_mixer.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": CSX_tank_mixer_accounts,
"scaled_param": m.fs.cleaner_mixer.power,
"source": 1,
"n_equip": 2,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 6.3 is UKy Cleaner Solvent Extraction - Process Pump
CSX_pump_accounts = ["6.3"]
m.fs.cleaner_pump = UnitModelBlock()
m.fs.cleaner_pump.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": CSX_pump_accounts,
"scaled_param": m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0],
"source": 1,
"n_equip": 3,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 6.4 is UKy Cleaner Solvent Extraction - Mixer Settler
CSX_mixer_settler_accounts = ["6.4"]
m.fs.cleaner_solex_settler = UnitModelBlock()
m.fs.cleaner_solex_settler.volume = Var(
initialize=24.44, units=pyunits.gal, bounds=(0, None)
)
@m.fs.cleaner_solex_settler.Constraint(CSX_mixer_settler_accounts)
def volume_scaling_constraint(c, k):
return m.fs.cleaner_solex_settler.volume == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.gal
* (
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["cleaner_solex_aqueous_flow_vol"]
),
to_units=pyunits.gal,
)
m.fs.cleaner_solex_settler.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": CSX_mixer_settler_accounts,
"scaled_param": m.fs.cleaner_solex_settler.volume,
"source": 1,
"n_equip": 6,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# Precipitation costs
# 10.1 is UKy Oxalate Precipitation - Polyethylene Tanks
reep_pe_tanks_accounts = ["10.1"]
m.fs.precipitator.volume = Var(initialize=15.04, units=pyunits.gal, bounds=(0, None))
@m.fs.precipitator.Constraint(reep_pe_tanks_accounts)
def volume_scaling_constraint(c, k):
return m.fs.precipitator.volume == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.gal
* (
m.fs.precipitator.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["precipitator_solex_aqueous_flow_vol"]
),
to_units=pyunits.gal,
)
m.fs.precipitator.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": reep_pe_tanks_accounts,
"scaled_param": m.fs.precipitator.volume,
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 10.2 is UKy Oxalate Precipitation - Tank Mixer
reep_tank_mixer_accounts = ["10.2"]
m.fs.precipitator_mixer = UnitModelBlock()
m.fs.precipitator_mixer.power = Var(initialize=0.61, units=pyunits.hp, bounds=(0, None))
@m.fs.precipitator_mixer.Constraint(reep_tank_mixer_accounts)
def power_scaling_constraint(c, k):
return m.fs.precipitator_mixer.power == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.hp
* (
m.fs.precipitator.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["precipitator_solex_aqueous_flow_vol"]
),
to_units=pyunits.hp,
)
m.fs.precipitator_mixer.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": reep_tank_mixer_accounts,
"scaled_param": m.fs.precipitator_mixer.power,
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 10.3 is UKy Oxalate Precipitation - Process Pump
reep_pump_accounts = ["10.3"]
m.fs.precipitator_pump = UnitModelBlock()
m.fs.precipitator_pump.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": reep_pump_accounts,
"scaled_param": m.fs.precipitator.aqueous_inlet.flow_vol[0],
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 10.4 is UKy Oxalate Precipitation - Filter Press
reep_filter_press_accounts = ["10.4"]
m.fs.sl_sep2.volume = Var(initialize=0.405, units=pyunits.ft**3, bounds=(0, None))
@m.fs.sl_sep2.Constraint(reep_filter_press_accounts)
def volume_scaling_constraint(c, k):
return m.fs.sl_sep2.volume == pyunits.convert(
REE_costing_params["1"][k]["RP Value"]
* pyunits.ft**3
* (
m.fs.precipitator.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["precipitator_solex_aqueous_flow_vol"]
),
to_units=pyunits.ft**3,
)
m.fs.sl_sep2.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": reep_filter_press_accounts,
"scaled_param": m.fs.sl_sep2.volume,
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
# 10.5 is UKy Oxalate Precipitation - Roaster
reep_roaster_accounts = ["10.5"]
m.fs.roaster.costing = UnitModelCostingBlock(
flowsheet_costing_block=m.fs.costing,
costing_method=QGESSCostingData.get_REE_costing,
costing_method_arguments={
"cost_accounts": reep_roaster_accounts,
"scaled_param": abs(m.fs.roaster.heat_duty[0]),
"source": 1,
"n_equip": 1,
"scale_down_parallel_equip": False,
"CE_index_year": CE_index_year,
},
)
3.3 Add Data Needed For Plantwide Costing#
Before building plantwide costing (installation for equipment, fixed operating costs, variable operating costs, overnight costs), we need to add some more components to our model.
The framework calculates the sales revenue based on product composition and mass flows, so we need to define those as a dictionary:
# Molecular weights for convenience
REO_molar_mass = {
"Y2O3": 88.906 * 2 + 16 * 3,
"La2O3": 138.91 * 2 + 16 * 3,
"Ce2O3": 140.12 * 2 + 16 * 3,
"Pr2O3": 140.91 * 2 + 16 * 3,
"Nd2O3": 144.24 * 2 + 16 * 3,
"Sm2O3": 150.36 * 2 + 16 * 3,
"Gd2O3": 157.25 * 2 + 16 * 3,
"Dy2O3": 162.5 * 2 + 16 * 3,
"Sc2O3": 44.96 * 2 + 16 * 3,
}
# The flowsheet state variable for flow is molar flow
# Components are all in the form X2O3 so we can do this compactly
# Add a function to create mass flow parameters
def product_mass_flow(blk, component):
param = Param(
default=pyunits.convert(
m.fs.roaster.flow_mol_comp_product[0, component]
* REO_molar_mass[component + "2O3"]
* pyunits.g
/ pyunits.mol,
to_units=pyunits.kg / pyunits.hr,
),
units=pyunits.kg / pyunits.hr,
mutable=True,
)
return param
# Create the dictionary
m.fs.Y_product = product_mass_flow(m.fs, "Y")
m.fs.La_product = product_mass_flow(m.fs, "La")
m.fs.Ce_product = product_mass_flow(m.fs, "Ce")
m.fs.Pr_product = product_mass_flow(m.fs, "Pr")
m.fs.Nd_product = product_mass_flow(m.fs, "Nd")
m.fs.Sm_product = product_mass_flow(m.fs, "Sm")
m.fs.Gd_product = product_mass_flow(m.fs, "Gd")
m.fs.Dy_product = product_mass_flow(m.fs, "Dy")
m.fs.Sc_product = product_mass_flow(m.fs, "Sc")
pure_product_output_rates = {}
mixed_product_output_rates = {
"CeO2": m.fs.Ce_product,
"Sc2O3": m.fs.Sc_product,
"Y2O3": m.fs.Y_product,
"La2O3": m.fs.La_product,
"Nd2O3": m.fs.Nd_product,
"Pr6O11": m.fs.Pr_product,
"Sm2O3": m.fs.Sm_product,
"Gd2O3": m.fs.Gd_product,
"Dy2O3": m.fs.Dy_product,
}
The framework considers pure products separately from mixed products, which are products that exist in a “mixed basket” with many other products. These mixed baskets may be sold at a reduced price realization rather than discarded or further purified. In our case, all oxides are part of the mixed basket and we have no pure product streams.
Next, we need to define the streams for the variable operating costs. Let’s add the power to operate the tank mixers and the disposal cost of the leach filter waste:
# Power for tank mixers
m.fs.power = Var(m.fs.time, initialize=7, units=pyunits.hp)
m.fs.power_constraint = Constraint(
expr=m.fs.power[0]
== pyunits.convert(
m.fs.precipitator_mixer.power
+ m.fs.cleaner_mixer.power
+ m.fs.rougher_mixer.power
+ m.fs.leach_mixer.power,
to_units=pyunits.hp,
)
)
# Solid waste from leach filter
m.fs.solid_waste = Var(m.fs.time, initialize=0.0245, units=pyunits.ton / pyunits.hr)
m.fs.solid_waste_constraint = Constraint(
expr=m.fs.solid_waste[0]
== pyunits.convert(
m.fs.leach_filter_cake.flow_mass[0], to_units=pyunits.ton / pyunits.hr
)
)
Finally, let’s add the land cost; this will be included in the overnight cost calculation. The land leasing cost is taken as $0.303736 per ton of feed processed per day, which is the total annual cost of lease agreements normalized by the total annual feedstock rate to the plant as presented by the University of Kentucky report.
We’ll also define an annual REE recovery rate so the method can calculate the REE recovery cost:
# Some time quantities for convenience
hours_per_shift = 8 * pyunits.hr
shifts_per_day = 3 * pyunits.day**-1
operating_days_per_year = 336 * pyunits.day
m.fs.annual_operating_hours = Param(
initialize=hours_per_shift * shifts_per_day * operating_days_per_year,
mutable=True,
units=pyunits.hours / pyunits.a,
)
# Land leasing cost
m.fs.land_cost = Expression(
expr=0.303736
* 1e-6
* getattr(pyunits, "MUSD_" + CE_index_year)
/ pyunits.ton # leasing cost in flowsheet cost year basis
* pyunits.convert(
m.fs.leach_solid_feed.flow_mass[0], to_units=pyunits.ton / pyunits.hr
)
* m.fs.annual_operating_hours
* pyunits.a
)
# Annual recovery rate
m.fs.recovery_rate_per_year = Var(initialize=13.306, units=pyunits.kg / pyunits.yr)
m.fs.recovery_rate_per_year_constraint = Constraint(
expr=m.fs.recovery_rate_per_year
== pyunits.convert(
m.fs.roaster.flow_mass_product[0] * m.fs.annual_operating_hours,
to_units=pyunits.kg / pyunits.yr,
)
)
3.4 Build Plantwide Costs#
Now, let’s build the plantwide costs. This includes installation costs (Lang factor), overnight costs, and fixed and variable operating costs. The values set below are from the University of Kentucky report and are thus case-specific.
We can do this with one method call:
m.fs.costing.build_process_costs(
# arguments related to installation costs
Lang_factor=2.97,
# argument related to fixed O&M costs
fixed_OM=True, # calculate fixed O&M costs
labor_types=[
"skilled",
"unskilled",
"supervisor",
"maintenance",
"technician",
"engineer",
],
labor_rate=[24.98, 19.08, 30.39, 22.73, 21.97, 45.85], # USD/hr
labor_burden=25, # % fringe benefits
operators_per_shift=[4, 9, 2, 2, 2, 3],
hours_per_shift=hours_per_shift,
shifts_per_day=shifts_per_day,
operating_days_per_year=operating_days_per_year,
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 total owners costs
land_cost=m.fs.land_cost,
# arguments related to variable O&M costs
variable_OM=True, # calculate variable O&M costs
resources=[
"nonhazardous_solid_waste",
"power",
], # variable cost names
rates=[
m.fs.solid_waste,
m.fs.power,
], # variable cost rates
efficiency=0.80, # power usage efficiency, or fixed motor/distribution efficiency
waste=[
"nonhazardous_solid_waste",
], # waste is considered for the overnight costs
recovery_rate_per_year=m.fs.recovery_rate_per_year,
CE_index_year=CE_index_year,
)
3.5 Add Additional Costs#
Suppose we have an additional cost we want to include, such as initial fills of tank chemicals. We can use a variable create by the build_process_costs() method to do so. Note that these quantities are also scaled from the University of Kentucky report by their respective section flowrates using an exponent of 0.7, which is a common selection for units which scale by volume or volumetric flowrate:
# Define reagent fill costs as an other plant cost so framework adds this to TPC calculation
m.fs.costing.other_plant_costs.unfix()
m.fs.costing.other_plant_costs_eq = Constraint(
expr=(
m.fs.costing.other_plant_costs
== pyunits.convert(
1218.073
* pyunits.USD_2016 # Rougher Solvent Extraction
* (
m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["rougher_solex_aqueous_flow_vol"]
)
** 0.7
+ 48.723
* pyunits.USD_2016 # Cleaner Solvent Extraction
* (
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["cleaner_solex_aqueous_flow_vol"]
)
** 0.7
+ 182.711
* pyunits.USD_2016 # Solvent Extraction Wash and Saponification
* (
m.fs.precipitator.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["precipitator_solex_aqueous_flow_vol"]
)
** 0.7,
to_units=getattr(pyunits, "MUSD_" + CE_index_year),
)
)
)
4 Solving and Displaying Results#
4.1 Initialization and Solving#
It is always a good idea to initialize models by pre-calculating some variables before attempting to solve the entire model. This helps the solver start closer to the final solution and limits the risk of divergence.
The QGESSCosting() class has some built-in methods to initialize the costing equations:
QGESSCostingData.costing_initialization(m.fs.costing)
QGESSCostingData.initialize_fixed_OM_costs(m.fs.costing)
QGESSCostingData.initialize_variable_OM_costs(m.fs.costing)
Now, let’s solve the model:
solver = get_solver("ipopt_v2")
results = solver.solve(m, tee=True)
assert_optimal_termination(results)
Ipopt 3.13.2: linear_solver="ma57"
max_iter=200
nlp_scaling_method="gradient-based"
tol=1e-06
******************************************************************************
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 ma57.
Number of nonzeros in equality constraint Jacobian...: 4833
Number of nonzeros in inequality constraint Jacobian.: 0
Number of nonzeros in Lagrangian Hessian.............: 653
Total number of variables............................: 1533
variables with only lower bounds: 1159
variables with lower and upper bounds: 55
variables with only upper bounds: 0
Total number of equality constraints.................: 1533
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 2.25e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0
Reallocating memory for MA57: lfact (90511)
Reallocating memory for MA57: lfact (100853)
1 0.0000000e+00 2.25e+04 9.77e+01 -1.0 3.42e+03 - 5.74e-02 5.85e-04h 1
2r 0.0000000e+00 2.25e+04 9.99e+02 3.7 0.00e+00 - 0.00e+00 3.66e-07R 5
3r 0.0000000e+00 1.89e+04 2.95e+03 3.7 5.09e+05 - 3.54e-03 8.03e-04f 1
4r 0.0000000e+00 6.23e+03 8.34e+03 3.7 3.51e+05 - 2.05e-02 6.40e-03f 1
5r 0.0000000e+00 4.83e+03 1.43e+04 3.7 7.01e+04 - 2.59e-02 1.17e-02f 1
6r 0.0000000e+00 3.45e+03 2.22e+04 3.7 6.71e+04 - 1.16e-01 2.27e-02f 1
7r 0.0000000e+00 2.75e+03 2.77e+04 3.7 5.15e+04 - 1.54e-01 4.73e-02f 1
8r 0.0000000e+00 2.24e+03 2.95e+04 3.7 1.61e+03 - 2.00e-01 7.58e-02f 1
9r 0.0000000e+00 8.75e+02 1.93e+04 3.7 4.13e+02 - 1.82e-01 2.88e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
10r 0.0000000e+00 3.40e+02 2.65e+04 3.7 1.97e+02 - 7.30e-01 5.04e-01f 1
11r 0.0000000e+00 2.95e+02 2.12e+04 3.7 7.24e+01 - 6.52e-01 2.56e-01f 1
12r 0.0000000e+00 1.60e+02 1.17e+04 3.0 1.23e+02 - 8.61e-01 5.55e-01f 1
13r 0.0000000e+00 1.00e+02 5.11e+03 2.3 1.32e+01 2.0 8.19e-01 7.56e-01f 1
14r 0.0000000e+00 7.53e+01 4.80e+03 1.6 9.69e+00 1.5 5.81e-01 3.46e-01f 1
15r 0.0000000e+00 5.84e+01 2.13e+03 1.6 3.75e+00 1.9 4.04e-01 4.68e-01f 1
16r 0.0000000e+00 4.01e+01 2.27e+03 0.9 7.10e+00 1.5 5.33e-01 3.45e-01f 1
17r 0.0000000e+00 2.64e+01 1.75e+03 0.9 4.27e+01 1.0 3.24e-01 2.76e-01f 1
18r 0.0000000e+00 2.11e+01 1.59e+03 0.9 2.20e+00 2.3 6.66e-01 4.60e-01f 1
19r 0.0000000e+00 2.35e+01 1.49e+03 0.9 4.12e+00 1.8 5.22e-01 2.69e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
20r 0.0000000e+00 2.41e+01 1.36e+03 0.9 4.32e+01 1.4 1.54e-01 1.05e-01f 1
21r 0.0000000e+00 2.45e+01 1.42e+03 0.9 8.53e+00 0.9 2.68e-01 3.82e-02f 1
22r 0.0000000e+00 2.74e+01 7.09e+02 0.9 3.43e+00 1.3 3.65e-01 5.24e-01f 1
23r 0.0000000e+00 3.00e+01 5.92e+02 0.9 1.47e+00 1.7 6.29e-01 6.70e-01f 1
24r 0.0000000e+00 3.13e+01 5.09e+02 0.9 7.41e-01 2.2 1.00e+00 8.38e-01f 1
25r 0.0000000e+00 3.13e+01 3.95e+02 0.2 2.47e+00 1.7 2.94e-01 2.80e-01f 1
26r 0.0000000e+00 3.13e+01 7.92e+02 0.2 4.80e+00 1.2 2.28e-01 7.43e-02f 1
27r 0.0000000e+00 2.96e+01 9.04e+02 0.2 1.68e+01 0.7 1.54e-01 9.29e-02f 1
28r 0.0000000e+00 2.96e+01 2.50e+03 0.2 4.17e+00 1.2 6.33e-01 2.38e-02f 1
29r 0.0000000e+00 3.01e+01 2.53e+03 0.2 2.65e+01 0.7 4.59e-01 1.19e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
30r 0.0000000e+00 3.13e+01 8.29e+02 0.2 7.58e+00 1.1 4.04e-01 6.13e-01f 1
31r 0.0000000e+00 3.13e+01 1.78e+03 0.2 1.54e+00 1.5 7.73e-01 3.16e-02f 1
32r 0.0000000e+00 3.14e+01 1.33e+03 0.2 9.97e+00 1.1 1.66e-01 3.42e-01f 1
33r 0.0000000e+00 3.13e+01 1.42e+03 0.2 5.02e+00 0.6 2.19e-01 1.01e-01f 1
34r 0.0000000e+00 3.13e+01 3.15e+03 0.2 1.89e+00 1.0 9.12e-01 1.50e-01f 1
35r 0.0000000e+00 3.12e+01 1.36e+03 0.2 7.89e+00 0.5 9.78e-02 1.74e-01f 1
36r 0.0000000e+00 3.12e+01 3.77e+03 0.2 3.49e+00 1.0 6.53e-01 2.07e-01f 1
37r 0.0000000e+00 3.12e+01 4.47e+03 0.2 9.74e-01 1.4 1.86e-01 5.30e-01f 1
38r 0.0000000e+00 3.12e+01 3.20e+03 0.2 2.36e+00 0.9 2.18e-01 1.34e-01f 1
39r 0.0000000e+00 3.11e+01 2.09e+03 0.2 1.46e+01 0.4 2.29e-01 1.11e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
40r 0.0000000e+00 3.10e+01 2.15e+03 0.2 1.94e+00 0.9 9.88e-01 3.49e-01f 1
41r 0.0000000e+00 3.09e+01 5.81e+02 0.2 8.22e-01 1.3 1.00e+00 8.17e-01f 1
42r 0.0000000e+00 3.07e+01 6.00e+02 0.2 2.80e+00 0.8 8.32e-01 5.49e-01f 1
43r 0.0000000e+00 3.06e+01 1.20e+03 0.2 3.25e+00 0.3 1.00e+00 1.73e-01f 1
44r 0.0000000e+00 2.99e+01 7.65e+02 0.2 9.39e+00 -0.1 1.00e+00 2.97e-01f 1
45r 0.0000000e+00 2.87e+01 6.72e+02 0.2 2.58e+01 -0.6 2.42e-01 2.16e-01f 1
46r 0.0000000e+00 2.83e+01 1.22e+03 0.2 7.90e+02 - 2.58e-02 5.90e-03f 1
47r 0.0000000e+00 2.63e+01 3.92e+03 0.2 2.36e+02 - 1.47e-01 2.89e-02f 1
48r 0.0000000e+00 2.22e+01 2.62e+03 0.2 2.28e+02 - 3.82e-01 4.11e-02f 1
49r 0.0000000e+00 4.28e+00 2.23e+03 0.2 2.18e+02 - 2.00e-01 1.85e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
50r 0.0000000e+00 3.13e+00 1.51e+03 0.2 1.77e+02 - 9.20e-01 4.58e-02f 1
51r 0.0000000e+00 6.55e-01 7.98e+02 0.2 1.67e+02 - 1.00e+00 2.66e-01f 1
52r 0.0000000e+00 5.04e-01 7.74e+02 0.2 1.22e+02 - 2.56e-01 2.64e-01f 1
53r 0.0000000e+00 4.87e-01 6.43e+02 0.2 8.97e+01 - 1.73e-01 2.19e-01f 1
54r 0.0000000e+00 4.69e-01 2.86e+02 0.2 7.00e+01 - 8.16e-01 5.46e-01f 1
55r 0.0000000e+00 4.62e-01 7.87e+02 0.2 1.82e+00 0.7 8.80e-01 7.35e-01h 1
56r 0.0000000e+00 4.55e-01 6.67e+02 0.2 3.18e+01 - 1.27e-01 1.00e+00f 1
57r 0.0000000e+00 4.55e-01 4.95e+02 0.2 7.24e-01 - 1.00e+00 3.41e-01f 1
58r 0.0000000e+00 4.55e-01 2.02e+01 0.2 4.41e-01 - 1.00e+00 1.00e+00f 1
59r 0.0000000e+00 1.83e-01 1.17e+02 -0.5 1.68e+01 - 1.00e+00 8.27e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
60r 0.0000000e+00 8.43e-02 7.63e+01 -0.5 4.57e+01 - 1.00e+00 8.34e-01f 1
61r 0.0000000e+00 7.16e-02 1.16e+00 -0.5 7.60e+00 - 1.00e+00 1.00e+00f 1
62r 0.0000000e+00 3.35e-02 7.31e+01 -1.9 1.33e+01 - 9.47e-01 6.27e-01f 1
63r 0.0000000e+00 2.91e-02 3.26e+02 -1.9 3.59e+02 - 1.00e+00 1.57e-01f 1
64r 0.0000000e+00 2.35e-02 2.11e+02 -1.9 2.73e+02 - 3.05e-01 3.45e-01f 1
65r 0.0000000e+00 1.86e-02 3.93e+02 -1.9 1.01e+02 - 1.00e+00 2.18e-01f 1
66r 0.0000000e+00 7.75e-03 2.02e+02 -1.9 5.91e+00 - 1.00e+00 6.34e-01f 1
67r 0.0000000e+00 3.11e-03 2.54e+01 -1.9 7.20e-01 - 1.00e+00 9.17e-01f 1
68r 0.0000000e+00 2.75e-03 4.48e-02 -1.9 7.08e-02 - 1.00e+00 1.00e+00f 1
69r 0.0000000e+00 9.86e-04 1.09e+02 -4.2 3.06e+00 - 1.00e+00 5.99e-01f 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
70r 0.0000000e+00 5.97e-04 3.82e+02 -4.2 2.86e+00 - 1.00e+00 3.61e-01f 1
71r 0.0000000e+00 2.80e-04 2.67e+02 -4.2 1.22e-01 - 9.72e-01 5.12e-01f 1
72r 0.0000000e+00 1.48e-04 3.22e+02 -4.2 5.49e-02 - 9.94e-01 4.77e-01f 1
73r 0.0000000e+00 8.96e-05 2.69e+02 -4.2 2.87e-02 - 1.00e+00 4.22e-01f 1
74r 0.0000000e+00 4.55e-05 2.49e+02 -4.2 1.45e-02 - 1.00e+00 5.52e-01f 1
75r 0.0000000e+00 2.38e-05 1.90e+02 -4.2 3.20e-03 - 1.00e+00 6.04e-01f 1
Number of Iterations....: 75
(scaled) (unscaled)
Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00
Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00
Constraint violation....: 7.7024613021207799e-07 2.3795966897052903e-05
Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00
Overall NLP error.......: 7.7024613021207799e-07 2.3795966897052903e-05
Number of objective function evaluations = 81
Number of objective gradient evaluations = 4
Number of equality constraint evaluations = 81
Number of inequality constraint evaluations = 0
Number of equality constraint Jacobian evaluations = 77
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations = 75
Total CPU secs in IPOPT (w/o function evaluations) = 0.402
Total CPU secs in NLP function evaluations = 0.010
EXIT: Optimal Solution Found.
4.2 Reporting Cost Results#
Let’s call some built-in reporting method to view the cost results:
QGESSCostingData.report(m.fs.costing)
# TODO validate these results
# assert m.fs.costing.total_BEC.value == pytest.approx(0.52043, rel=1e-4)
# assert m.fs.costing.total_installation_cost.value == pytest.approx(1.0252, rel=1e-4)
# assert m.fs.costing.total_plant_cost.value == pytest.approx(1.5457, rel=1e-4)
# assert m.fs.costing.total_fixed_OM_cost.value == pytest.approx(6.8275, rel=1e-4)
# assert m.fs.costing.total_sales_revenue.value == pytest.approx(0, abs=1e-4)
# assert m.fs.costing.total_variable_OM_cost[0].value == pytest.approx(1.3656, rel=1e-4)
# assert m.fs.costing.plant_overhead_cost[0].value == pytest.approx(1.3655, rel=1e-4)
# assert value(m.fs.costing.cost_of_recovery) == pytest.approx(4.7736e07, rel=1e-4)
====================================================================================
costing
------------------------------------------------------------------------------------
Value
Plant Cost Units MUSD_2023
Total Plant Cost 1.5481
Total Bare Erected Cost 0.52124
Total Installation Cost 1.0268
Total Other Plant Costs 9.9460e-06
Total Fixed Operating & Maintenance Cost 6.8276
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 0.030962
Total Quality Assurance and Control Cost 0.38090
Total Sales, Patenting and Research Cost 2.5537e-07
Summation of Sales, Admin and Insurance Cost 0.77729
Total Admin Support and Labor Cost 0.76181
Total Property Taxes and Insurance Cost 0.015481
Total Other Fixed Costs 1.0000e-12
Total Variable Power Cost 2.1070e-05
Total Variable Waste Cost 2.5964e-10
Total Variable Chemicals Cost 0.0000
General Plant Overhead Cost 1.3655
Total Plant Overhead Cost, Including Maintenance & Quality Assurance 1.7774
Total Variable Operating & Maintenance Cost 1.3656
Total Land Cost 6.1234e-05
Total Sales Revenue Cost 5.1075e-05
Cost of Recovery (USD/kg REE) 4.9828e+07
====================================================================================
m.fs.costing.variable_operating_costs.display()
variable_operating_costs : Variable operating costs
Size=2, Index=fs._time*{nonhazardous_solid_waste, power}, Units=MUSD_2023/a
Key : Lower : Value : Upper : Fixed : Stale : Domain
(0.0, 'nonhazardous_solid_waste') : None : 2.596407709179655e-10 : None : False : False : Reals
(0.0, 'power') : None : 2.1070262144170174e-05 : None : False : False : Reals
QGESSCostingData.display_bare_erected_costs(m.fs.costing)
-----Bare Erected Costs (MUSD)-----
fs.leach.costing: 0.00048
fs.sl_sep1.costing: 0.00258
fs.leach_mixer.costing: 0.00568
fs.rougher_mixer.costing: 0.00220
fs.cleaner_mixer.costing: 0.00036
fs.leach_sx_mixer.costing: 0.00049
fs.precipitator.costing: 0.00022
fs.sl_sep2.costing: 0.00022
fs.roaster.costing: 0.04833
fs.leach_pump.costing: 0.01893
fs.leach_solution_heater.costing: 0.00022
fs.rougher_solex_tank.costing: 0.00029
fs.rougher_pump.costing: 0.00926
fs.rougher_solex_settler.costing: 0.35358
fs.cleaner_solex_tank.costing: 0.00022
fs.cleaner_pump.costing: 0.00692
fs.cleaner_solex_settler.costing: 0.06897
fs.precipitator_mixer.costing: 0.00047
fs.precipitator_pump.costing: 0.00185
QGESSCostingData.display_flowsheet_cost(m.fs.costing)
Total bare erected cost (MUSD): 0.521
Total overnight (installed) equipment cost: 1.548
Total annualized capital cost (MUSD): 0.177
Total annual fixed O&M cost (MUSD): 6.828
Total annual variable O&M cost (MUSD): 1.366
Total annual O&M cost (MUSD): 8.193
Total annual O&M cost per kg REE recovered (USD/kg): 48771318.341
Total annualized plant cost (MUSD): 8.371
Annual rate of recovery (kg/year): 0.168
Cost of recovery per kg REE recovered (USD/kg): 49827654.233