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#

simplified_uky.png

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#

uky_flowsheet.png

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.

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,
    Block,
    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,
    UnitModelBlockData,
    UnitModelCostingBlock,
)
import logging

# 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 import DiagnosticsToolbox
from idaes.core.util.model_statistics import degrees_of_freedom
from idaes.models.properties.modular_properties.base.generic_property import (
    GenericParameterBlock,
    ModularPropertiesScaler,
)

# 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.properties import HClStrippingParameterBlock
from prommis.properties.hcl_stripping_properties import HClStrippingPropertiesScaler
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.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

# 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 unit models 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 unit models 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()
m.fs.HCl_stripping_params = HClStrippingParameterBlock()

# 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=False,
    create_hydrostatic_pressure_terms=False,
)

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.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,
)

# 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.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.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,
)

# 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.HCl_stripping_params)
m.fs.acid_feed2 = Feed(property_package=m.fs.HCl_stripping_params)
m.fs.acid_feed3 = Feed(property_package=m.fs.HCl_stripping_params)

# 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 unit models 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_solid = PrecipitateParameters()  # Components are rare earth oxalates

# Precipitation unit model
m.fs.precipitator = Precipitator(
    property_package_aqueous=m.fs.HCl_stripping_params,
    property_package_precipitate=m.fs.properties_solid,
    make_volume_balance_constraint=False,
)

# 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.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,
)

# Define outlets from the flowsheet
m.fs.precip_purge = Product(property_package=m.fs.HCl_stripping_params)

Step 2.5: Create unit models 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.scrubber_HCl_leach_translator = TranslatorHClLeach(
    inlet_property_package=m.fs.HCl_stripping_params,
    outlet_property_package=m.fs.leach_soln,
)
m.fs.cleaner_HCl_leach_translator = TranslatorHClLeach(
    inlet_property_package=m.fs.HCl_stripping_params,
    outlet_property_package=m.fs.leach_soln,
)

Step 2.6: Create unit models 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.HCl_stripping_params,
    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_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)

Step 4: Set the operating conditions#

Now specify the operating conditions for the flowsheet such that we have a square problem (DOF = 0).

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

# 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.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.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)

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

# Fix the conditions of the HCl acid feeds
# 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)

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

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

# Fix the cleaner recycle split fraction
m.fs.cleaner_sep.split_fraction[:, "recycle"].fix(0.9)

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

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

# 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. Since this flowsheet contains some very small concentrations and flows, certain variables must be scaled accordingly. In more conventional flowsheets, simply applying the default scaling (shown below) may suffice.

# Update the default scaling factors
liquid_properties_scaler = m.fs.HCl_stripping_params.default_state_scaler_class()
vapor_properties_scaler = m.fs.prop_gas.default_state_scaler_class()

liquid_properties_scaler.default_scaling_factors["flow_vol"] = 1
liquid_properties_scaler.default_scaling_factors["conc_mass_comp[H]"] = 1e-3
liquid_properties_scaler.default_scaling_factors["conc_mass_comp[Cl]"] = 1e-5
vapor_properties_scaler.default_scaling_factors["flow_mol_phase"] = 1 / 0.00781

m.fs.HCl_stripping_params.default_state_scaler_object = liquid_properties_scaler
m.fs.prop_gas.default_state_scaler_object = vapor_properties_scaler

# Update the max and min scaling factors
# Default max is 1e10 and default min is 1e-10
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()

# Apply the default scaling to each unit model
for blk in m.fs.component_data_objects(ctype=Block, descend_into=False):
    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
            )
Scaling fs.leach
Scaling fs.sl_sep1
Scaling fs.leach_mixer
Scaling fs.leach_liquid_feed
Scaling fs.leach_solid_feed
Scaling fs.leach_filter_cake
Scaling fs.leach_filter_cake_liquid
Scaling fs.solex_rougher_load
Scaling fs.solex_rougher_scrub
Scaling fs.solex_rougher_strip
Scaling fs.rougher_sep
Scaling fs.rougher_mixer
Scaling fs.load_sep
Scaling fs.scrub_sep
Scaling fs.solex_cleaner_load
Scaling fs.solex_cleaner_strip
Scaling fs.cleaner_sep
Scaling fs.cleaner_mixer
Scaling fs.leach_sx_mixer
Scaling fs.rougher_org_make_up
Scaling fs.cleaner_org_make_up
Scaling fs.acid_feed1
Scaling fs.acid_feed2
Scaling fs.acid_feed3
Scaling fs.rougher_organic_purge
Scaling fs.cleaner_organic_purge
Scaling fs.precipitator
Scaling fs.sl_sep2
Scaling fs.precip_sep
Scaling fs.precip_sx_mixer
Scaling fs.precip_purge
Scaling fs.scrubber_HCl_leach_translator
Scaling fs.cleaner_HCl_leach_translator
Scaling fs.roaster
Scaling fs.leaching_sol_feed_expanded
Scaling fs.leaching_liq_feed_expanded
Scaling fs.leaching_feed_mixture_expanded
Scaling fs.leaching_solid_outlet_expanded
Scaling fs.leaching_liquid_outlet_expanded
Scaling fs.sl_sep1_solid_outlet_expanded
Scaling fs.sl_sep1_retained_liquid_outlet_expanded
Scaling fs.sl_sep1_liquid_outlet_expanded
Scaling fs.sx_rougher_load_aq_feed_expanded
Scaling fs.sx_rougher_org_feed_expanded
Scaling fs.sx_rougher_mixed_org_recycle_expanded
Scaling fs.sx_rougher_load_aq_outlet_expanded
Scaling fs.sx_rougher_load_aq_recycle_expanded
Scaling fs.sx_rougher_load_org_outlet_expanded
Scaling fs.sx_rougher_scrub_acid_feed_expanded
Scaling fs.sx_rougher_scrub_aq_outlet_expanded
Scaling fs.sx_rougher_scrub_aq_translator_expanded
Scaling fs.translator_scrub_recycle_expanded
Scaling fs.sx_rougher_scrub_org_outlet_expanded
Scaling fs.sx_rougher_strip_acid_feed_expanded
Scaling fs.sx_rougher_strip_org_outlet_expanded
Scaling fs.sx_rougher_strip_org_purge_expanded
Scaling fs.sx_rougher_strip_org_recycle_expanded
Scaling fs.sx_rougher_strip_aq_outlet_expanded
Scaling fs.sx_cleaner_load_aq_feed_expanded
Scaling fs.sx_cleaner_org_feed_expanded
Scaling fs.sx_cleaner_mixed_org_recycle_expanded
Scaling fs.sx_cleaner_load_aq_outlet_translator_expanded
Scaling fs.sx_cleaner_load_translator_leach_sx_mixer_expanded
Scaling fs.sx_cleaner_strip_acid_feed_expanded
Scaling fs.sx_cleaner_load_org_outlet_expanded
Scaling fs.sx_cleaner_strip_org_outlet_expanded
Scaling fs.sx_cleaner_strip_org_purge_expanded
Scaling fs.sx_cleaner_strip_org_recycle_expanded
Scaling fs.sx_cleaner_strip_aq_precip_expanded
Scaling fs.precip_solid_outlet_expanded
Scaling fs.precip_aq_sl_sep2_expanded
Scaling fs.sl_sep2_solid_outlet_expanded
Scaling fs.sl_sep2_liquid_outlet_expanded
Scaling fs.sl_sep2_retained_liquid_roaster_expanded
Scaling fs.sl_sep2_aq_purge_expanded
Scaling fs.sl_sep2_aq_recycle_expanded

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.

# 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: 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,
    },
}
# Guesses for the organic SX Rougher loading stream conditions
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,
    },
}
# Guesses for the aqueous SX Rougher loading stream conditions
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,
    },
}
# Guesses for the organic SX Cleaner loading stream conditions
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,
    },
}
# Guesses for the aqueous SX Cleaner loading stream conditions
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 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
idaeslog.getLogger("idaes").setLevel(logging.ERROR)
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,
]

# 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)
2026-06-30 10:48:42 [INFO] idaes.__main__: Initializing fs.leach_solid_feed
2026-06-30 10:48:42 [INFO] idaes.__main__: Initializing fs.leach_liquid_feed
2026-06-30 10:48:42 [INFO] idaes.__main__: Initializing fs.solex_rougher_load
2026-06-30 10:48:44 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:44 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:44 [INFO] idaes.__main__: Initializing fs.rougher_org_make_up
2026-06-30 10:48:44 [INFO] idaes.__main__: Initializing fs.acid_feed1
2026-06-30 10:48:44 [INFO] idaes.__main__: Initializing fs.acid_feed2
2026-06-30 10:48:44 [INFO] idaes.__main__: Initializing fs.solex_cleaner_load
2026-06-30 10:48:44 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:45 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:45 [INFO] idaes.__main__: Initializing fs.cleaner_org_make_up
2026-06-30 10:48:45 [INFO] idaes.__main__: Initializing fs.acid_feed3
2026-06-30 10:48:45 [INFO] idaes.__main__: Initializing fs.leach
2026-06-30 10:48:45 [INFO] idaes.init.fs.leach.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:45 [INFO] idaes.init.fs.leach.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:45 [INFO] idaes.__main__: Initializing fs.load_sep
2026-06-30 10:48:45 [INFO] idaes.__main__: Initializing fs.solex_rougher_scrub
2026-06-30 10:48:45 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:46 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:46 [INFO] idaes.__main__: Initializing fs.cleaner_HCl_leach_translator
2026-06-30 10:48:46 [INFO] idaes.__main__: Initializing fs.solex_cleaner_strip
2026-06-30 10:48:46 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:46 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:46 [INFO] idaes.__main__: Initializing fs.sl_sep1
2026-06-30 10:48:46 [INFO] idaes.__main__: Initializing fs.scrub_sep
2026-06-30 10:48:46 [INFO] idaes.init.fs.scrub_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-06-30 10:48:46 [INFO] idaes.__main__: Initializing fs.solex_rougher_strip
2026-06-30 10:48:47 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:47 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.cleaner_sep
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.precipitator
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.leach_filter_cake
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.leach_filter_cake_liquid
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.leach_sx_mixer
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.scrubber_HCl_leach_translator
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.rougher_sep
2026-06-30 10:48:47 [INFO] idaes.__main__: Initializing fs.cleaner_mixer
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.cleaner_organic_purge
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.sl_sep2
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.precip_sep
2026-06-30 10:48:48 [INFO] idaes.init.fs.precip_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.leach_mixer
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.rougher_mixer
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.rougher_organic_purge
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.roaster
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.precip_purge
2026-06-30 10:48:48 [INFO] idaes.__main__: Initializing fs.precip_sx_mixer
2026-06-30 10:48:49 [INFO] idaes.init.fs.precip_sx_mixer: Initialization Complete: optimal - <undefined>
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.leach_solid_feed
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.leach_liquid_feed
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.rougher_org_make_up
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.acid_feed1
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.acid_feed2
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.cleaner_org_make_up
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.acid_feed3
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.solex_cleaner_load
2026-06-30 10:48:49 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:49 [INFO] idaes.init.fs.solex_cleaner_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:49 [INFO] idaes.__main__: Initializing fs.solex_rougher_load
2026-06-30 10:48:50 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:50 [INFO] idaes.init.fs.solex_rougher_load.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:50 [INFO] idaes.__main__: Initializing fs.leach
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Sc2O3]' to a numeric value
`-3.487829219263003e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Y2O3]' to a numeric value
`-2.9328444750486306e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[La2O3]' to a numeric value
`-1.3652786271046261e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Ce2O3]' to a numeric value
`-5.300645252152697e-08` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Pr2O3]' to a numeric value
`-5.723522969621362e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Nd2O3]' to a numeric value
`-1.3677900847173715e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Sm2O3]' to a numeric value
`-6.650814070758785e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Gd2O3]' to a numeric value
`-9.501980349082598e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,1].conversion_comp[Dy2O3]' to a numeric value
`-1.313980105642459e-06` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Y2O3]' to a numeric value
`-1.0993118621454631e-06` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[La2O3]' to a numeric value
`-5.2593056019732e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Ce2O3]' to a numeric value
`-2.204297361134597e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Pr2O3]' to a numeric value
`-2.1201175585412365e-06` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Nd2O3]' to a numeric value
`-5.26847447854096e-07` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Sm2O3]' to a numeric value
`-2.459302437965299e-06` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Gd2O3]' to a numeric value
`-3.502171921830392e-06` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Dy2O3]' to a numeric value
`-4.832853881187739e-06` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.leach.mscontactor.solid[0.0,2].conversion_comp[Sc2O3]' to a numeric value
`-1.3022891222813993e-06` outside the bounds (0, 0.99999999).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
2026-06-30 10:48:50 [INFO] idaes.init.fs.leach.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:50 [INFO] idaes.init.fs.leach.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:50 [INFO] idaes.__main__: Initializing fs.solex_cleaner_strip
2026-06-30 10:48:50 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:51 [INFO] idaes.init.fs.solex_cleaner_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:51 [INFO] idaes.__main__: Initializing fs.cleaner_HCl_leach_translator
2026-06-30 10:48:51 [INFO] idaes.__main__: Initializing fs.solex_rougher_scrub
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].conc_mass_comp[Al]' to a
numeric value `-8.813627952784652e-08` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].conc_mol_comp[Al]' to a
numeric value `-3.266472030144476e-12` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].flow_mol_comp[Al]' to a
numeric value `-3.266484305264328e-13` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].conc_mass_comp[Ca]' to a
numeric value `-1.5760625470793493e-08` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].conc_mol_comp[Ca]' to a
numeric value `-3.9324758829293394e-13` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].flow_mol_comp[Ca]' to a
numeric value `-3.9324880157787234e-14` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].conc_mass_comp[Ce]' to a
numeric value `-3.6579450075327737e-07` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].conc_mol_comp[Ce]' to a
numeric value `-2.6106543788949036e-12` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_scrub.mscontactor.aqueous[0.0,1].flow_mol_comp[Ce]' to a
numeric value `-2.610654747936536e-13` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
2026-06-30 10:48:51 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:51 [INFO] idaes.init.fs.solex_rougher_scrub.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:51 [INFO] idaes.__main__: Initializing fs.load_sep
2026-06-30 10:48:51 [INFO] idaes.__main__: Initializing fs.sl_sep1
2026-06-30 10:48:51 [INFO] idaes.__main__: Initializing fs.precipitator
2026-06-30 10:48:52 [INFO] idaes.__main__: Initializing fs.leach_sx_mixer
2026-06-30 10:48:52 [INFO] idaes.__main__: Initializing fs.cleaner_sep
2026-06-30 10:48:52 [INFO] idaes.__main__: Initializing fs.solex_rougher_strip
WARNING (W1002): Setting Var
'fs.solex_rougher_strip.mscontactor.aqueous[0.0,1].conc_mass_comp[Fe]' to a
numeric value `-7.343144240836275e-09` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_strip.mscontactor.aqueous[0.0,1].conc_mol_comp[Fe]' to a
numeric value `-1.3147731627563518e-13` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
WARNING (W1002): Setting Var
'fs.solex_rougher_strip.mscontactor.aqueous[0.0,1].flow_mol_comp[Fe]' to a
numeric value `-4.4372838736705934e-13` outside the bounds (1e-30, None).
    See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002
2026-06-30 10:48:52 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Stream Initialization Completed.
2026-06-30 10:48:52 [INFO] idaes.init.fs.solex_rougher_strip.mscontactor: Initialization Completed, optimal - <undefined>
2026-06-30 10:48:52 [INFO] idaes.__main__: Initializing fs.scrub_sep
2026-06-30 10:48:52 [INFO] idaes.init.fs.scrub_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-06-30 10:48:52 [INFO] idaes.__main__: Initializing fs.sl_sep2
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.cleaner_mixer
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.rougher_sep
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.scrubber_HCl_leach_translator
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.precip_sep
2026-06-30 10:48:53 [INFO] idaes.init.fs.precip_sep: Initialization Step 2 Complete: optimal - <undefined>
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.rougher_mixer
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.leach_mixer
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.precip_sx_mixer
2026-06-30 10:48:53 [INFO] idaes.init.fs.precip_sx_mixer: Initialization Complete: optimal - <undefined>
WARNING: Direct failed to converge in 1 iterations
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.leach_filter_cake
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.leach_filter_cake_liquid
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.rougher_organic_purge
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.cleaner_organic_purge
2026-06-30 10:48:53 [INFO] idaes.__main__: Initializing fs.roaster
2026-06-30 10:48:54 [INFO] idaes.__main__: Initializing fs.precip_purge

Step 6.2: Add solver#

Solve the system using the ipopt_v2 solver.

solver = SolverFactory("ipopt_v2")
# solver_obj.options.constr_viol_tol = 1e-8
results = solver.solve(m, tee=True)
Ipopt 3.13.2: 

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

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

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

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

Number of nonzeros in equality constraint Jacobian...:     4771
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:      650

Total number of variables............................:     1509
                     variables with only lower bounds:     1138
                variables with lower and upper bounds:       55
                     variables with only upper bounds:        0
Total number of equality constraints.................:     1509
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  0.0000000e+00 5.27e+03 1.00e+00  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  0.0000000e+00 1.40e+04 3.40e+03  -1.0 1.52e+05    -  9.10e-01 7.68e-01h  1
   2  0.0000000e+00 2.72e+03 4.70e+02  -1.0 6.09e+04    -  9.81e-01 9.05e-01h  1
   3  0.0000000e+00 4.42e+01 1.08e+03  -1.0 7.67e+03    -  9.90e-01 9.90e-01h  1
   4  0.0000000e+00 2.42e-01 3.92e+03  -1.0 9.61e+01    -  9.91e-01 9.95e-01h  1
   5  0.0000000e+00 5.52e-08 7.75e-02  -1.0 5.23e-01    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 5

                                   (scaled)                 (unscaled)
Objective...............:   0.0000000000000000e+00    0.0000000000000000e+00
Dual infeasibility......:   0.0000000000000000e+00    0.0000000000000000e+00
Constraint violation....:   1.2145208220159120e-12    5.5180862545967102e-08
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   1.2145208220159120e-12    5.5180862545967102e-08


Number of objective function evaluations             = 6
Number of objective gradient evaluations             = 6
Number of equality constraint evaluations            = 6
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 6
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 5
Total CPU secs in IPOPT (w/o function evaluations)   =      0.023
Total CPU secs in NLP function evaluations           =      0.004

EXIT: Optimal Solution Found.

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")
Total REE recovery is 22.957071291335627 %
Product purity is 76.08765278050879 %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