#####################################################################################################
# “PrOMMiS” was produced under the DOE Process Optimization and Modeling for Minerals Sustainability
# (“PrOMMiS”) initiative, and is copyright (c) 2023-2026 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory, et al. All rights reserved.
# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license information.
#####################################################################################################
r"""
University of Kentucky REE Processing Plant
===========================================
Authors: Marcus Holly, Brandon Paul, Douglas Allan
The University of Kentucky (UKy) rare earth element (REE) processing plant is designed to extract salable rare earth oxides
from domestic U.S. coal and coal byproducts. While this implementation of the plant does not take into account
all of the complexities and unit processes detailed in the 2019 report, it depicts the major phenomena
by utilizing a series of conventional REE extraction techniques,
including acid tank leaching, solvent extraction, precipitation, and product roasting.
Implementation
--------------
Figure 1 shows the process flow diagram for the simplified, steady-state UKy plant where the solid and liquid feeds are
sent to a leaching tank for initial processing; then the solids (with some retained liquid) are separated out as a filter
cake while the remaining liquid is sent to the solvent extraction process which is comprised of 2 distinct circuits.
In the rougher circuit, solutes in the aqueous phase are transferred to the organic phase, and a portion of the
depleted aqueous solution is recycled back to the leaching process while the remainder is sent to the cleaner circuit.
The degree to which components are transferred from one phase to the other is dependent upon the unit's partition coefficient for that particular component.
In the cleaner circuit, solutes in the organic phase are transferred to the aqueous phase, and a portion of the loaded
aqueous solution is recycled back to the rougher circuit while the remainder is sent to the precipitator. The precipitate
(with some retained liquid) is sent to the roaster where the product rare earth oxides are generated, and the liquid from
the precipitator is recycled back to the cleaner circuit.
.. figure:: ../tutorials/uky_flowsheet.png
:width: 800
:align: center
University of Kentucky flowsheet
Degrees of Freedom
------------------
The following variables must be specified by the user to run the UKy flowsheet:
* liquid feed volumetric flow rate and component concentrations
* solid feed mass flow and component mass fractions
* volume of leach tank(s)
* partition coefficients for each solvent extraction unit
* flow rate and component concentrations for organic make-up streams and HCl feeds
* liquid recovery fraction for solid-liquid separators
* precipitator inlet temperature
* roaster inlet temperature and pressure, outlet temperature, and pressure drop
* roaster gas and moisture molar flows, vapor component mole fractions, and oxide recovery fraction
* split fractions for each recycle loop
Default Flowsheet Specifications
--------------------------------
===================================================================== ============ ============================
Description Value Units
===================================================================== ============ ============================
Leaching
Tank volume 100 :math:`\text{gal}`
Liquid feed volumetric flow 100 :math:`\text{L/hr}`
Liquid feed H concentration 277 :math:`\text{mg/L}`
Liquid feed HSO4 concentration 25025 :math:`\text{mg/L}`
Liquid feed SO4 concentration 915 :math:`\text{mg/L}`
Liquid feed REE and contaminant concentrations 1e-10 :math:`\text{mg/L}`
Solid feed mass flow 22.68 :math:`\text{kg/hr}`
Solid feed inerts mass fraction 0.6952 :math:`\text{dimensionless}`
Solid feed Al2O3 mass fraction 0.237 :math:`\text{dimensionless}`
Solid feed Fe2O3 mass fraction 0.0642 :math:`\text{dimensionless}`
Solid feed CaO mass fraction 0.00331 :math:`\text{dimensionless}`
Solid feed Sc2O3 mass fraction 2.8e-5 :math:`\text{dimensionless}`
Solid feed Y2O3 mass fraction 3.3e-5 :math:`\text{dimensionless}`
Solid feed La2O3 mass fraction 6.8e-5 :math:`\text{dimensionless}`
Solid feed Ce2O3 mass fraction 1.6e-4 :math:`\text{dimensionless}`
Solid feed Pr2O3 mass fraction 1.7e-5 :math:`\text{dimensionless}`
Solid feed Nd2O3 mass fraction 6.8e-5 :math:`\text{dimensionless}`
Solid feed Sm2O3 mass fraction 1.5e-5 :math:`\text{dimensionless}`
Solid feed Gd2O3 mass fraction 1.0e-5 :math:`\text{dimensionless}`
Solid feed Dy2O3 mass fraction 7.5e-6 :math:`\text{dimensionless}`
Solvent Extraction Rougher
Loading section organic feed volumetric flow 12.89 :math:`\text{L/hr}`
Organic make-up REE and contaminant concentrations 1e-7 :math:`\text{mg/L}`
Scrubbing section acid feed volumetric flow 0.09 :math:`\text{L/hr}`
Scrubbing section acid feed H concentration 10.36 :math:`\text{mg/L}`
Scrubbing section acid feed Cl concentration 359.64 :math:`\text{mg/L}`
Scrubbing section acid feed REE and contaminant concentrations 1e-7 :math:`\text{mg/L}`
Stripping section acid feed volumetric flow 0.09 :math:`\text{L/hr}`
Stripping section acid feed H concentration 41.44 :math:`\text{mg/L}`
Stripping section acid feed Cl concentration 1438.56 :math:`\text{mg/L}`
Stripping section acid feed REE and contaminant concentrations 1e-7 :math:`\text{mg/L}`
Solvent Extraction Cleaner
Loading section organic feed volumetric flow 60.33 :math:`\text{L/hr}`
Organic make-up REE and contaminant concentrations 1e-7 :math:`\text{mg/L}`
Stripping section acid feed volumetric flow 0.09 :math:`\text{L/hr}`
Stripping section acid feed H concentration 41.44 :math:`\text{mg/L}`
Stripping section acid feed Cl concentration 1438.56 :math:`\text{mg/L}`
Stripping section acid feed REE and contaminant concentrations 1e-7 :math:`\text{mg/L}`
Precipitator
Inlet temperature 348.15 :math:`\text{K}`
Roaster
Pressure drop 0 :math:`\text{Pa}`
Gas inlet temperature 348.15 :math:`\text{K}`
Gas outlet temperature 873.15 :math:`\text{K}`
Gas inlet pressure 101325 :math:`\text{Pa}`
Gas inlet molar flow 0.00781 :math:`\text{mol/s}`
Gas inlet O2 mole fraction 0.1118 :math:`\text{dimensionless}`
Gas inlet H2O mole fraction 0.1005 :math:`\text{dimensionless}`
Gas inlet CO2 mole fraction 0.0431 :math:`\text{dimensionless}`
Gas inlet N2 mole fraction 0.7446 :math:`\text{dimensionless}`
Moisture inlet molar flow 6.75e-4 :math:`\text{mol/s}`
Oxide recovery fraction 0.95 :math:`\text{dimensionless}`
Separators
Leaching solid-liquid separator liquid recovery fraction 0.7 :math:`\text{dimensionless}`
Solvent extraction rougher load recycle split fraction 0.9 :math:`\text{dimensionless}`
Solvent extraction rougher scrub recycle split fraction 0.9 :math:`\text{dimensionless}`
Solvent extraction rougher organic recycle split fraction 0.9 :math:`\text{dimensionless}`
Solvent extraction cleaner organic recycle split fraction 0.9 :math:`\text{dimensionless}`
Precipitator solid-liquid separator liquid recovery fraction 0.7 :math:`\text{dimensionless}`
Precipitator solid-liquid separator liquid recycle split fraction 0.9 :math:`\text{dimensionless}`
===================================================================== ============ ============================
Costing
-------
Unit model costing in this flowsheet is based on the commercial scale unit model parameters provided in Table 4-28 :math:`^1`.
The reference cost and capacity parameter data from [1] are at commercial scale. As this flowsheet is at the pilot scale, some
of the unit model capacity parameters have been scaled down accordingly by unit feed rate.
References:
[1] Keim, Steven Anthony and Naumann, Hans. "Production of Salable Rare Earths Products from Coal and Coal Byproducts
in the U.S. Using Advanced Separation Processes (Final Technical Report)." , Sep. 2019. https://doi.org/10.2172/1569277
"""
import logging
import idaes.logger as idaeslog
from idaes.core import (
FlowDirection,
FlowsheetBlock,
MaterialBalanceType,
MomentumBalanceType,
UnitModelBlock,
UnitModelBlockData,
UnitModelCostingBlock,
)
from idaes.core.initialization import BlockTriangularizationInitializer
from idaes.core.scaling import CustomScalerBase, ConstraintScalingScheme
from idaes.core.scaling.util import get_scaling_factor, set_scaling_factor
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
from idaes.models.properties.modular_properties.base.generic_property import (
GenericParameterBlock,
ModularPropertiesScaler,
)
from idaes.models.unit_models.feed import Feed, FeedInitializer
from idaes.models.unit_models.mixer import (
Mixer,
MixerInitializer,
MixingType,
MomentumMixingType,
)
from idaes.models.unit_models.product import Product, ProductInitializer
from idaes.models.unit_models.separator import (
EnergySplittingType,
Separator,
SeparatorInitializer,
SplittingType,
)
from idaes.models.unit_models.solid_liquid import SLSeparator
from idaes.models_extra.power_generation.properties.natural_gas_PR import (
EosType,
get_prop,
)
from pyomo.common.collections import ComponentMap
from pyomo.contrib.incidence_analysis import (
solve_strongly_connected_components,
)
from pyomo.environ import (
Block,
ConcreteModel,
Constraint,
Expression,
Objective,
Param,
Set,
TransformationFactory,
Var,
check_optimal_termination,
units,
value,
)
from pyomo.network import Arc, SequentialDecomposition
from pyomo.util.subsystems import create_subsystem_block
from prommis.leaching.leach_reactions import CoalRefuseLeachingReactionParameterBlock
from prommis.properties.coal_refuse_properties import CoalRefuseParameters
from prommis.properties.sulfuric_acid_leaching_properties import (
SulfuricAcidLeachingParameters,
)
from prommis.leaching.leach_train import LeachingTrain, LeachingTrainInitializer
from prommis.properties import HClStrippingParameterBlock
from prommis.properties.hcl_stripping_properties import HClStrippingPropertiesScaler
from prommis.precipitate.precipitate_solids_properties import PrecipitateParameters
from prommis.precipitate.precipitator import Precipitator
from prommis.roasting.ree_oxalate_roaster import REEOxalateRoaster
from prommis.solvent_extraction.ree_og_distribution import REESolExOgParameters
from prommis.solvent_extraction.solvent_extraction import (
SolventExtraction,
SolventExtractionInitializer,
)
from prommis.properties.translator_hcl_leach import TranslatorHClLeach
from prommis.solvent_extraction.solvent_extraction_reaction_package import (
SolventExtractionReactions,
)
from prommis.uky.costing.costing_dictionaries import load_REE_costing_dictionary
from prommis.uky.costing.ree_plant_capcost import QGESSCosting, QGESSCostingData
_log = idaeslog.getLogger(__name__)
# Epsilon represents near-zero component concentrations
eps = 1e-8 * units.mg / units.L
[docs]
def main():
"""
Run the flowsheet by calling the appropriate functions in series.
"""
m = build()
set_operating_conditions(m)
set_scaling(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, tee=True)
# 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, tee=True)
if not check_optimal_termination(results):
raise RuntimeError(
"Solver failed to terminate with an optimal solution. Please check the solver logs for more details"
)
add_result_expressions(m)
display_results(m)
add_costing(m)
initialize_costing(m)
# diagnostics, initialize, and solve
dt = DiagnosticsToolbox(m)
dt.assert_no_structural_warnings()
solve_system(m, tee=True)
dt.assert_no_numerical_warnings()
display_costing(m)
return m, results
[docs]
def build():
"""
Build and connect the unit model blocks present in the University of Kentucky REE processing plant.
"""
m = ConcreteModel()
m.fs = FlowsheetBlock(dynamic=False)
# Leaching property and unit models
m.fs.leach_soln = SulfuricAcidLeachingParameters()
m.fs.coal = CoalRefuseParameters()
m.fs.leach_rxns = CoalRefuseLeachingReactionParameterBlock()
m.fs.HCl_stripping_params = HClStrippingParameterBlock()
m.fs.leach = LeachingTrain(
number_of_tanks=2,
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,
)
m.fs.sl_sep1 = SLSeparator(
solid_property_package=m.fs.coal,
liquid_property_package=m.fs.leach_soln,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.scrubber_HCl_leach_translator = TranslatorHClLeach(
inlet_property_package=m.fs.HCl_stripping_params,
outlet_property_package=m.fs.leach_soln,
)
m.fs.leach_mixer = Mixer(
property_package=m.fs.leach_soln,
num_inlets=3,
inlet_list=["load_recycle", "scrub_recycle", "feed"],
material_balance_type=MaterialBalanceType.componentTotal,
energy_mixing_type=MixingType.none,
momentum_mixing_type=MomentumMixingType.none,
)
m.fs.leach_liquid_feed = Feed(property_package=m.fs.leach_soln)
m.fs.leach_solid_feed = Feed(property_package=m.fs.coal)
m.fs.leach_filter_cake = Product(property_package=m.fs.coal)
m.fs.leach_filter_cake_liquid = Product(property_package=m.fs.leach_soln)
# ----------------------------------------------------------------------------------------------------------------
# Solvent extraction property, reaction and unit models
m.fs.prop_o = REESolExOgParameters()
m.fs.reaxn = SolventExtractionReactions()
m.fs.rougher_org_make_up = Feed(property_package=m.fs.prop_o)
m.fs.solex_rougher_load = SolventExtraction(
number_of_finite_elements=3,
dynamic=False,
aqueous_stream={
"property_package": m.fs.leach_soln,
"flow_direction": FlowDirection.forward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
organic_stream={
"property_package": m.fs.prop_o,
"flow_direction": FlowDirection.backward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
heterogeneous_reaction_package=m.fs.reaxn,
has_holdup=False,
create_hydrostatic_pressure_terms=False,
)
m.fs.acid_feed1 = Feed(property_package=m.fs.HCl_stripping_params)
m.fs.solex_rougher_scrub = SolventExtraction(
number_of_finite_elements=1,
dynamic=False,
aqueous_stream={
"property_package": m.fs.HCl_stripping_params,
"flow_direction": FlowDirection.backward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
organic_stream={
"property_package": m.fs.prop_o,
"flow_direction": FlowDirection.forward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
heterogeneous_reaction_package=m.fs.reaxn,
has_holdup=False,
create_hydrostatic_pressure_terms=False,
)
m.fs.acid_feed2 = Feed(property_package=m.fs.HCl_stripping_params)
m.fs.solex_rougher_strip = SolventExtraction(
number_of_finite_elements=2,
dynamic=False,
aqueous_stream={
"property_package": m.fs.HCl_stripping_params,
"flow_direction": FlowDirection.backward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
organic_stream={
"property_package": m.fs.prop_o,
"flow_direction": FlowDirection.forward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
heterogeneous_reaction_package=m.fs.reaxn,
has_holdup=False,
create_hydrostatic_pressure_terms=False,
)
m.fs.rougher_sep = Separator(
property_package=m.fs.prop_o,
outlet_list=["recycle", "purge"],
split_basis=SplittingType.totalFlow,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.rougher_mixer = Mixer(
property_package=m.fs.prop_o,
num_inlets=2,
inlet_list=["make_up", "recycle"],
material_balance_type=MaterialBalanceType.componentTotal,
energy_mixing_type=MixingType.none,
momentum_mixing_type=MomentumMixingType.none,
)
m.fs.load_sep = Separator(
property_package=m.fs.leach_soln,
outlet_list=["recycle", "purge"],
split_basis=SplittingType.totalFlow,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.scrub_sep = Separator(
property_package=m.fs.HCl_stripping_params,
outlet_list=["recycle", "purge"],
split_basis=SplittingType.totalFlow,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.rougher_organic_purge = Product(property_package=m.fs.prop_o)
m.fs.solex_cleaner_load = SolventExtraction(
number_of_finite_elements=3,
dynamic=False,
aqueous_stream={
"property_package": m.fs.HCl_stripping_params,
"flow_direction": FlowDirection.forward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
organic_stream={
"property_package": m.fs.prop_o,
"flow_direction": FlowDirection.backward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
heterogeneous_reaction_package=m.fs.reaxn,
has_holdup=False,
create_hydrostatic_pressure_terms=False,
)
m.fs.solex_cleaner_strip = SolventExtraction(
number_of_finite_elements=3,
dynamic=False,
aqueous_stream={
"property_package": m.fs.HCl_stripping_params,
"flow_direction": FlowDirection.backward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
organic_stream={
"property_package": m.fs.prop_o,
"flow_direction": FlowDirection.forward,
"has_energy_balance": False,
"has_pressure_balance": False,
},
heterogeneous_reaction_package=m.fs.reaxn,
has_holdup=False,
create_hydrostatic_pressure_terms=False,
)
m.fs.cleaner_org_make_up = Feed(property_package=m.fs.prop_o)
m.fs.cleaner_mixer = Mixer(
property_package=m.fs.prop_o,
num_inlets=2,
inlet_list=["make_up", "recycle"],
material_balance_type=MaterialBalanceType.componentTotal,
energy_mixing_type=MixingType.none,
momentum_mixing_type=MomentumMixingType.none,
)
m.fs.cleaner_sep = Separator(
property_package=m.fs.prop_o,
outlet_list=["recycle", "purge"],
split_basis=SplittingType.totalFlow,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.cleaner_HCl_leach_translator = TranslatorHClLeach(
inlet_property_package=m.fs.HCl_stripping_params,
outlet_property_package=m.fs.leach_soln,
)
m.fs.leach_sx_mixer = Mixer(
property_package=m.fs.leach_soln,
num_inlets=2,
inlet_list=["leach", "cleaner"],
material_balance_type=MaterialBalanceType.componentTotal,
energy_mixing_type=MixingType.none,
momentum_mixing_type=MomentumMixingType.none,
)
m.fs.acid_feed3 = Feed(property_package=m.fs.HCl_stripping_params)
m.fs.cleaner_organic_purge = Product(property_package=m.fs.prop_o)
# --------------------------------------------------------------------------------------------------------------
# Precipitation property and unit models
m.fs.properties_solid = PrecipitateParameters()
m.fs.precipitator = Precipitator(
property_package_aqueous=m.fs.HCl_stripping_params,
property_package_precipitate=m.fs.properties_solid,
make_volume_balance_constraint=False,
)
m.fs.sl_sep2 = SLSeparator(
solid_property_package=m.fs.properties_solid,
liquid_property_package=m.fs.HCl_stripping_params,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.precip_sep = Separator(
property_package=m.fs.HCl_stripping_params,
outlet_list=["recycle", "purge"],
split_basis=SplittingType.totalFlow,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.precip_sx_mixer = Mixer(
property_package=m.fs.HCl_stripping_params,
num_inlets=2,
inlet_list=["precip", "rougher"],
material_balance_type=MaterialBalanceType.componentTotal,
energy_mixing_type=MixingType.none,
momentum_mixing_type=MomentumMixingType.none,
)
m.fs.precip_purge = Product(property_package=m.fs.HCl_stripping_params)
# -----------------------------------------------------------------------------------------------------------------
# Roasting property and unit models
gas_species = {"O2", "H2O", "CO2", "N2"}
m.fs.prop_gas = GenericParameterBlock(
**get_prop(gas_species, ["Vap"], EosType.IDEAL),
doc="gas property",
)
m.fs.prop_solid = PrecipitateParameters()
m.fs.roaster = REEOxalateRoaster(
property_package_gas=m.fs.prop_gas,
property_package_precipitate_solid=m.fs.prop_solid,
property_package_precipitate_liquid=m.fs.HCl_stripping_params,
has_holdup=False,
has_heat_transfer=True,
has_pressure_change=True,
)
# -----------------------------------------------------------------------------------------------------------------
# UKy flowsheet connections
m.fs.leaching_sol_feed = Arc(
source=m.fs.leach_solid_feed.outlet, destination=m.fs.leach.solid_inlet
)
m.fs.leaching_liq_feed = Arc(
source=m.fs.leach_liquid_feed.outlet, destination=m.fs.leach_mixer.feed
)
m.fs.leaching_feed_mixture = Arc(
source=m.fs.leach_mixer.outlet, destination=m.fs.leach.liquid_inlet
)
m.fs.leaching_solid_outlet = Arc(
source=m.fs.leach.solid_outlet, destination=m.fs.sl_sep1.solid_inlet
)
m.fs.leaching_liquid_outlet = Arc(
source=m.fs.leach.liquid_outlet, destination=m.fs.sl_sep1.liquid_inlet
)
m.fs.sl_sep1_solid_outlet = Arc(
source=m.fs.sl_sep1.solid_outlet, destination=m.fs.leach_filter_cake.inlet
)
m.fs.sl_sep1_retained_liquid_outlet = Arc(
source=m.fs.sl_sep1.retained_liquid_outlet,
destination=m.fs.leach_filter_cake_liquid.inlet,
)
m.fs.sl_sep1_liquid_outlet = Arc(
source=m.fs.sl_sep1.recovered_liquid_outlet,
destination=m.fs.leach_sx_mixer.leach,
)
m.fs.sx_rougher_load_aq_feed = Arc(
source=m.fs.leach_sx_mixer.outlet,
destination=m.fs.solex_rougher_load.aqueous_inlet,
)
m.fs.sx_rougher_org_feed = Arc(
source=m.fs.rougher_org_make_up.outlet, destination=m.fs.rougher_mixer.make_up
)
m.fs.sx_rougher_mixed_org_recycle = Arc(
source=m.fs.rougher_mixer.outlet,
destination=m.fs.solex_rougher_load.organic_inlet,
)
m.fs.sx_rougher_load_aq_outlet = Arc(
source=m.fs.solex_rougher_load.aqueous_outlet,
destination=m.fs.load_sep.inlet,
)
m.fs.sx_rougher_load_aq_recycle = Arc(
source=m.fs.load_sep.recycle, destination=m.fs.leach_mixer.load_recycle
)
m.fs.sx_rougher_load_org_outlet = Arc(
source=m.fs.solex_rougher_load.organic_outlet,
destination=m.fs.solex_rougher_scrub.organic_inlet,
)
m.fs.sx_rougher_scrub_acid_feed = Arc(
source=m.fs.acid_feed1.outlet,
destination=m.fs.solex_rougher_scrub.aqueous_inlet,
)
m.fs.sx_rougher_scrub_aq_outlet = Arc(
source=m.fs.solex_rougher_scrub.aqueous_outlet,
destination=m.fs.scrub_sep.inlet,
)
m.fs.sx_rougher_scrub_aq_translator = Arc(
source=m.fs.scrub_sep.recycle,
destination=m.fs.scrubber_HCl_leach_translator.inlet,
)
m.fs.translator_scrub_recycle = Arc(
source=m.fs.scrubber_HCl_leach_translator.outlet,
destination=m.fs.leach_mixer.scrub_recycle,
)
m.fs.sx_rougher_scrub_org_outlet = Arc(
source=m.fs.solex_rougher_scrub.organic_outlet,
destination=m.fs.solex_rougher_strip.organic_inlet,
)
m.fs.sx_rougher_strip_acid_feed = Arc(
source=m.fs.acid_feed2.outlet,
destination=m.fs.solex_rougher_strip.aqueous_inlet,
)
m.fs.sx_rougher_strip_org_outlet = Arc(
source=m.fs.solex_rougher_strip.organic_outlet,
destination=m.fs.rougher_sep.inlet,
)
m.fs.sx_rougher_strip_org_purge = Arc(
source=m.fs.rougher_sep.purge, destination=m.fs.rougher_organic_purge.inlet
)
m.fs.sx_rougher_strip_org_recycle = Arc(
source=m.fs.rougher_sep.recycle, destination=m.fs.rougher_mixer.recycle
)
m.fs.sx_rougher_strip_aq_outlet = Arc(
source=m.fs.solex_rougher_strip.aqueous_outlet,
destination=m.fs.precip_sx_mixer.rougher,
)
m.fs.sx_cleaner_load_aq_feed = Arc(
source=m.fs.precip_sx_mixer.outlet,
destination=m.fs.solex_cleaner_load.aqueous_inlet,
)
m.fs.sx_cleaner_org_feed = Arc(
source=m.fs.cleaner_org_make_up.outlet, destination=m.fs.cleaner_mixer.make_up
)
m.fs.sx_cleaner_mixed_org_recycle = Arc(
source=m.fs.cleaner_mixer.outlet,
destination=m.fs.solex_cleaner_load.organic_inlet,
)
m.fs.sx_cleaner_load_aq_outlet_translator = Arc(
source=m.fs.solex_cleaner_load.aqueous_outlet,
destination=m.fs.cleaner_HCl_leach_translator.inlet,
)
m.fs.sx_cleaner_load_translator_leach_sx_mixer = Arc(
source=m.fs.cleaner_HCl_leach_translator.outlet,
destination=m.fs.leach_sx_mixer.cleaner,
)
m.fs.sx_cleaner_strip_acid_feed = Arc(
source=m.fs.acid_feed3.outlet,
destination=m.fs.solex_cleaner_strip.aqueous_inlet,
)
m.fs.sx_cleaner_load_org_outlet = Arc(
source=m.fs.solex_cleaner_load.organic_outlet,
destination=m.fs.solex_cleaner_strip.organic_inlet,
)
m.fs.sx_cleaner_strip_org_outlet = Arc(
source=m.fs.solex_cleaner_strip.organic_outlet,
destination=m.fs.cleaner_sep.inlet,
)
m.fs.sx_cleaner_strip_org_purge = Arc(
source=m.fs.cleaner_sep.purge, destination=m.fs.cleaner_organic_purge.inlet
)
m.fs.sx_cleaner_strip_org_recycle = Arc(
source=m.fs.cleaner_sep.recycle, destination=m.fs.cleaner_mixer.recycle
)
m.fs.sx_cleaner_strip_aq_precip = Arc(
source=m.fs.solex_cleaner_strip.aqueous_outlet,
destination=m.fs.precipitator.aqueous_inlet,
)
m.fs.precip_solid_outlet = Arc(
source=m.fs.precipitator.precipitate_outlet,
destination=m.fs.sl_sep2.solid_inlet,
)
m.fs.precip_aq_sl_sep2 = Arc(
source=m.fs.precipitator.aqueous_outlet,
destination=m.fs.sl_sep2.liquid_inlet,
)
m.fs.sl_sep2_solid_outlet = Arc(
source=m.fs.sl_sep2.solid_outlet, destination=m.fs.roaster.solid_inlet
)
m.fs.sl_sep2_liquid_outlet = Arc(
source=m.fs.sl_sep2.recovered_liquid_outlet, destination=m.fs.precip_sep.inlet
)
m.fs.sl_sep2_retained_liquid_roaster = Arc(
source=m.fs.sl_sep2.retained_liquid_outlet,
destination=m.fs.roaster.liquid_inlet,
)
m.fs.sl_sep2_aq_purge = Arc(
source=m.fs.precip_sep.purge,
destination=m.fs.precip_purge.inlet,
)
m.fs.sl_sep2_aq_recycle = Arc(
source=m.fs.precip_sep.recycle,
destination=m.fs.precip_sx_mixer.precip,
)
TransformationFactory("network.expand_arcs").apply_to(m)
return m
[docs]
def set_scaling(m):
"""
Set the scaling factors to improve solver performance.
Args:
m: pyomo model
"""
# Changing the default scaling factor in the class dictionary
# applies this scaling factor globally. This sort of global
# mutation is potentially dangerous, but we'll use it here until
# there is a better way to set global default scaling factors
HClStrippingPropertiesScaler.DEFAULT_SCALING_FACTORS["flow_vol"] = 1
HClStrippingPropertiesScaler.DEFAULT_SCALING_FACTORS["conc_mass_comp[H]"] = 1e-3
HClStrippingPropertiesScaler.DEFAULT_SCALING_FACTORS["conc_mass_comp[Cl]"] = 1e-5
ModularPropertiesScaler.DEFAULT_SCALING_FACTORS["flow_mol_phase"] = 1 / 0.00781
# Also use global mutation to change the max and min scaling factors
# allowed from objects derived from CustomScalerBase, i.e., all the
# model scaler objects
CustomScalerBase.CONFIG["max_variable_scaling_factor"] = 1e20
CustomScalerBase.CONFIG["max_constraint_scaling_factor"] = 1e20
CustomScalerBase.CONFIG["max_expression_scaling_hint"] = 1e20
CustomScalerBase.CONFIG["min_variable_scaling_factor"] = 1e-20
CustomScalerBase.CONFIG["min_constraint_scaling_factor"] = 1e-20
CustomScalerBase.CONFIG["min_expression_scaling_hint"] = 1e-20
csb = CustomScalerBase()
for blk in m.fs.component_data_objects(ctype=Block, descend_into=False):
# if blk.parent_block() is m.fs:
if isinstance(blk, UnitModelBlockData):
if hasattr(blk, "default_scaler") and blk.default_scaler is not None:
print(f"Scaling {blk.name}")
scaler = blk.default_scaler()
scaler.scale_model(blk)
else:
print(f"No default scaler for unit model {blk.name}")
elif "_expanded" in blk.name:
print(f"Scaling {blk.name}")
# Expanded arc block
for con in blk.component_data_objects(Constraint):
csb.scale_constraint_by_nominal_value(
con, scheme=ConstraintScalingScheme.inverseMaximum
)
[docs]
def set_operating_conditions(m):
"""
Set the operating conditions of the flowsheet such that the degrees of freedom are zero.
Args:
m: pyomo model
"""
# Constants
# Assume a 20% volume-by-volume ratio
dosage = 20 / 100
dehpa_conc = 975.8e3 * dosage * units.mg / units.L
kerosene_conc = 8.2e5 * units.mg / units.L
Temp_room = 303 * units.K
P_atm = 101235 * units.Pa
m.fs.leach_liquid_feed.properties[0.0].pressure.fix(P_atm)
m.fs.leach_liquid_feed.properties[0.0].temperature.fix(Temp_room)
# NOTE: This is an arbitrary lower bound set by the optimization - shouldn't go lower
m.fs.leach_liquid_feed.flow_vol.fix(100 * units.L / units.hour)
m.fs.leach_liquid_feed.conc_mass_comp.fix(1e-10 * units.mg / units.L)
m.fs.leach_liquid_feed.conc_mass_comp[0, "H2O"].fix(1e6 * units.mg / units.L)
m.fs.leach_liquid_feed.conc_mass_comp[0, "H"].fix(277 * units.mg / units.L)
m.fs.leach_liquid_feed.conc_mass_comp[0, "HSO4"].fix(25025 * units.mg / units.L)
m.fs.leach_liquid_feed.conc_mass_comp[0, "SO4"].fix(915 * units.mg / units.L)
m.fs.leach_solid_feed.flow_mass.fix(22.68 * units.kg / units.hour)
m.fs.leach_solid_feed.mass_frac_comp[0, "inerts"].fix(0.6952 * units.kg / units.kg)
m.fs.leach_solid_feed.mass_frac_comp[0, "Al2O3"].fix(0.237 * units.kg / units.kg)
m.fs.leach_solid_feed.mass_frac_comp[0, "Fe2O3"].fix(0.0642 * units.kg / units.kg)
m.fs.leach_solid_feed.mass_frac_comp[0, "CaO"].fix(3.31e-3 * units.kg / units.kg)
m.fs.leach_solid_feed.mass_frac_comp[0, "Sc2O3"].fix(
2.77966e-05 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "Y2O3"].fix(
3.28653e-05 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "La2O3"].fix(
6.77769e-05 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "Ce2O3"].fix(
0.000156161 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "Pr2O3"].fix(
1.71438e-05 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "Nd2O3"].fix(
6.76618e-05 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "Sm2O3"].fix(
1.47926e-05 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "Gd2O3"].fix(
1.0405e-05 * units.kg / units.kg
)
m.fs.leach_solid_feed.mass_frac_comp[0, "Dy2O3"].fix(
7.54827e-06 * units.kg / units.kg
)
m.fs.leach.volume.fix(100 * units.gallon)
# Fix all temperatures and pressure
m.fs.scrubber_HCl_leach_translator.outlet.temperature.fix(Temp_room)
m.fs.scrubber_HCl_leach_translator.outlet.pressure.fix(P_atm)
m.fs.cleaner_HCl_leach_translator.outlet.temperature.fix(Temp_room)
m.fs.cleaner_HCl_leach_translator.outlet.pressure.fix(P_atm)
m.fs.solex_rougher_load.mscontactor.aqueous[:, :].temperature.fix(Temp_room)
m.fs.solex_rougher_load.mscontactor.aqueous[:, :].pressure.fix(P_atm)
m.fs.solex_rougher_load.mscontactor.organic[:, :].temperature.fix(Temp_room)
m.fs.solex_rougher_load.mscontactor.organic[:, :].pressure.fix(P_atm)
m.fs.solex_rougher_scrub.mscontactor.organic[:, :].temperature.fix(Temp_room)
m.fs.solex_rougher_scrub.mscontactor.organic[:, :].pressure.fix(P_atm)
m.fs.solex_rougher_strip.mscontactor.organic[:, :].temperature.fix(Temp_room)
m.fs.solex_rougher_strip.mscontactor.organic[:, :].pressure.fix(P_atm)
m.fs.solex_cleaner_load.mscontactor.organic[:, :].temperature.fix(Temp_room)
m.fs.solex_cleaner_load.mscontactor.organic[:, :].pressure.fix(P_atm)
m.fs.solex_cleaner_strip.mscontactor.organic[:, :].temperature.fix(Temp_room)
m.fs.solex_cleaner_strip.mscontactor.organic[:, :].pressure.fix(P_atm)
m.fs.cleaner_sep.recycle_state[0.0].temperature.fix(Temp_room)
m.fs.cleaner_sep.recycle_state[0.0].pressure.fix(P_atm)
m.fs.cleaner_sep.purge_state[0.0].temperature.fix(Temp_room)
m.fs.cleaner_sep.purge_state[0.0].pressure.fix(P_atm)
m.fs.leach.mscontactor.liquid[0.0, 2].temperature.fix(Temp_room)
m.fs.leach.mscontactor.liquid[0.0, 2].pressure.fix(P_atm)
m.fs.leach_mixer.mixed_state[0.0].pressure.fix(P_atm)
m.fs.leach_mixer.mixed_state[0.0].temperature.fix(Temp_room)
m.fs.load_sep.recycle_state[0.0].pressure.fix(P_atm)
m.fs.load_sep.recycle_state[0.0].temperature.fix(Temp_room)
m.fs.leach_sx_mixer.mixed_state[0.0].pressure.fix(P_atm)
m.fs.leach_sx_mixer.mixed_state[0.0].temperature.fix(Temp_room)
m.fs.load_sep.split_fraction[:, "recycle"].fix(0.9)
m.fs.scrub_sep.split_fraction[:, "recycle"].fix(0.9)
m.fs.rougher_org_make_up.flow_vol.fix(12.89)
m.fs.rougher_org_make_up.properties[0.0].pressure.fix(P_atm)
m.fs.rougher_org_make_up.properties[0.0].temperature.fix(Temp_room)
m.fs.rougher_mixer.mixed_state[0.0].pressure.fix(P_atm)
m.fs.rougher_mixer.mixed_state[0.0].temperature.fix(Temp_room)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Al_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Ca_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Fe_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Sc_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Y_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "La_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Ce_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Pr_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Nd_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Sm_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Gd_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Dy_o"].fix(eps)
m.fs.rougher_org_make_up.conc_mass_comp[0, "DEHPA"].fix(dehpa_conc)
m.fs.rougher_org_make_up.conc_mass_comp[0, "Kerosene"].fix(kerosene_conc)
# 0.974M HCl; pH = 0.01
m.fs.acid_feed1.flow_vol.fix(0.1 * units.L / units.hr)
m.fs.acid_feed1.conc_mass_comp[0, "H2O"].fix(1000000)
m.fs.acid_feed1.conc_mass_comp[0, "H"].fix(981.44 * units.mg / units.L)
m.fs.acid_feed1.conc_mass_comp[0, "Cl"].fix(34518.74 * units.mg / units.L)
m.fs.acid_feed1.conc_mass_comp[0, "Al"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Ca"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Fe"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Sc"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Y"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "La"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Ce"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Pr"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Nd"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Sm"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Gd"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Dy"].fix(eps)
# 1M HCl; pH = 0
m.fs.acid_feed2.flow_vol.fix(3.375 * units.L / units.hr)
m.fs.acid_feed2.conc_mass_comp[0, "H2O"].fix(1000000)
m.fs.acid_feed2.conc_mass_comp[0, "H"].fix(1008 * units.mg / units.L)
m.fs.acid_feed2.conc_mass_comp[0, "Cl"].fix(35453 * units.mg / units.L)
m.fs.acid_feed2.conc_mass_comp[0, "Al"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Ca"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Fe"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Sc"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Y"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "La"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Ce"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Pr"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Nd"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Sm"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Gd"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Dy"].fix(eps)
m.fs.rougher_sep.split_fraction[:, "recycle"].fix(0.9)
m.fs.rougher_sep.purge_state[0.0].pressure.fix(P_atm)
m.fs.rougher_sep.purge_state[0.0].temperature.fix(Temp_room)
m.fs.rougher_sep.recycle_state[0.0].pressure.fix(P_atm)
m.fs.rougher_sep.recycle_state[0.0].temperature.fix(Temp_room)
# 1M HCl; pH = 0
m.fs.acid_feed3.flow_vol.fix(3.517 * units.L / units.hr)
m.fs.acid_feed3.conc_mass_comp[0, "H2O"].fix(1000000)
m.fs.acid_feed3.conc_mass_comp[0, "H"].fix(1008 * units.mg / units.L)
m.fs.acid_feed3.conc_mass_comp[0, "Cl"].fix(35453 * units.mg / units.L)
m.fs.acid_feed3.conc_mass_comp[0, "Al"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Ca"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Fe"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Sc"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Y"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "La"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Ce"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Pr"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Nd"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Sm"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Gd"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Dy"].fix(eps)
m.fs.cleaner_org_make_up.flow_vol.fix(60.33)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Al_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Ca_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Fe_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Sc_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Y_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "La_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Ce_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Pr_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Nd_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Sm_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Gd_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Dy_o"].fix(eps)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "DEHPA"].fix(dehpa_conc)
m.fs.cleaner_org_make_up.conc_mass_comp[0, "Kerosene"].fix(kerosene_conc)
m.fs.cleaner_org_make_up.properties[0.0].pressure.fix(P_atm)
m.fs.cleaner_org_make_up.properties[0.0].temperature.fix(Temp_room)
m.fs.cleaner_mixer.mixed_state[0.0].pressure.fix(P_atm)
m.fs.cleaner_mixer.mixed_state[0.0].temperature.fix(Temp_room)
m.fs.cleaner_sep.split_fraction[:, "recycle"].fix(0.9)
m.fs.sl_sep1.liquid_recovery.fix(0.7)
m.fs.sl_sep1.split.recovered_state[0.0].pressure.fix(P_atm)
m.fs.sl_sep1.split.recovered_state[0.0].temperature.fix(Temp_room)
m.fs.sl_sep1.split.retained_state[0.0].pressure.fix(P_atm)
m.fs.sl_sep1.split.retained_state[0.0].temperature.fix(Temp_room)
m.fs.sl_sep2.liquid_recovery.fix(0.9)
m.fs.precip_sep.split_fraction[:, "recycle"].fix(0.9)
# Fix preciptator outlet temperature
m.fs.precipitator.precipitate_state_block[0].temperature.fix(348.15 * units.K)
# Roaster gas feed
m.fs.roaster.deltaP.fix(0)
m.fs.roaster.gas_inlet.temperature.fix(1330)
m.fs.roaster.gas_inlet.pressure.fix(101325)
# Inlet flue gas mole flow rate
fgas = 0.00781
# Inlet flue gas composition, typical flue gas by burning CH4 with air with stoichiometric ratio of 2.3
gas_comp = {
"O2": 0.1118,
"H2O": 0.1005,
"CO2": 0.0431,
"N2": 0.7446,
}
for i, v in gas_comp.items():
m.fs.roaster.gas_inlet.mole_frac_comp[0, i].fix(v)
m.fs.roaster.gas_inlet.flow_mol.fix(fgas)
# Fix outlet product temperature
m.fs.roaster.gas_outlet.temperature.fix(873.15)
# Fix operating conditions
m.fs.roaster.frac_comp_recovery.fix(0.95)
# Touch properties that are used in the UI
m.fs.leach.mscontactor.solid_inlet_state[0].flow_mass
m.fs.leach.mscontactor.solid_inlet_state[0].mass_frac_comp
m.fs.leach.mscontactor.liquid_inlet_state[0].flow_vol
m.fs.leach.mscontactor.liquid_inlet_state[0].conc_mol_comp
m.fs.solex_cleaner_load.mscontactor.organic_inlet_state[0].conc_mass_comp
m.fs.solex_cleaner_strip.mscontactor.organic_inlet_state[0].conc_mass_comp
m.fs.solex_rougher_load.mscontactor.organic_inlet_state[0].conc_mass_comp
m.fs.solex_rougher_strip.mscontactor.organic_inlet_state[0].conc_mass_comp
m.fs.solex_rougher_scrub.mscontactor.organic_inlet_state[0].conc_mass_comp
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet_state[0].conc_mass_comp
m.fs.solex_cleaner_strip.mscontactor.aqueous_inlet_state[0].conc_mass_comp
m.fs.solex_rougher_load.mscontactor.aqueous_inlet_state[0].conc_mass_comp
m.fs.solex_rougher_strip.mscontactor.aqueous_inlet_state[0].conc_mass_comp
m.fs.solex_rougher_scrub.mscontactor.aqueous_inlet_state[0].conc_mass_comp
m.fs.precipitator.cv_aqueous.properties_out[0].flow_vol
m.fs.precipitator.cv_aqueous.properties_out[0].conc_mass_comp
m.fs.precipitator.precipitate_state_block[0].flow_mol_comp
[docs]
def initialize_system(m):
"""
Provide initialized values for all streams in the system.
Args:
m: pyomo model
"""
seq = SequentialDecomposition()
seq.options.tear_method = "Direct"
seq.options.iterLim = 1
seq.options.tear_set = [
m.fs.leaching_feed_mixture,
m.fs.sx_rougher_load_aq_feed,
m.fs.sx_rougher_mixed_org_recycle,
m.fs.sx_cleaner_load_aq_feed,
m.fs.sx_cleaner_mixed_org_recycle,
]
G = seq.create_graph(m)
order = seq.calculation_order(G)
if _log.isEnabledFor(logging.INFO):
_init_ord = ", ".join([o[0].name for o in order])
_log.info("Initialization Order: {_init_ord}")
tear_guesses1 = {
"flow_vol": {0: 288.18},
"conc_mass_comp": {
(0, "Al"): 1759.30,
(0, "Ca"): 228.06,
(0, "Ce"): 1.36,
(0, "Cl"): 2097.49,
(0, "Dy"): 7.63e-3,
(0, "Fe"): 2089.62,
(0, "Gd"): 0.064,
(0, "H"): 40.89,
(0, "H2O"): 1000000,
(0, "HSO4"): 19465.77,
(0, "La"): 4.29,
(0, "Nd"): 1.63,
(0, "Pr"): 1.37,
(0, "SO4"): 4820.67,
(0, "Sc"): 2.5e-3,
(0, "Sm"): 0.69,
(0, "Y"): 3.06e-3,
},
}
tear_guesses2 = {
"flow_vol": {0: 128.95},
"conc_mass_comp": {
(0, "Al_o"): 73.38,
(0, "Ca_o"): 13.24,
(0, "Ce_o"): 3.91,
(0, "Dy_o"): 0.18,
(0, "Fe_o"): 362.63,
(0, "Gd_o"): 0.33,
(0, "La_o"): 0.26,
(0, "Nd_o"): 1.36,
(0, "Pr_o"): 0.15,
(0, "Sc_o"): 2.43,
(0, "Sm_o"): 0.019,
(0, "Y_o"): 5.36,
(0, "DEHPA"): 185882.7,
(0, "Kerosene"): 8.2e5,
},
}
tear_guesses3 = {
"flow_vol": {0: 208.99},
"conc_mass_comp": {
(0, "Al"): 2738.65,
(0, "Ca"): 355.51,
(0, "Ce"): 17.72,
(0, "Cl"): 3197.12,
(0, "Dy"): 0.50,
(0, "Fe"): 3278.86,
(0, "Gd"): 1.47,
(0, "H"): 4.26,
(0, "H2O"): 1000000,
(0, "HSO4"): 6900.7,
(0, "La"): 7.38,
(0, "Nd"): 7.99,
(0, "Pr"): 2.23,
(0, "SO4"): 16419.12,
(0, "Sc"): 0.17,
(0, "Sm"): 1.12,
(0, "Y"): 0.80,
},
}
tear_guesses4 = {
"flow_vol": {0: 603.34},
"conc_mass_comp": {
(0, "Al_o"): 36.41,
(0, "Ca_o"): 5.38,
(0, "Ce_o"): 1.14,
(0, "Dy_o"): 0.013,
(0, "Fe_o"): 91.1,
(0, "Gd_o"): 0.079,
(0, "La_o"): 0.14,
(0, "Nd_o"): 0.44,
(0, "Pr_o"): 0.059,
(0, "Sc_o"): 2.19e-4,
(0, "Sm_o"): 9.35e-3,
(0, "Y_o"): 0.21,
(0, "DEHPA"): 192174.95,
(0, "Kerosene"): 8.2e5,
},
}
tear_guesses5 = {
"flow_vol": {0: 6.91},
"conc_mass_comp": {
(0, "Al"): 1588.08,
(0, "Ca"): 197.47,
(0, "Ce"): 525.13,
(0, "Cl"): 35453.0,
(0, "Dy"): 15.14,
(0, "Fe"): 1932.16,
(0, "Gd"): 42.49,
(0, "H"): 692.2,
(0, "H2O"): 1000000,
(0, "La"): 29.36,
(0, "Nd"): 175.35,
(0, "Pr"): 3.77,
(0, "Sc"): 2.13e-3,
(0, "Sm"): 1.87,
(0, "Y"): 14.079,
},
}
# Pass the tear_guess to the SD tool
seq.set_guesses_for(m.fs.leach.liquid_inlet, tear_guesses1)
seq.set_guesses_for(m.fs.solex_rougher_load.organic_inlet, tear_guesses2)
seq.set_guesses_for(m.fs.solex_rougher_load.aqueous_inlet, tear_guesses3)
seq.set_guesses_for(m.fs.solex_cleaner_load.organic_inlet, tear_guesses4)
seq.set_guesses_for(m.fs.solex_cleaner_load.aqueous_inlet, tear_guesses5)
initializer_feed = FeedInitializer()
feed_units = [
m.fs.leach_liquid_feed,
m.fs.leach_solid_feed,
m.fs.rougher_org_make_up,
m.fs.acid_feed1,
m.fs.acid_feed2,
m.fs.acid_feed3,
m.fs.cleaner_org_make_up,
]
initializer_product = ProductInitializer()
product_units = [
m.fs.leach_filter_cake,
m.fs.leach_filter_cake_liquid,
m.fs.cleaner_organic_purge,
m.fs.rougher_organic_purge,
m.fs.precip_purge,
]
initializer_sep = SeparatorInitializer()
sep_units = [
m.fs.scrub_sep,
m.fs.precip_sep,
]
initializer_mix = MixerInitializer()
mix_units = [
m.fs.precip_sx_mixer,
]
initializer_leach = LeachingTrainInitializer()
leach_units = [
m.fs.leach,
]
initializer_sx = SolventExtractionInitializer()
sx_units = [
m.fs.solex_rougher_load,
m.fs.solex_rougher_scrub,
m.fs.solex_rougher_strip,
m.fs.solex_cleaner_load,
m.fs.solex_cleaner_strip,
]
initializer_bt = BlockTriangularizationInitializer()
def function(unit):
if unit in feed_units:
_log.info(f"Initializing {unit}")
initializer_feed.initialize(unit)
elif unit in product_units:
_log.info(f"Initializing {unit}")
initializer_product.initialize(unit)
elif unit in sep_units:
_log.info(f"Initializing {unit}")
initializer_sep.initialize(unit)
elif unit in mix_units:
_log.info(f"Initializing {unit}")
initializer_mix.initialize(unit)
elif unit in leach_units:
_log.info(f"Initializing {unit}")
initializer_leach.initialize(unit)
elif unit in sx_units:
_log.info(f"Initializing {unit}")
initializer_sx.initialize(unit)
else:
_log.info(f"Initializing {unit}")
initializer_bt.initialize(unit)
seq.run(m, function)
[docs]
def solve_system(m, solver_obj=None, tee=False):
"""
Solve the model.
Args:
m: pyomo model
solver_obj: Pyomo solver object to use
tee: boolean indicator to stream IPOPT solution
"""
if solver_obj is None:
# Why isn't it getting ipopt_v2 automatically?
solver_obj = get_solver("ipopt_v2")
solver_obj.options.constr_viol_tol = 1e-8
results = solver_obj.solve(m, tee=tee)
return results
[docs]
def fix_organic_recycle(m):
"""
Fix the volumetric flow rate of the organic recycle streams and unfix the flow of make-up streams.
Args:
m: pyomo model
"""
rougher_flow = value(m.fs.rougher_mixer.outlet.flow_vol[0])
m.fs.rougher_org_make_up.outlet.flow_vol.unfix()
m.fs.rougher_mixer.outlet.flow_vol.fix(rougher_flow)
cleaner_flow = value(m.fs.cleaner_mixer.outlet.flow_vol[0])
m.fs.cleaner_org_make_up.outlet.flow_vol.unfix()
m.fs.cleaner_mixer.outlet.flow_vol.fix(cleaner_flow)
def add_result_expressions(m):
fs = m.fs
ree_list = ["Sc", "Y", "La", "Ce", "Pr", "Nd", "Sm", "Gd", "Dy"]
gangue_list = ["Al", "Fe", "Ca"]
fs.ree_set = Set(initialize=ree_list)
fs.gangue_set = Set(initialize=gangue_list)
fs.metal_set = Set(initialize=ree_list + gangue_list)
metal_mass_frac = {
"Al2O3": 26.98 * 2 / (26.98 * 2 + 16 * 3),
"Fe2O3": 55.845 * 2 / (55.845 * 2 + 16 * 3),
"CaO": 40.078 / (40.078 + 16),
"Sc2O3": 44.956 * 2 / (44.956 * 2 + 16 * 3),
"Y2O3": 88.906 * 2 / (88.906 * 2 + 16 * 3),
"La2O3": 138.91 * 2 / (138.91 * 2 + 16 * 3),
"Ce2O3": 140.12 * 2 / (140.12 * 2 + 16 * 3),
"Pr2O3": 140.91 * 2 / (140.91 * 2 + 16 * 3),
"Nd2O3": 144.24 * 2 / (144.24 * 2 + 16 * 3),
"Sm2O3": 150.36 * 2 / (150.36 * 2 + 16 * 3),
"Gd2O3": 157.25 * 2 / (157.25 * 2 + 16 * 3),
"Dy2O3": 162.5 * 2 / (162.5 * 2 + 16 * 3),
}
molar_mass = {
"Al2O3": (26.98 * 2 + 16 * 3) * units.g / units.mol,
"Fe2O3": (55.845 * 2 + 16 * 3) * units.g / units.mol,
"CaO": (40.078 + 16) * units.g / units.mol,
"Sc2O3": (44.956 * 2 + 16 * 3) * units.g / units.mol,
"Y2O3": (88.906 * 2 + 16 * 3) * units.g / units.mol,
"La2O3": (138.91 * 2 + 16 * 3) * units.g / units.mol,
"Ce2O3": (140.12 * 2 + 16 * 3) * units.g / units.mol,
"Pr2O3": (140.91 * 2 + 16 * 3) * units.g / units.mol,
"Nd2O3": (144.24 * 2 + 16 * 3) * units.g / units.mol,
"Sm2O3": (150.36 * 2 + 16 * 3) * units.g / units.mol,
"Gd2O3": (157.25 * 2 + 16 * 3) * units.g / units.mol,
"Dy2O3": (162.5 * 2 + 16 * 3) * units.g / units.mol,
}
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals into leaching. "
"Includes metals from both the coal refuse solids "
"and the solvent extraction recycle stream",
)
def leaching_metal_inlet_flow(b, t, j):
if j == "Ca":
j_oxide = "CaO"
else:
j_oxide = f"{j}2O3"
return units.convert(
b.leach_solid_feed.flow_mass[t]
* b.leach_solid_feed.mass_frac_comp[t, j_oxide]
* metal_mass_frac[j_oxide],
to_units=units.kg / units.hr,
) + units.convert(
b.leach.mscontactor.liquid_inlet_state[t].conc_mass_comp[j]
* b.leach.mscontactor.liquid_inlet_state[t].flow_vol,
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals fed into the overall process",
)
def metal_feed_flow(b, t, j):
if j == "Ca":
j_oxide = "CaO"
else:
j_oxide = f"{j}2O3"
return units.convert(
b.leach_solid_feed.flow_mass[t]
* b.leach_solid_feed.mass_frac_comp[t, j_oxide]
* metal_mass_frac[j_oxide],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving sl_sep1 to go " "to solvent extraction.",
)
def leaching_metal_recovery_flow(b, t, j):
return units.convert(
b.sl_sep1.recovered_liquid_outlet.conc_mass_comp[t, j]
* b.sl_sep1.recovered_liquid_outlet.flow_vol[t],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"the flowsheet product stream.",
)
def metal_product_flow(b, t, j):
if j == "Ca":
j_oxide = "CaO"
else:
j_oxide = f"{j}2O3"
return units.convert(
b.roaster.flow_mol_comp_product[t, j]
* molar_mass[j_oxide]
* metal_mass_frac[j_oxide],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"the leach filter cake solids",
)
def metal_leach_filter_cake_solids_flow(b, t, j):
if j == "Ca":
j_oxide = "CaO"
else:
j_oxide = f"{j}2O3"
return units.convert(
+b.leach_filter_cake.flow_mass[t]
* b.leach_filter_cake.mass_frac_comp[t, j_oxide]
* metal_mass_frac[j_oxide],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"liquid entrained in the filter cake",
)
def metal_leach_filter_cake_liquid_flow(b, t, j):
return units.convert(
b.leach_filter_cake_liquid.flow_vol[t]
* b.leach_filter_cake_liquid.conc_mass_comp[t, j],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"the rougher load aqueous purge stream",
)
def metal_rougher_load_aqueous_purge_flow(b, t, j):
return units.convert(
b.load_sep.purge.flow_vol[t] * b.load_sep.purge.conc_mass_comp[t, j],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"the rougher scrub aqueous purge stream",
)
def metal_rougher_scrub_aqueous_purge_flow(b, t, j):
return units.convert(
b.scrub_sep.purge.flow_vol[t] * b.scrub_sep.purge.conc_mass_comp[t, j],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"the rougher organic purge stream",
)
def metal_rougher_organic_purge_flow(b, t, j):
return units.convert(
b.rougher_sep.purge.flow_vol[t]
* b.rougher_sep.purge.conc_mass_comp[t, f"{j}_o"],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"the cleaner aqueous purge stream",
)
def metal_cleaner_aqueous_purge_flow(b, t, j):
return units.convert(
b.precip_sep.purge.flow_vol[t] * b.precip_sep.purge.conc_mass_comp[t, j],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Mass flow rate of metals leaving process in "
"the cleaner organic purge stream",
)
def metal_cleaner_organic_purge_flow(b, t, j):
return units.convert(
b.cleaner_sep.purge.flow_vol[t]
* b.cleaner_sep.purge.conc_mass_comp[t, f"{j}_o"],
to_units=units.kg / units.hr,
)
@fs.Expression(
fs.time, fs.metal_set, doc="Single-pass recovery for each metal in leaching"
)
def leaching_metal_recovery_percentage(b, t, j):
return (
100
* b.leaching_metal_recovery_flow[t, j]
/ b.leaching_metal_inlet_flow[t, j]
)
@fs.Expression(
fs.time, fs.metal_set, doc="Overall recovery of each metal for the flowsheet"
)
def overall_metal_recovery_percentage(b, t, j):
return 100 * b.metal_product_flow[t, j] / b.metal_feed_flow[t, j]
@fs.Expression(
fs.time,
fs.metal_set,
doc="Percent of metal rejected in the " "leach filter cake solids.",
)
def leach_filter_cake_solids_percent_rejected(b, t, j):
return (
100 * b.metal_leach_filter_cake_solids_flow[t, j] / b.metal_feed_flow[t, j]
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Percent of metal rejected in the "
"liquid entrained in leach filter cake.",
)
def leach_filter_cake_liquid_percent_rejected(b, t, j):
return (
100 * b.metal_leach_filter_cake_liquid_flow[t, j] / b.metal_feed_flow[t, j]
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Percent of metal rejected through the rougher "
"load aqueous purge streams",
)
def metal_rougher_load_aqueous_percent_rejected(b, t, j):
return (
100
* b.metal_rougher_load_aqueous_purge_flow[t, j]
/ b.metal_feed_flow[t, j]
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Percent of metal rejected through the rougher "
"scrub aqueous purge streams",
)
def metal_rougher_scrub_aqueous_percent_rejected(b, t, j):
return (
100
* b.metal_rougher_scrub_aqueous_purge_flow[t, j]
/ b.metal_feed_flow[t, j]
)
@fs.Expression(
fs.time,
fs.metal_set,
doc="Percent of metal rejected through the rougher " "organic purge stream",
)
def metal_rougher_organic_percent_rejected(b, t, j):
return 100 * b.metal_rougher_organic_purge_flow[t, j] / b.metal_feed_flow[t, j]
@fs.Expression(
fs.time,
fs.metal_set,
doc="Percent of metal rejected through the cleaner " "aqueous purge stream",
)
def metal_cleaner_aqueous_percent_rejected(b, t, j):
return 100 * b.metal_cleaner_aqueous_purge_flow[t, j] / b.metal_feed_flow[t, j]
@fs.Expression(
fs.time,
fs.metal_set,
doc="Percent of metal rejected through the cleaner " "organic purge stream",
)
def metal_cleaner_organic_percent_rejected(b, t, j):
return 100 * b.metal_cleaner_organic_purge_flow[t, j] / b.metal_feed_flow[t, j]
@fs.Expression(fs.time, doc="Total mass flow rate of all REEs into process.")
def ree_feed_flow(b, t):
return sum(b.metal_feed_flow[t, i] for i in b.ree_set)
@fs.Expression(
fs.time, doc="Total mass flow rate of all REEs in flowsheet product stream."
)
def ree_product_flow(b, t):
return sum(b.metal_product_flow[t, i] for i in b.ree_set)
@fs.Expression(fs.time)
def overall_ree_recovery_percentage(b, t):
return 100 * b.ree_product_flow[t] / b.ree_feed_flow[t]
@fs.Expression(fs.time)
def ree_product_purity_percentage(b, t):
return (
100
* b.ree_product_flow[t]
/ units.convert(
b.roaster.flow_mass_product[0], to_units=units.kg / units.hr
)
)
[docs]
def display_results(m):
"""
Print key flowsheet outputs.
Args:
m: pyomo model
"""
metal_name_dict = {
"Sc": "scandium",
"Y": "yttrium",
"La": "lanthanum",
"Ce": "cerium",
"Pr": "praseodymium",
"Nd": "neodynium",
"Sm": "samarium",
"Gd": "gadolinium",
"Dy": "dysprosium",
"Al": "aluminum",
"Fe": "iron",
"Ca": "calcium",
}
def print_element_report(j):
name = metal_name_dict[j]
leach_recovery_percent = value(m.fs.leaching_metal_recovery_percentage[0, j])
overall_recovery_percent = value(m.fs.overall_metal_recovery_percentage[0, j])
lfcs_reject = value(m.fs.leach_filter_cake_solids_percent_rejected[0, j])
lfcl_reject = value(m.fs.leach_filter_cake_liquid_percent_rejected[0, j])
ral_reject = value(m.fs.metal_rougher_load_aqueous_percent_rejected[0, j])
ras_reject = value(m.fs.metal_rougher_scrub_aqueous_percent_rejected[0, j])
ro_reject = value(m.fs.metal_rougher_organic_percent_rejected[0, j])
ca_reject = value(m.fs.metal_cleaner_aqueous_percent_rejected[0, j])
co_reject = value(m.fs.metal_cleaner_organic_percent_rejected[0, j])
total = (
overall_recovery_percent
+ lfcs_reject
+ lfcl_reject
+ ral_reject
+ ras_reject
+ ro_reject
+ ca_reject
+ co_reject
)
print(f"\nLeaching {name} recovery is {leach_recovery_percent} %")
print(f"Overall {name} recovery is {overall_recovery_percent} %")
print(
f"{name.capitalize()} rejected in leach filter cake solids is {lfcs_reject} %"
)
print(
f"{name.capitalize()} rejected in leach filter cake entrained liquid is {lfcl_reject} %"
)
print(
f"{name.capitalize()} rejected in rougher load aqueous purge is {ral_reject} %"
)
print(
f"{name.capitalize()} rejected in rougher scrub aqueous purge is {ras_reject} %"
)
print(f"{name.capitalize()} rejected in rougher organic purge is {ro_reject} %")
print(f"{name.capitalize()} rejected in cleaner aqueous purge is {ca_reject} %")
print(f"{name.capitalize()} rejected in cleaner organic purge is {co_reject} %")
print(f"{name.capitalize()} accounted for is {total} %")
print("")
m.fs.roaster.report()
print(f"REE product mass flow is {value(m.fs.ree_product_flow[0])} kg/hr")
print(f"REE feed mass flow is {value(m.fs.ree_feed_flow[0])} kg/hr")
print(f"Total REE recovery is {value(m.fs.overall_ree_recovery_percentage[0])} %")
print(f"Product purity is {value(m.fs.ree_product_purity_percentage[0])} % REE")
print("")
print("")
print("REE element recovery:")
for j in m.fs.ree_set:
print_element_report(j)
print("")
print("Gangue recovery:")
for j in m.fs.gangue_set:
print_element_report(j)
print("")
[docs]
def calculate_results(fs):
"""
Calculate key flowsheet output for use in the flowsheet UI.
Args:
fs: Flowsheet
"""
data = {} # put all results here
# Total mass basis yield calculation
data["REE-product"] = value(fs.ree_product_flow[0])
data["REE-feed"] = value(fs.ree_feed_flow[0])
data["REE-recovery"] = value(fs.overall_ree_recovery_percentage[0])
data["product-purity"] = value(fs.ree_product_purity_percentage[0])
# Individual elemental recoveries
for metal in fs.metal_set:
data[f"total-{metal}-recovery"] = value(
fs.overall_metal_recovery_percentage[0, metal]
)
for metal in fs.metal_set:
data[f"{metal}-recovery"] = value(
fs.leaching_metal_recovery_percentage[0, metal]
)
return data
[docs]
def add_costing(m):
"""
Set the costing parameters for each unit model.
Args:
m: pyomo model
"""
m.fs.scaling_constraints = ComponentMap()
m.fs.costing = QGESSCosting()
CE_index_year = "UKy_2019"
# 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 * units.ton / units.hr, # p. 273, 351
"rougher_solex_aqueous_flow_vol": 23131 * units.L / units.min,
"cleaner_solex_aqueous_flow_vol": 925 * units.L / units.min,
"precipitator_solex_aqueous_flow_vol": 231 * units.L / units.min,
}
# Leaching costs
# 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,
},
)
# 4.3 is UKy Leaching - Tank Mixer
L_tank_mixer_accounts = ["4.3"]
m.fs.leach_mixer.power = Var(initialize=4.74, units=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.hp
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=units.hp,
)
m.fs.scaling_constraints[m.fs.leach_mixer.power] = (
m.fs.leach_mixer.power_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.ft**2
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=units.ft**2,
)
m.fs.scaling_constraints[m.fs.leach_sx_mixer.area] = (
m.fs.leach_sx_mixer.area_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.ft**3
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=units.ft**3,
)
m.fs.scaling_constraints[m.fs.sl_sep1.volume] = (
m.fs.sl_sep1.volume_scaling_constraint
)
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=units.MBTU / units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.MBTU
/ units.hr
* (
m.fs.leach_solid_feed.flow_mass[0]
/ reference_basis_flow["leach_sol_flow_mass"]
),
to_units=units.MBTU / units.hr,
)
m.fs.scaling_constraints[m.fs.leach_solution_heater.duty] = (
m.fs.leach_solution_heater.duty_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.gal
* (
m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["rougher_solex_aqueous_flow_vol"]
),
to_units=units.gal,
)
m.fs.scaling_constraints[m.fs.rougher_solex_tank.volume] = (
m.fs.rougher_solex_tank.volume_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.hp
* (
m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["rougher_solex_aqueous_flow_vol"]
),
to_units=units.hp,
)
m.fs.scaling_constraints[m.fs.rougher_mixer.power] = (
m.fs.rougher_mixer.power_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.gal
* (
m.fs.solex_rougher_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["rougher_solex_aqueous_flow_vol"]
),
to_units=units.gal,
)
m.fs.scaling_constraints[m.fs.rougher_solex_settler.volume] = (
m.fs.rougher_solex_settler.volume_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.gal
* (
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["cleaner_solex_aqueous_flow_vol"]
),
to_units=units.gal,
)
m.fs.scaling_constraints[m.fs.cleaner_solex_tank.volume] = (
m.fs.cleaner_solex_tank.volume_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.hp
* (
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["cleaner_solex_aqueous_flow_vol"]
),
to_units=units.hp,
)
m.fs.scaling_constraints[m.fs.cleaner_mixer.power] = (
m.fs.cleaner_mixer.power_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.gal
* (
m.fs.solex_cleaner_load.mscontactor.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["cleaner_solex_aqueous_flow_vol"]
),
to_units=units.gal,
)
m.fs.scaling_constraints[m.fs.cleaner_solex_settler.volume] = (
m.fs.cleaner_solex_settler.volume_scaling_constraint
)
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=units.gal, bounds=(0, None))
@m.fs.precipitator.Constraint(reep_pe_tanks_accounts)
def volume_scaling_constraint(c, k):
return m.fs.precipitator.volume == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.gal
* (
m.fs.precipitator.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["precipitator_solex_aqueous_flow_vol"]
),
to_units=units.gal,
)
m.fs.scaling_constraints[m.fs.precipitator.volume] = (
m.fs.precipitator.volume_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.hp
* (
m.fs.precipitator.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["precipitator_solex_aqueous_flow_vol"]
),
to_units=units.hp,
)
m.fs.scaling_constraints[m.fs.precipitator_mixer.power] = (
m.fs.precipitator_mixer.power_scaling_constraint
)
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=units.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 == units.convert(
REE_costing_params["1"][k]["RP Value"]
* units.ft**3
* (
m.fs.precipitator.aqueous_inlet.flow_vol[0]
/ reference_basis_flow["precipitator_solex_aqueous_flow_vol"]
),
to_units=units.ft**3,
)
m.fs.scaling_constraints[m.fs.sl_sep2.volume] = (
m.fs.sl_sep2.volume_scaling_constraint
)
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,
},
)
REE_mass_frac = {
"Y2O3": 88.906 * 2 / (88.906 * 2 + 16 * 3),
"La2O3": 138.91 * 2 / (138.91 * 2 + 16 * 3),
"Ce2O3": 140.12 * 2 / (140.12 * 2 + 16 * 3),
"Pr2O3": 140.91 * 2 / (140.91 * 2 + 16 * 3),
"Nd2O3": 144.24 * 2 / (144.24 * 2 + 16 * 3),
"Sm2O3": 150.36 * 2 / (150.36 * 2 + 16 * 3),
"Gd2O3": 157.25 * 2 / (157.25 * 2 + 16 * 3),
"Dy2O3": 162.5 * 2 / (162.5 * 2 + 16 * 3),
}
feed_REE = sum(
m.fs.leach_solid_feed.flow_mass[0]
* m.fs.leach_solid_feed.mass_frac_comp[0, molecule]
* REE_frac
for molecule, REE_frac in REE_mass_frac.items()
)
m.fs.feed_input = Var(
initialize=0.025, units=units.ton / units.hr, bounds=(0, None)
)
m.fs.feed_input_constraint = Constraint(
expr=m.fs.feed_input
== units.convert(
m.fs.leach_solid_feed.flow_mass[0], to_units=units.ton / units.hr
)
)
m.fs.scaling_constraints[m.fs.feed_input] = m.fs.feed_input_constraint
m.fs.feed_grade = Var(initialize=318.015, units=units.ppm, bounds=(0, None))
m.fs.feed_grade_constraint = Constraint(
expr=m.fs.feed_grade
== units.convert(
feed_REE / m.fs.leach_solid_feed.flow_mass[0], to_units=units.ppm
)
)
m.fs.scaling_constraints[m.fs.feed_grade] = m.fs.feed_grade_constraint
hours_per_shift = 8
shifts_per_day = 3
operating_days_per_year = 336
# for convenience
m.fs.annual_operating_hours = Param(
initialize=hours_per_shift * shifts_per_day * operating_days_per_year,
mutable=True,
units=units.hours / units.a,
)
m.fs.recovery_rate_per_year = Var(
initialize=13.306, units=units.kg / units.yr, bounds=(0, None)
)
m.fs.recovery_rate_per_year_constraint = Constraint(
expr=m.fs.recovery_rate_per_year
== units.convert(
m.fs.roaster.flow_mass_product[0] * m.fs.annual_operating_hours,
to_units=units.kg / units.yr,
)
)
m.fs.scaling_constraints[m.fs.recovery_rate_per_year] = (
m.fs.recovery_rate_per_year_constraint
)
# the land cost is the lease cost, or refining cost of REO produced
m.fs.land_cost = Expression(
expr=0.303736
* 1e-6
* getattr(units, "MUSD_" + CE_index_year)
/ units.ton
* units.convert(m.fs.feed_input, to_units=units.ton / units.hr)
* hours_per_shift
* units.hr
* shifts_per_day
* units.day**-1
* operating_days_per_year
* units.day
)
m.fs.solid_waste = Var(
m.fs.time, initialize=0.0245, units=units.ton / units.hr, bounds=(0, None)
)
m.fs.solid_waste_constraint = Constraint(
expr=m.fs.solid_waste[0]
== units.convert(
m.fs.leach_filter_cake.flow_mass[0], to_units=units.ton / units.hr
)
)
m.fs.scaling_constraints[m.fs.solid_waste] = m.fs.solid_waste_constraint
# TODO where is the corresponding constraint for this Var?
m.fs.precipitate = Var(
m.fs.time, initialize=1e-8, units=units.ton / units.hr, bounds=(0, None)
) # non-hazardous precipitate
m.fs.dust_and_volatiles = Var(
m.fs.time, initialize=9.5e-8, units=units.ton / units.hr, bounds=(0, None)
)
m.fs.dust_and_volatiles_constraint = Constraint(
expr=m.fs.dust_and_volatiles[0]
== units.convert(m.fs.roaster.flow_mass_dust[0], to_units=units.ton / units.hr)
)
m.fs.scaling_constraints[m.fs.dust_and_volatiles] = (
m.fs.dust_and_volatiles_constraint
)
m.fs.power = Var(m.fs.time, initialize=7, units=units.hp, bounds=(0, None))
m.fs.power_constraint = Constraint(
expr=m.fs.power[0]
== units.convert(
m.fs.precipitator_mixer.power
+ m.fs.cleaner_mixer.power
+ m.fs.rougher_mixer.power
+ m.fs.leach_mixer.power,
to_units=units.hp,
)
)
m.fs.scaling_constraints[m.fs.power] = m.fs.power_constraint
m.fs.acid_feed = Var(
m.fs.time, initialize=2.57e-4, units=units.tonne / units.hr, bounds=(0, None)
)
m.fs.acid_feed_constraint = Constraint(
expr=m.fs.acid_feed[0]
== units.convert(
(
m.fs.acid_feed1.conc_mass_comp[0, "H"] * m.fs.acid_feed1.flow_vol[0]
+ m.fs.acid_feed2.conc_mass_comp[0, "H"] * m.fs.acid_feed2.flow_vol[0]
+ m.fs.acid_feed3.conc_mass_comp[0, "H"] * m.fs.acid_feed3.flow_vol[0]
)
# Ratio of HCl and H molecular weight in mg
* 36460 / 1008,
to_units=units.tonne / units.hr,
)
)
m.fs.scaling_constraints[m.fs.acid_feed] = m.fs.acid_feed_constraint
resources = [
"nonhazardous_solid_waste",
"nonhazardous_precipitate_waste",
"dust_and_volatiles",
"power",
"HCl",
]
rates = [
m.fs.solid_waste,
m.fs.precipitate,
m.fs.dust_and_volatiles,
m.fs.power,
m.fs.acid_feed,
]
# define product flowrates
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,
}
# TODO Why are these Params and not Expressions?
m.fs.Ce_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Ce"]
* REO_molar_mass["Ce2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product cerium oxide mass flow",
)
m.fs.Dy_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Dy"]
* REO_molar_mass["Dy2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product dysprosium oxide mass flow",
)
m.fs.Gd_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Gd"]
* REO_molar_mass["Gd2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product gadolinium oxide mass flow",
)
m.fs.La_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "La"]
* REO_molar_mass["La2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product lanthanum oxide mass flow",
)
m.fs.Nd_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Nd"]
* REO_molar_mass["Nd2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product neodymium oxide mass flow",
)
m.fs.Pr_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Pr"]
* REO_molar_mass["Pr2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product praseodymium oxide mass flow",
)
m.fs.Sc_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Sc"]
* REO_molar_mass["Sc2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product scandium oxide mass flow",
)
m.fs.Sm_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Sm"]
* REO_molar_mass["Sm2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product samarium oxide mass flow",
)
m.fs.Y_product = Param(
default=units.convert(
m.fs.roaster.flow_mol_comp_product[0, "Y"]
* REO_molar_mass["Y2O3"]
* units.g
/ units.mol,
to_units=units.kg / units.hr,
),
units=units.kg / units.hr,
mutable=True,
doc="Product yttrium oxide mass flow",
)
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,
}
m.fs.costing.build_process_costs(
# 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 OM 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,
resources=resources,
rates=rates,
fixed_OM=True,
variable_OM=True,
feed_input=m.fs.feed_input,
efficiency=0.80, # power usage efficiency, or fixed motor/distribution efficiency
waste=[
"nonhazardous_solid_waste",
"nonhazardous_precipitate_waste",
"dust_and_volatiles",
],
recovery_rate_per_year=m.fs.recovery_rate_per_year,
CE_index_year=CE_index_year,
)
# 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
== units.convert(
1218.073
* units.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
* units.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
* units.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(units, "MUSD_" + CE_index_year),
)
)
)
# fix costing vars that shouldn't change
m.fs.precipitate.fix()
return m
[docs]
def initialize_costing(m):
"""
Initializes costing by calling block triangularization on the costing constraints and variables.
Args:
m: Model containing flowsheet with already-initialized unit models.
"""
# Create empty lists for costing vars and constraints
costing_vars = []
costing_constraints = []
# Add the variables and constraints from costing_constraints
for c in m.fs.scaling_constraints.values():
costing_constraints.append(c)
for v in m.fs.scaling_constraints.keys():
costing_vars.append(v)
# Add the variables and constraints from m.fs.costing
for v in m.fs.costing.component_data_objects(
ctype=Var, active=True, descend_into=False
):
costing_vars.append(v)
for c in m.fs.costing.component_data_objects(
ctype=Constraint, active=True, descend_into=False
):
costing_constraints.append(c)
# Add the variables and constraints from the unit model costing blocks
for unit in m.fs.component_data_objects(
ctype=Block, active=True, descend_into=False
):
if hasattr(unit, "costing"):
for v in unit.costing.component_data_objects(
ctype=Var, active=True, descend_into=False
):
costing_vars.append(v)
for c in unit.costing.component_data_objects(
ctype=Constraint, active=True, descend_into=False
):
costing_constraints.append(c)
# Ignore fixed variables (ensures 0 DOF)
costing_vars = [
v
for v in costing_vars
if v is not m.fs.costing.other_variable_costs[0]
and v is not m.fs.costing.additional_cost_of_recovery
and v is not m.fs.costing.other_fixed_costs
]
# Build a temporary block with only the costing variables & constraints and solve
subsystem = create_subsystem_block(costing_constraints, costing_vars)
solve_strongly_connected_components(subsystem, solver=get_solver("ipopt_v2"))
[docs]
def display_costing(m):
"""
Print the key costing results.
Args:
m: pyomo model
"""
QGESSCostingData.report(m.fs.costing)
m.fs.costing.variable_operating_costs.display()
QGESSCostingData.display_bare_erected_costs(m.fs.costing)
QGESSCostingData.display_flowsheet_cost(m.fs.costing)
def optimize_model(m):
m.obj = Objective(
expr=(
0.01
* (
m.fs.leach_liquid_feed.flow_vol[0]
+ m.fs.acid_feed1.flow_vol[0]
+ m.fs.acid_feed2.flow_vol[0]
+ m.fs.acid_feed3.flow_vol[0]
)
+ 1e5
* (
-m.fs.ree_product_flow[0]
+ m.fs.metal_product_flow[0, "Al"]
+ m.fs.metal_product_flow[0, "Ca"]
+ m.fs.metal_product_flow[0, "Fe"]
)
)
)
# Unfix the H2SO4 feed rate and feed concentration
m.fs.leach_liquid_feed.flow_vol.unfix()
m.fs.leach_liquid_feed.conc_mass_comp[0, "H"].unfix()
m.fs.leach_liquid_feed.conc_mass_comp[0, "HSO4"].unfix()
m.fs.leach_liquid_feed.conc_mass_comp[0, "SO4"].unfix()
@m.fs.leach_liquid_feed.Constraint(m.fs.time)
def H2SO4_stoich_eqn(b, t):
return (
b.properties[t].conc_mol_comp["H"]
== 2 * b.properties[t].conc_mol_comp["SO4"]
+ b.properties[t].conc_mol_comp["HSO4"]
)
for condata in m.fs.leach_liquid_feed.H2SO4_stoich_eqn.values():
set_scaling_factor(condata, 10)
# Because we have defined_state=True for the feed
# block, we need to create the dissociation
# equilibrium manually
# TODO maybe we should convert it into a FeedFlash?
@m.fs.leach_liquid_feed.Constraint(m.fs.time)
def HSO4_dissociation(b, t):
return (
b.properties[t].params.Ka2 * b.properties[t].conc_mol_comp["HSO4"]
== b.properties[t].conc_mol_comp["SO4"] * b.properties[t].conc_mol_comp["H"]
)
sf = get_scaling_factor(m.fs.leach.mscontactor.liquid[0, 1].hso4_dissociation)
for condata in m.fs.leach_liquid_feed.HSO4_dissociation.values():
set_scaling_factor(condata, sf)
# We should think about how strong of acid we can use for leaching
m.fs.leach_liquid_feed.properties[0].pH_phase["liquid"].setlb(0)
# IPOPT wanted to use a liquid feed of 13.7 L/hr, which would be
# far too low to let the leach solids move as a slurry.
# The original flow rate is 224.3 L/hr, so use 100 as a rough
# estimate for a lower bound.
m.fs.leach_liquid_feed.properties[0].flow_vol.setlb(100)
m.fs.rougher_mixer.outlet.flow_vol.unfix()
m.fs.cleaner_mixer.outlet.flow_vol.unfix()
# We probably also need a performance equation for the filter
# press (sl_sep1) to determine the fraction of fluid entrained
# as a function of the liquid to solid ratio.
# Unfix HCl feed flow rates and concentrations
for feed in [m.fs.acid_feed1, m.fs.acid_feed2, m.fs.acid_feed3]:
# Trying to optimize acid_feed1 will bring its flow to zero
# Since optimizing this stream has proved problematic, let's fix to 0.1 L/hr
if feed == m.fs.acid_feed1:
continue
else:
feed.flow_vol.unfix()
feed.conc_mass_comp[0, "H"].unfix()
feed.conc_mass_comp[0, "Cl"].unfix()
# Revisit how strong of an acid we can use
feed.properties[0].pH_phase["liquid"].setlb(0)
@feed.Constraint(m.fs.time)
def HCl_stoich_eqn(b, t):
return (
b.properties[t].conc_mol_comp["H"]
== b.properties[t].conc_mol_comp["Cl"]
)
for condata in feed.HCl_stoich_eqn.values():
set_scaling_factor(condata, 10)
# We can't make extractant dosage a decision variable until we have a
# correlation for how impurity (Fe, Al, Ca) distribution coefficients
# vary with dosage
m.fs.rougher_org_make_up.conc_mass_comp[0, "DEHPA"].unfix()
m.fs.cleaner_org_make_up.conc_mass_comp[0, "DEHPA"].unfix()
m.fs.rougher_org_make_up.properties[0].extractant_dosage.bounds = (5, 20)
m.fs.cleaner_org_make_up.properties[0].extractant_dosage.bounds = (5, 20)
# If the pH in the rougher scrub goes above 5 or 6, the equations get
# extremely ill conditioned.
m.fs.solex_rougher_scrub.mscontactor.aqueous[0.0, 1].pH_phase["liquid"].setub(6)
solver = get_solver("ipopt_v2")
solver.options.constr_viol_tol = 1e-8
solver.options.max_iter = 300
results = solver.solve(m, tee=True)
if check_optimal_termination(results):
display_results(m)
else:
print("Flowsheet optimization did not converge.")
def data_reconcilliation(m):
m.fs.acid_feed1.flow_vol.unfix()
m.obj = Objective(expr=m.fs.acid_feed1.flow_vol[0])
m.fs.solex_rougher_scrub.mscontactor.aqueous[0.0, 1].pH_phase["liquid"].setub(3)
solver = get_solver("ipopt_v2")
solver.options.constr_viol_tol = 1e-8
solver.options.max_iter = 300
results = solver.solve(m, tee=True)
if check_optimal_termination(results):
display_results(m)
else:
print("Data reconcilliation optimization did not converge.")
if __name__ == "__main__":
m, results = main()
optimize_model(m)
# data_reconcilliation(m)