#####################################################################################################
# “PrOMMiS” was produced under the DOE Process Optimization and Modeling for Minerals Sustainability
# (“PrOMMiS”) initiative, and is copyright (c) 2023-2026 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory, et al. All rights reserved.
# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license information.
#####################################################################################################
r"""
Initial property package for the organic phase solution of the solvent extraction
unit operation.
----------------------------------------------------------------------------------
Author: Arkoprabho Dasgupta
"""
from pyomo.environ import Param, Var, units, Constraint, PositiveReals, Reals
from idaes.core import (
Component,
MaterialFlowBasis,
PhysicalParameterBlock,
StateBlock,
StateBlockData,
declare_process_block_class,
LiquidPhase,
)
from idaes.core.util.initialization import fix_state_vars
from idaes.core.util.misc import add_object_reference
from idaes.core.scaling import CustomScalerBase
__author__ = "Arkoprabho Dasgupta, Douglas Allan"
ree_list = ["Sc_o", "Y_o", "La_o", "Ce_o", "Pr_o", "Nd_o", "Sm_o", "Gd_o", "Dy_o"]
[docs]
class OrganicSolventPropertiesScaler(CustomScalerBase):
"""
Scaler for organic solvent solution.
"""
CONFIG = CustomScalerBase.CONFIG
DEFAULT_SCALING_FACTORS = {
"extractant_dosage": 1 / 5,
"flow_vol": 1e-2,
"pressure": 1e-5,
"temperature": 1 / 300,
"conc_mass_comp[Kerosene]": 1e-6,
"conc_mass_comp[DEHPA]": 1e-5,
"conc_mass_comp[Al_o]": 1e-1,
"conc_mass_comp[Ca_o]": 1e-1,
"conc_mass_comp[Fe_o]": 1e-1,
}
for ree in ree_list:
DEFAULT_SCALING_FACTORS[f"conc_mass_comp[{ree}]"] = 1e-2
[docs]
def variable_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
# Scale state variables
self.scale_variable_by_default(model.flow_vol, overwrite=overwrite)
self.scale_variable_by_default(model.pressure, overwrite=overwrite)
self.scale_variable_by_default(model.temperature, overwrite=overwrite)
for idx, var in model.conc_mass_comp.items():
self.scale_variable_by_default(var, overwrite=overwrite)
# Scale other variables
if model.is_property_constructed("extractant_dosage"):
self.scale_variable_by_default(model.extractant_dosage, overwrite=overwrite)
for idx, vardata in model.conc_mol_comp.items():
self.scale_variable_by_definition_constraint(
vardata, model.molar_concentration_constraint[idx], overwrite=overwrite
)
[docs]
def constraint_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
for idx, condata in model.molar_concentration_constraint.items():
self.scale_constraint_by_component(
condata, model.conc_mass_comp[idx], overwrite=overwrite
)
if model.is_property_constructed("extractant_dosage"):
sf = self.get_scaling_factor(model.extractant_dosage)
self.set_constraint_scaling_factor(
model.extractant_dosage_eqn, sf, overwrite=overwrite
)
if model.is_property_constructed("kerosene_concentration"):
sf = self.get_scaling_factor(model.conc_mass_comp["Kerosene"])
self.set_constraint_scaling_factor(
model.kerosene_concentration, sf, overwrite=overwrite
)
[docs]
@declare_process_block_class("REESolExOgParameters")
class REESolExOgParameterData(PhysicalParameterBlock):
"""
This is a property package for the organic phase solution of the solvent extraction
unit operation of the University of Kentucky pilot plant flowsheet.
This includes the following components:
* Solvent: Kerosene
* Extractant: DEHPA
* Rare Earths: Sc, Y, La, Ce, Pr, Nd, Sm, Gd, Dy
* Impurities: Al, Ca, Fe
Kerosene is not considered to be involved in any reaction.
"""
[docs]
def build(self):
super().build()
self.organic = LiquidPhase()
# Solvent
self.Kerosene = Component()
# Extractant
self.DEHPA = Component()
# Contaminants
self.Al_o = Component()
self.Ca_o = Component()
self.Fe_o = Component()
# REEs
self.Sc_o = Component()
self.Y_o = Component()
self.La_o = Component()
self.Ce_o = Component()
self.Pr_o = Component()
self.Nd_o = Component()
self.Sm_o = Component()
self.Gd_o = Component()
self.Dy_o = Component()
self.mw = Param(
self.component_list,
units=units.kg / units.mol,
initialize={
"Kerosene": 170e-3,
"DEHPA": 322.431e-3,
"Sc_o": 44.946e-3,
"Y_o": 88.905e-3,
"La_o": 138.905e-3,
"Ce_o": 140.116e-3,
"Pr_o": 140.907e-3,
"Nd_o": 144.242e-3,
"Sm_o": 150.36e-3,
"Gd_o": 157.25e-3,
"Dy_o": 162.50e-3,
"Al_o": 26.982e-3,
"Ca_o": 40.078e-3,
"Fe_o": 55.845e-3,
},
)
# density of Kerosene
self.dens_mass = Param(
initialize=0.82,
units=units.kg / units.litre,
mutable=True,
)
# Heat capacity of kerosene
self.cp_mol = Param(
mutable=True,
initialize=341.7,
doc="Molar heat capacity of kerosene [J/mol.K]",
units=units.J / units.mol / units.K,
)
self.temperature_ref = Param(
within=PositiveReals,
mutable=True,
default=298.15,
doc="Reference temperature [K]",
units=units.K,
)
self._state_block_class = REESolExOgStateBlock
class _REESolExOgStateBlock(StateBlock):
default_scaler = OrganicSolventPropertiesScaler
def fix_initialization_states(self):
fix_state_vars(self)
for sbd in self.values():
if not sbd.config.defined_state:
sbd.conc_mass_comp["Kerosene"].unfix()
[docs]
@declare_process_block_class("REESolExOgStateBlock", block_class=_REESolExOgStateBlock)
class REESolExOgStateBlockData(StateBlockData):
"""
State block for organic phase solution of the solvent extraction process.
"""
default_scaler = OrganicSolventPropertiesScaler
[docs]
def build(self):
super().build()
self.conc_mass_comp = Var(
self.params.component_list,
units=units.mg / units.L,
initialize=1e-8,
bounds=(1e-20, None),
)
self.flow_vol = Var(
units=units.L / units.hour, initialize=1e-5, bounds=(1e-8, None)
)
self.conc_mol_comp = Var(
self.params.component_list,
units=units.mol / units.L,
initialize=1e-5,
bounds=(1e-20, None),
)
self.temperature = Var(
domain=Reals,
initialize=298.15,
bounds=(298.1, None),
doc="State temperature [K]",
units=units.K,
)
self.pressure = Var(
domain=Reals,
initialize=101325.0,
bounds=(1e3, 1e6),
doc="State pressure [Pa]",
units=units.Pa,
)
self.extractant_dosage = Var(
domain=Reals,
initialize=1.0,
bounds=(1e-8, None),
doc="Extractant dosage v/v%",
units=units.dimensionless,
)
@self.Constraint()
def extractant_dosage_eqn(b):
return (
b.extractant_dosage
== (b.conc_mass_comp["DEHPA"] / (975.8e3 * units.mg / units.L)) * 100
)
# Concentration conversion constraint
@self.Constraint(self.params.component_list)
def molar_concentration_constraint(b, j):
return (
units.convert(
b.conc_mol_comp[j] * b.params.mw[j], to_units=units.mg / units.litre
)
== b.conc_mass_comp[j]
)
if not self.config.defined_state:
# Concentration of kerosene based on assumed density
self.kerosene_concentration = Constraint(
expr=self.conc_mass_comp["Kerosene"] == 8.2e5 * units.mg / units.L
)
[docs]
def get_material_flow_basis(self):
return MaterialFlowBasis.molar
def _dens_mass(self):
add_object_reference(self, "dens_mass", self.params.dens_mass)
[docs]
def get_material_flow_terms(self, p, j):
if j == "Kerosene":
return self.flow_vol * self.params.dens_mass / self.params.mw[j]
else:
return units.convert(
self.flow_vol * self.conc_mass_comp[j] / self.params.mw[j],
to_units=units.mol / units.hour,
)
[docs]
def get_material_density_terms(self, p, j):
if j == "Kerosene":
return units.convert(
self.params.dens_mass / self.params.mw[j],
to_units=units.mol / units.m**3,
)
else:
return units.convert(
self.conc_mass_comp[j] / self.params.mw[j],
to_units=units.mol / units.m**3,
)
[docs]
def define_state_vars(self):
return {
"flow_vol": self.flow_vol,
"conc_mass_comp": self.conc_mass_comp,
"temperature": self.temperature,
"pressure": self.pressure,
}