University of Kentucky Flowsheet Tutorial#
The University of Kentucky (UKy) flowsheet is a continuous, pilot-scale plant operation for recovering rare earth elements (REEs) from coal and coal byproducts with the goal of producing marketable mixed rare earth oxides (REOs). The feedstock is from a coal preparation plant in Kentucky containing around 300 ppm of REEs (0.03%) on a total mass basis. PrOMMiS has simulated a portion of this plant design by considering the following key unit operations.
Simplified Depiction of Flowsheet Connectivity#

The figure above represents a simplified schematic of the flowsheet’s connectivity. After going through this tutorial, users will understand how to build, initialize, and simulate each of these unit operations with PrOMMiS as well as how to connect these units to generate meaningful results depicting the UKy plant.
Full Implementation of Flowsheet Connectivity#

The figure above represents full flowsheet connectivity implemented in PrOMMiS’s UKy flowsheet and can be broken down into the following steps:
(1) The leaching process uses sulfuric acid leaching to extract REEs from the solid feed (coal refuse containing various REEs and inert species) into a pregnant leach solution (PLS) that is loaded with metals, including the REEs of interest. The solid waste is turned into a filter cake and discarded, while the PLS is sent to solvent extraction.
(2) The goal of the solvent extraction (SX) circuit is to separate out the REEs from the other metals present in the PLS. Di(2-ethylhexyl)phosphoric acid (DEHPA), an extractant commonly used in hydrometallurigical processes, is used to bind the REEs in the organic phase while contaminants are left in the aqueous phase and recycled back to the leaching process. In the final stripping stage, the loaded organic is contacted with HCl, which strips the REEs away from DEHPA (the organic phase) and into a concentrated aqueous solution. Meanwhile, the organic solvent gets recycled to extract more REEs from the incoming PLS. This process is repeated twice - once in the SX rougher circuit, which aims to remove as many metals as possible from the PLS and the subsequent cleaner circuit aims to improve the REE purity by selectively removing contaminant metals (Al, Ca, Fe).
(3) Next, the concentrated aqueous solution of REEs is sent to the precipitation unit. Since REEs are mainly used in solid form, the precipitator contacts the solution with oxalic acid, a precipitating agent, to form a solid rare earth oxalate. The liquid is recycled back to the solvent extraction process while the solids are sent to the roaster.
(4) Lastly, the roaster applies heat to decompose the rare earth oxalate into free gases and REOs, which is a marketable product.
Useful Links:
Public Github Repository: prommis/prommis
UKy Flowsheet Code: prommis/prommis
Step 1: Import the necessary tools#
We’ll use some basic functionalities from Pyomo, generic models from IDAES, and process-specific models from PrOMMiS.
# Import the essentials from Pyomo
from pyomo.environ import (
assert_optimal_termination,
ConcreteModel,
Constraint,
Expression,
Param,
SolverFactory,
Suffix,
TransformationFactory,
Var,
check_optimal_termination,
units,
value,
)
from pyomo.network import Arc, SequentialDecomposition
# Import the essentials from IDAES
import idaes.logger as idaeslog
from idaes.core import (
FlowDirection,
FlowsheetBlock,
MaterialBalanceType,
MomentumBalanceType,
UnitModelBlock,
UnitModelCostingBlock,
)
# Import scaling, initialization, and diagnostic tools from IDAES
from idaes.core.initialization import BlockTriangularizationInitializer
from idaes.core.scaling.scaling_base import ScalerBase
from idaes.core.scaling import CustomScalerBase, ConstraintScalingScheme
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,
)
# Import unit models from IDAES
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,
)
# Import the UKy-specific unit and property models
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.precipitate.precipitate_liquid_properties import AqueousParameter
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.solvent_extraction.translator_leach_precip import TranslatorLeachPrecip
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
# Set up logger
_log = idaeslog.getLogger(__name__)
Step 2: Flowsheet building#
Now we will begin calling the individual unit models that will represent the unit operations depicted above. Each unit model must be linked to at least one property package, and some require additional specifications, such as reaction packages or configuration arguments, which will tailor the unit model to this specific application.
Step 2.1: Create Flowsheet#
Start by creating a pyomo model and a flowsheet.
m = ConcreteModel()
m.fs = FlowsheetBlock(dynamic=False)
Then begin assembling the unit, property, and reaction models section-by-section. These variables will be created in chronological order - beginning with the leaching section of the flowsheet and concluding with the product roasting section.
Step 2.2: Create variables for the leaching section#
Specify the necessary unit, property, and reaction models for the leaching section with the following syntax: m.fs.Name = ImportedModel(arguments), where Name is a user-defined name for the model.
# Leaching property models
m.fs.leach_soln = SulfuricAcidLeachingParameters() # Aqueous property model
m.fs.coal = CoalRefuseParameters() # Solid property model
# Leaching reaction model - handles H2SO4 chemistry
m.fs.leach_rxns = CoalRefuseLeachingReactionParameterBlock()
# Leaching unit model
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,
)
# Solid-liquid separator used to approximate a filter press
m.fs.sl_sep1 = SLSeparator(
solid_property_package=m.fs.coal,
liquid_property_package=m.fs.leach_soln,
material_balance_type=MaterialBalanceType.componentTotal,
# Ignore momentum balance since the property package does not have pressure or momentum terms
momentum_balance_type=MomentumBalanceType.none,
# Ignore energy split basis since the property package does not have temperature terms
energy_split_basis=EnergySplittingType.none,
)
# Recycle loop mixer
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,
# Ignore mixing type since the property package does not have enthalpy terms
energy_mixing_type=MixingType.none,
# Ignore momentum mixing since the property package does not have pressure or momentum terms
momentum_mixing_type=MomentumMixingType.none,
)
# Define inlets into the flowsheet
m.fs.leach_liquid_feed = Feed(property_package=m.fs.leach_soln)
m.fs.leach_solid_feed = Feed(property_package=m.fs.coal)
# Define outlets from the flowsheet
m.fs.leach_filter_cake = Product(property_package=m.fs.coal)
m.fs.leach_filter_cake_liquid = Product(property_package=m.fs.leach_soln)
Step 2.3: Create variables for the solvent extraction section#
Specify the necessary unit, property, and reaction models for the solvent extraction section.
# Solvent extraction property models
m.fs.prop_o = REESolExOgParameters()
# Solvent extraction reaction model
m.fs.reaxn = SolventExtractionReactions()
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=True,
create_hydrostatic_pressure_terms=True,
)
m.fs.solex_rougher_scrub = SolventExtraction(
number_of_finite_elements=1,
dynamic=False,
aqueous_stream={
"property_package": m.fs.leach_soln,
"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=True,
create_hydrostatic_pressure_terms=True,
)
m.fs.solex_rougher_strip = SolventExtraction(
number_of_finite_elements=2,
dynamic=False,
aqueous_stream={
"property_package": m.fs.leach_soln,
"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=True,
create_hydrostatic_pressure_terms=True,
)
# SX Rougher separator for organic recycle stream
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,
)
# SX Rougher mixer for organic recycle stream
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,
)
# Separators for SX Rougher aqueous recycle streams
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.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.solex_cleaner_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=True,
create_hydrostatic_pressure_terms=True,
)
m.fs.solex_cleaner_strip = SolventExtraction(
number_of_finite_elements=3,
dynamic=False,
aqueous_stream={
"property_package": m.fs.leach_soln,
"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=True,
create_hydrostatic_pressure_terms=True,
)
# SX Cleaner separator for organic stream
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,
)
# SX Cleaner mixer for organic streams
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,
)
# Mixes PLS with a recycled stream from the SX Cleaner
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,
)
# Define inlets into the flowsheet
m.fs.rougher_org_make_up = Feed(property_package=m.fs.prop_o)
m.fs.cleaner_org_make_up = Feed(property_package=m.fs.prop_o)
# Dilute HCl feed
m.fs.acid_feed1 = Feed(property_package=m.fs.leach_soln)
m.fs.acid_feed2 = Feed(property_package=m.fs.leach_soln)
m.fs.acid_feed3 = Feed(property_package=m.fs.leach_soln)
# Define outlets from the flowsheet
m.fs.rougher_organic_purge = Product(property_package=m.fs.prop_o)
m.fs.cleaner_organic_purge = Product(property_package=m.fs.prop_o)
Step 2.4: Create variables for the precipitation section#
Specify the necessary unit, property, and reaction models for the precipitation section. Note that the aqueous and solid property packages are different from the previously defined aqueous and solid property packages.
# Precipitation property packages
m.fs.properties_aq = AqueousParameter() # Computes split fractions
m.fs.properties_solid = PrecipitateParameters() # Components are rare earth oxalates
# Precipitation unit model
m.fs.precipitator = Precipitator(
property_package_aqueous=m.fs.properties_aq,
property_package_precipitate=m.fs.properties_solid,
make_volume_balance_constraint=True,
)
# Solid-liquid separator used to approximate a filter press
m.fs.sl_sep2 = SLSeparator(
solid_property_package=m.fs.properties_solid,
liquid_property_package=m.fs.leach_soln,
material_balance_type=MaterialBalanceType.componentTotal,
momentum_balance_type=MomentumBalanceType.none,
energy_split_basis=EnergySplittingType.none,
)
m.fs.precip_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.precip_sx_mixer = Mixer(
property_package=m.fs.leach_soln,
num_inlets=2,
inlet_list=["precip", "rougher"],
material_balance_type=MaterialBalanceType.componentTotal,
energy_mixing_type=MixingType.none,
momentum_mixing_type=MomentumMixingType.none,
)
# Define outlets from the flowsheet
m.fs.precip_purge = Product(property_package=m.fs.properties_aq)
Step 2.5: Create variables for the translator blocks#
Specify the inlet and outlet property models for the translators blocks, which facilitates the conversion from the inlet component list to the outlet component list.
# Translator unit models
m.fs.translator_leaching_to_precipitate = TranslatorLeachPrecip(
inlet_property_package=m.fs.leach_soln,
outlet_property_package=m.fs.properties_aq,
)
m.fs.translator_precipitate_to_leaching = TranslatorLeachPrecip(
inlet_property_package=m.fs.properties_aq,
outlet_property_package=m.fs.leach_soln,
)
m.fs.translator_sep_to_roast = TranslatorLeachPrecip(
inlet_property_package=m.fs.leach_soln,
outlet_property_package=m.fs.properties_aq,
)
m.fs.translator_precip_sep_to_purge = TranslatorLeachPrecip(
inlet_property_package=m.fs.leach_soln,
outlet_property_package=m.fs.properties_aq,
)
Step 2.6: Create variables for the product roaster section#
Specify the necessary unit, property, and reaction models for the roaster section.
# Define the relevant gas species
gas_species = {"O2", "H2O", "CO2", "N2"}
# Roaster property packages
m.fs.prop_gas = GenericParameterBlock(
**get_prop(gas_species, ["Vap"], EosType.IDEAL),
doc="gas property",
)
m.fs.prop_solid = PrecipitateParameters()
# Roaster unit model
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.properties_aq,
has_holdup=False,
has_heat_transfer=True,
has_pressure_change=True,
)
Step 3: Connect the unit models#
We’ve now assembled all the pieces necessary to build the flowsheet by defining all the unit operations and assigning them their appropriate configuration arguments, but we haven’t established how all these operations are connected. Thus, the next step will be to use Pyomo Arcs to connect the units with the following syntax:
m.fs.Name = Arc(source, destination)
# Establish Arc 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_recycle = Arc(
source=m.fs.scrub_sep.recycle, 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 = Arc(
source=m.fs.solex_cleaner_load.aqueous_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_outlet = Arc(
source=m.fs.solex_cleaner_strip.aqueous_outlet,
destination=m.fs.translator_leaching_to_precipitate.inlet,
)
m.fs.precip_aq_inlet = Arc(
source=m.fs.translator_leaching_to_precipitate.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_outlet = Arc(
source=m.fs.precipitator.aqueous_outlet,
destination=m.fs.translator_precipitate_to_leaching.inlet,
)
m.fs.sl_sep2_solid_inlet = Arc(
source=m.fs.translator_precipitate_to_leaching.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_outlet = Arc(
source=m.fs.sl_sep2.retained_liquid_outlet,
destination=m.fs.translator_sep_to_roast.inlet,
)
m.fs.roaster_liquid_inlet = Arc(
source=m.fs.translator_sep_to_roast.outlet,
destination=m.fs.roaster.liquid_inlet,
)
m.fs.sl_sep2_aq_purge = Arc(
source=m.fs.precip_sep.purge, destination=m.fs.translator_precip_sep_to_purge.inlet
)
m.fs.precip_purge_inlet = Arc(
source=m.fs.translator_precip_sep_to_purge.outlet,
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)
Step 4: Set the operating conditions#
Now specify the operating conditions for the flowsheet such that we have a square problem (DOF = 0).
# Constants
dehpa_conc = 975.8e3 * units.mg / units.L
kerosene_conc = 8.2e5 * units.mg / units.L
Temp_room = 303 * units.K
P_atm = 101235 * units.Pa
# Episilon represents near-zero component concentrations
eps = 1e-8 * units.mg / units.L
# Fix liquid leach feed
m.fs.leach_liquid_feed.properties[0.0].pressure.fix(P_atm)
m.fs.leach_liquid_feed.properties[0.0].temperature.fix(Temp_room)
m.fs.leach_liquid_feed.flow_vol.fix(224.3 * 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, "H"].fix(2 * 0.05 * 1e3 * units.mg / units.L)
m.fs.leach_liquid_feed.conc_mass_comp[0, "HSO4"].fix(1e-8 * units.mg / units.L)
m.fs.leach_liquid_feed.conc_mass_comp[0, "SO4"].fix(0.05 * 96e3 * units.mg / units.L)
# Fix solid leach feed
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)
# Fix leach tank volume
m.fs.leach.volume.fix(100 * units.gallon)
# Fix temperatures and pressures
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.scrub_sep.recycle_state[0.0].pressure.fix(P_atm)
m.fs.scrub_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)
# Fix solvent extraction degrees of freedom
m.fs.solex_rougher_load.mscontactor.volume[:].fix(0.4 * units.m**3)
m.fs.solex_rougher_load.area_cross_stage[:] = 1
m.fs.solex_rougher_load.elevation[:] = 0
m.fs.solex_rougher_load.mscontactor.aqueous[0.0, 3].temperature.fix(Temp_room)
m.fs.solex_rougher_load.mscontactor.organic[0.0, 1].temperature.fix(Temp_room)
m.fs.solex_rougher_scrub.mscontactor.volume[:].fix(0.4 * units.m**3)
m.fs.solex_rougher_scrub.area_cross_stage[:] = 1
m.fs.solex_rougher_scrub.elevation[:] = 0
m.fs.solex_rougher_scrub.mscontactor.aqueous[0.0, 1].temperature.fix(Temp_room)
m.fs.solex_rougher_scrub.mscontactor.organic[0.0, 1].temperature.fix(Temp_room)
m.fs.solex_rougher_strip.mscontactor.volume[:].fix(0.4 * units.m**3)
m.fs.solex_rougher_strip.area_cross_stage[:] = 1
m.fs.solex_rougher_strip.elevation[:] = 0
m.fs.solex_rougher_strip.mscontactor.organic[0.0, 2].temperature.fix(Temp_room)
m.fs.solex_rougher_strip.mscontactor.aqueous[0.0, 1].temperature.fix(Temp_room)
m.fs.solex_cleaner_load.mscontactor.volume[:].fix(0.4 * units.m**3)
m.fs.solex_cleaner_load.area_cross_stage[:] = 1
m.fs.solex_cleaner_load.elevation[:] = 0
m.fs.solex_cleaner_load.mscontactor.aqueous[0.0, 3].temperature.fix(Temp_room)
m.fs.solex_cleaner_load.mscontactor.organic[0.0, 1].temperature.fix(Temp_room)
m.fs.solex_cleaner_strip.mscontactor.volume[:].fix(0.4 * units.m**3)
m.fs.solex_cleaner_strip.area_cross_stage[:] = 1
m.fs.solex_cleaner_strip.elevation[:] = 0
m.fs.solex_cleaner_strip.mscontactor.organic[0.0, 3].temperature.fix(Temp_room)
m.fs.solex_cleaner_strip.mscontactor.aqueous[0.0, 1].temperature.fix(Temp_room)
# Fix the recycle split fractions
m.fs.load_sep.split_fraction[:, "recycle"].fix(0.9)
m.fs.scrub_sep.split_fraction[:, "recycle"].fix(0.9)
# Fix the conditions of the organic make-up
m.fs.rougher_org_make_up.flow_vol.fix(6.201)
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)
# Fix the conditions of the HCl acid feeds
m.fs.acid_feed1.flow_vol.fix(90)
m.fs.acid_feed1.properties[0.0].pressure.fix(P_atm)
m.fs.acid_feed1.properties[0.0].temperature.fix(Temp_room)
m.fs.acid_feed1.conc_mass_comp[0, "H2O"].fix(1000000)
m.fs.acid_feed1.conc_mass_comp[0, "H"].fix(10.36)
m.fs.acid_feed1.conc_mass_comp[0, "SO4"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "HSO4"].fix(eps)
m.fs.acid_feed1.conc_mass_comp[0, "Cl"].fix(359.64)
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)
m.fs.acid_feed2.flow_vol.fix(9)
m.fs.acid_feed2.properties[0.0].pressure.fix(P_atm)
m.fs.acid_feed2.properties[0.0].temperature.fix(Temp_room)
m.fs.acid_feed2.conc_mass_comp[0, "H2O"].fix(1000000)
m.fs.acid_feed2.conc_mass_comp[0, "H"].fix(
10.36 * 4
) # Arbitrarily choose 4x the dilute solution
m.fs.acid_feed2.conc_mass_comp[0, "SO4"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "HSO4"].fix(eps)
m.fs.acid_feed2.conc_mass_comp[0, "Cl"].fix(359.64 * 4)
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.acid_feed3.flow_vol.fix(9)
m.fs.acid_feed3.properties[0.0].pressure.fix(P_atm)
m.fs.acid_feed3.properties[0.0].temperature.fix(Temp_room)
m.fs.acid_feed3.conc_mass_comp[0, "H2O"].fix(1000000)
m.fs.acid_feed3.conc_mass_comp[0, "H"].fix(
10.36 * 4
) # Arbitrarily choose 4x the dilute solution
m.fs.acid_feed3.conc_mass_comp[0, "SO4"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "HSO4"].fix(eps)
m.fs.acid_feed3.conc_mass_comp[0, "Cl"].fix(359.64 * 4)
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)
# Fix the rougher recycle split fraction
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)
# Fix the conditions of the cleaner organic make-up
m.fs.cleaner_org_make_up.flow_vol.fix(6.201)
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)
# Fix the cleaner recycle split fraction
m.fs.cleaner_sep.split_fraction[:, "recycle"].fix(0.9)
m.fs.cleaner_sep.purge_state[0.0].pressure.fix(P_atm)
m.fs.cleaner_sep.purge_state[0.0].temperature.fix(Temp_room)
m.fs.cleaner_sep.recycle_state[0.0].pressure.fix(P_atm)
m.fs.cleaner_sep.recycle_state[0.0].temperature.fix(Temp_room)
# Fix the conditions of the solid-liquid separators
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.sl_sep2.split.recovered_state[0.0].pressure.fix(P_atm)
m.fs.sl_sep2.split.recovered_state[0.0].temperature.fix(Temp_room)
m.fs.sl_sep2.split.retained_state[0.0].pressure.fix(P_atm)
m.fs.sl_sep2.split.retained_state[0.0].temperature.fix(Temp_room)
m.fs.translator_precipitate_to_leaching.outlet.pressure.fix(P_atm)
m.fs.translator_precipitate_to_leaching.outlet.temperature.fix(Temp_room)
# Fix preciptator outlet temperature
m.fs.precipitator.precipitate_state_block[0].temperature.fix(348.15 * units.K)
# Fix the precipitator recycle split fraction
m.fs.precip_sep.split_fraction[:, "recycle"].fix(0.9)
m.fs.precip_sep.purge_state[0.0].pressure.fix(P_atm)
m.fs.precip_sep.purge_state[0.0].temperature.fix(Temp_room)
m.fs.precip_sep.recycle_state[0.0].pressure.fix(P_atm)
m.fs.precip_sep.recycle_state[0.0].temperature.fix(Temp_room)
m.fs.precip_sx_mixer.mixed_state[0.0].pressure.fix(P_atm)
m.fs.precip_sx_mixer.mixed_state[0.0].temperature.fix(Temp_room)
# Fix the roaster gas feed conditions
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)
Step 5: Apply scaling#
In order for the flowsheet to solve, variables will need to be scaled appropriately. While variables may have a default scaling factor set, it is important to re-scale those with poor initial scaling or non-existent scaling because large and small magnitudes can make it harder to converge to a feasible solution.
def set_scaling(m):
# Attaches scaling factors to "m" and exports values to the solver
m.scaling_factor = Suffix(direction=Suffix.EXPORT)
# Called to utilize IDAES scaling tools
sb = ScalerBase()
csb = CustomScalerBase()
# Apply scaling to constraints
csb.scale_constraint_by_nominal_value(
m.fs.solex_rougher_load.distribution_extent_constraint[0, 1, "Ca"],
scheme=ConstraintScalingScheme.inverseMaximum,
overwrite=False,
)
csb.scale_constraint_by_nominal_value(
m.fs.solex_rougher_scrub.distribution_extent_constraint[0, 1, "Al"],
scheme=ConstraintScalingScheme.inverseMaximum,
overwrite=False,
)
csb.scale_constraint_by_nominal_value(
m.fs.roaster.energy_balance_eqn[0],
scheme=ConstraintScalingScheme.inverseMaximum,
overwrite=False,
)
csb.scale_constraint_by_nominal_value(
m.fs.precipitator.aqueous_depletion[0, "H2O"],
scheme=ConstraintScalingScheme.inverseMaximum,
overwrite=False,
)
# Apply scaling to variables
sb.set_variable_scaling_factor(m.fs.roaster.heat_duty[0], 1e-2)
for var in m.fs.component_data_objects(Var, descend_into=True):
if "temperature" in var.name:
sb.set_variable_scaling_factor(var, 1e-2, overwrite=True)
if "pressure" in var.name:
sb.set_variable_scaling_factor(var, 1e-5)
if "flow_mol" in var.name:
sb.set_variable_scaling_factor(var, 1e-3)
if "conc_mass_comp" in var.name:
sb.set_variable_scaling_factor(var, 1e0, overwrite=True)
return m
Step 6: Solve the square problem#
Step 6.1: Initialize the system#
At this point, we have successfully defined all the models, connected them with appropriate Arcs, set their operating conditions such that the DOFs are 0, and have scaled the variables to limit the presence of very small or very large magnitudes. However, these problems can have more than one solution, so we need to give the solver a good starting point so that it reliably converges to a physically meaningful solution. Since there are multiple recycle loops involved in this process, sequential decomposition will be used to initialize the flowsheet and tear sets must be specified to initialize these recycle streams.
def initialize_system(m):
# Initialize the model with sequential decomposition
seq = SequentialDecomposition()
seq.options.tear_method = "Direct" # Alternatives are Wegstein and Newton
# Set limits on the number of sequential loops
seq.options.iterLim = 1
# Identify Arc names for recycle streams
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,
]
# Supply tear guesses with initial values that are close to the solution
# Guesses for the liquid leach inlet stream conditions
tear_guesses1 = {
"flow_vol": {0: 866.06},
"conc_mass_comp": {
(0, "Al"): 207.46,
(0, "Ca"): 40.23,
(0, "Ce"): 2.11,
(0, "Cl"): 158.36,
(0, "Dy"): 1.13e-2,
(0, "Fe"): 292.56,
(0, "Gd"): 0.24,
(0, "H"): 13.66,
(0, "H2O"): 1000000,
(0, "HSO4"): 1940.93,
(0, "La"): 0.76,
(0, "Nd"): 1.06,
(0, "Pr"): 0.26,
(0, "SO4"): 1438.92,
(0, "Sc"): 2.07e-3,
(0, "Sm"): 0.10,
(0, "Y"): 2.02e-2,
},
}
# Guesses for the organic SX Rougher loading stream conditions
tear_guesses2 = {
"flow_vol": {0: 62.01},
"conc_mass_comp": {
(0, "Al_o"): 0.048,
(0, "Ca_o"): 1.98e-2,
(0, "Ce_o"): 5.71e-3,
(0, "Dy_o"): 1.077,
(0, "Fe_o"): 1.954,
(0, "Gd_o"): 0.14,
(0, "La_o"): 4.03e-3,
(0, "Nd_o"): 3.37e-3,
(0, "Pr_o"): 1.04e-3,
(0, "Sc_o"): 1.74,
(0, "Sm_o"): 4.91e-3,
(0, "Y_o"): 4.17,
(0, "DEHPA"): 9.7e5,
(0, "Kerosene"): 8.2e5,
},
}
# Guesses for the aqueous SX Rougher loading stream conditions
tear_guesses3 = {
"flow_vol": {0: 623.07},
"conc_mass_comp": {
(0, "Al"): 320.46,
(0, "Ca"): 62.14,
(0, "Ce"): 3.26,
(0, "Cl"): 192.63,
(0, "Dy"): 4.6e-2,
(0, "Fe"): 452.28,
(0, "Gd"): 0.40,
(0, "H"): 2.92,
(0, "H2O"): 1000000,
(0, "HSO4"): 732.71,
(0, "La"): 1.18,
(0, "Nd"): 1.63,
(0, "Pr"): 0.41,
(0, "SO4"): 2543.95,
(0, "Sc"): 2.25e-2,
(0, "Sm"): 0.16,
(0, "Y"): 0.11,
},
}
# Guesses for the organic SX Cleaner loading stream conditions
tear_guesses4 = {
"flow_vol": {0: 62},
"conc_mass_comp": {
(0, "Al_o"): 3.64e-3,
(0, "Ca_o"): 2.13e-3,
(0, "Ce_o"): 5.93e-4,
(0, "Dy_o"): 0.33,
(0, "Fe_o"): 0.75,
(0, "Gd_o"): 4.00e-2,
(0, "La_o"): 4.08e-4,
(0, "Nd_o"): 3.76e-4,
(0, "Pr_o"): 1.47e-4,
(0, "Sc_o"): 3.97e-3,
(0, "Sm_o"): 7.87e-4,
(0, "Y_o"): 1.03,
(0, "DEHPA"): 9.8e5,
(0, "Kerosene"): 8.2e5,
},
}
# Guesses for the aqueous SX Cleaner loading stream conditions
tear_guesses5 = {
"flow_vol": {0: 16.70},
"conc_mass_comp": {
(0, "Al"): 2.42,
(0, "Ca"): 0.68,
(0, "Ce"): 0.16,
(0, "Cl"): 1438.56,
(0, "Dy"): 0.64,
(0, "Fe"): 22.67,
(0, "Gd"): 1.01,
(0, "H"): 39.81,
(0, "H2O"): 1000000,
(0, "HSO4"): 2.88e-6,
(0, "La"): 0.13,
(0, "Nd"): 8.52e-2,
(0, "Pr"): 2.10e-2,
(0, "SO4"): 2.54e-6,
(0, "Sc"): 1.65e-3,
(0, "Sm"): 7.88e-2,
(0, "Y"): 1.17,
},
}
# Pass the tear guesses to the sequential decomposition 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)
# Associate units with their specialized initializers
output_level = idaeslog.INFO
initializer_feed = FeedInitializer(output_level=output_level)
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(output_level=output_level)
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(output_level=output_level)
sep_units = [
m.fs.scrub_sep,
m.fs.precip_sep,
]
initializer_mix = MixerInitializer(output_level=output_level)
mix_units = [
m.fs.precip_sx_mixer,
]
initializer_leach = LeachingTrainInitializer(output_level=output_level)
leach_units = [
m.fs.leach,
]
initializer_sx = SolventExtractionInitializer(output_level=output_level)
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,
]
# The BT Initializer will be used for any units not handled by the above initializers
initializer_bt = BlockTriangularizationInitializer()
# Initialize units using their respective initializers
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)
Step 6.2: Add solver#
Define a solve function using the ipopt solver.
def solve(m):
solver = SolverFactory("ipopt")
results = solver.solve(m, tee=True)
Step 6.3 Solve the system#
Scale, initialize, and solve the model.
# # Sets scaling factors
# set_scaling(m)
# # Makes a transformation object that can be applied to models
# scaling = TransformationFactory("core.scale_model")
# # Creates a scaled copy of "m"
# scaled_model = scaling.create_using(m, rename=False)
# # Initializes the scaled model
# initialize_system(scaled_model)
# # Solves the scaled model
# solve(scaled_model)
# # Convert scaled model back to the base model, m
# # E.g. The results should show temperature values of 300K, rather than 3K
# scaling.propagate_solution(scaled_model, m)
Step 7: Calculate performance metrics#
# # Define stoichiometric parameters
# 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,
# }
# 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),
# }
# # Total REE recovery calculation
# product = value(
# units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "Y"]
# * molar_mass["Y2O3"]
# * REE_mass_frac["Y2O3"],
# to_units=units.kg / units.hr,
# )
# + units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "La"]
# * molar_mass["La2O3"]
# * REE_mass_frac["La2O3"],
# to_units=units.kg / units.hr,
# )
# + units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "Ce"]
# * molar_mass["Ce2O3"]
# * REE_mass_frac["Ce2O3"],
# to_units=units.kg / units.hr,
# )
# + units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "Pr"]
# * molar_mass["Pr2O3"]
# * REE_mass_frac["Pr2O3"],
# to_units=units.kg / units.hr,
# )
# + units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "Nd"]
# * molar_mass["Nd2O3"]
# * REE_mass_frac["Nd2O3"],
# to_units=units.kg / units.hr,
# )
# + units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "Sm"]
# * molar_mass["Sm2O3"]
# * REE_mass_frac["Sm2O3"],
# to_units=units.kg / units.hr,
# )
# + units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "Gd"]
# * molar_mass["Gd2O3"]
# * REE_mass_frac["Gd2O3"],
# to_units=units.kg / units.hr,
# )
# + units.convert(
# m.fs.roaster.flow_mol_comp_product[0, "Dy"]
# * molar_mass["Dy2O3"]
# * REE_mass_frac["Dy2O3"],
# to_units=units.kg / units.hr,
# )
# )
# feed_REE = sum(
# value(
# 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()
# )
# # REE recovery % = 100 * Mass flow of REEs in product / Mass flow of REEs in feed
# REE_recovery = 100 * product / feed_REE
# # Product purity % = 100 * Mass flow of REEs in product / Mass flow of product
# product_purity = (
# 100
# * product
# / value(
# units.convert(m.fs.roaster.flow_mass_product[0], to_units=units.kg / units.hr)
# )
# )
# print(f"Total REE recovery is {REE_recovery} %")
# print(f"Product purity is {product_purity} %REE")
We have also integrated a costing framework into this flowsheet that displays an itemized list of all the plant costs, including metrics like cost of recovery per kg of REE recovered and total annualized plant costs. For more details, refer to the following costing tutorials:
1.) Basic Costing Features: prommis/prommis
2.) Advanced Costing Features: prommis/prommis
3.) Integrating Costing into UKy Flowsheet: prommis/prommis