#####################################################################################################
# “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.
#####################################################################################################
"""
Initial property package for REE leach solutions from coal refuse.
Authors: Andrew Lee
"""
from pyomo.environ import (
Constraint,
Param,
PositiveReals,
Reals,
Set,
units,
Var,
value,
)
from idaes.core import (
Component,
MaterialFlowBasis,
Phase,
PhysicalParameterBlock,
StateBlock,
StateBlockData,
declare_process_block_class,
)
from idaes.core.util.initialization import fix_state_vars
from idaes.core.util.misc import add_object_reference
from idaes.core.scaling import CustomScalerBase
contaminant_list = ["Fe", "Al", "Ca"]
ree_list = ["Sc", "Y", "La", "Ce", "Pr", "Nd", "Sm", "Gd", "Dy"]
[docs]
class SulfuricAcidLeachingPropertiesScaler(CustomScalerBase):
"""
Scaler for leach solution property package.
"""
CONFIG = CustomScalerBase.CONFIG
DEFAULT_SCALING_FACTORS = {
"flow_vol": 1e-2,
"pressure": 1e-5,
"temperature": 1 / 300,
"conc_mass_comp[H2O]": 1e-6,
"conc_mass_comp[H]": 1e-1,
"conc_mass_comp[SO4]": 1e-2,
"conc_mass_comp[HSO4]": 1e-3,
"conc_mass_comp[Cl]": 1e-3,
# Use a large scaling factor for pH
# to encourage the solver to take
# smaller steps
"pH_phase": 10,
}
for ree in ree_list:
DEFAULT_SCALING_FACTORS[f"conc_mass_comp[{ree}]"] = 10
for contaminant in contaminant_list:
DEFAULT_SCALING_FACTORS[f"conc_mass_comp[{contaminant}]"] = 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
params = model.params
if model.is_property_constructed("pH_phase"):
self.scale_variable_by_default(
model.pH_phase["liquid"], overwrite=overwrite
)
for idx, var in model.conc_mol_comp.items():
sf = self.get_scaling_factor(model.conc_mass_comp[idx]) * value(
units.convert(params.mw[idx], to_units=units.mg / units.mol)
)
self.set_variable_scaling_factor(var, sf, 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("pH_phase"):
self.scale_constraint_by_component(
model.pH_constraint["liquid"],
model.conc_mol_comp["H"],
overwrite=overwrite,
)
if model.is_property_constructed("h2o_concentration"):
self.scale_constraint_by_component(
model.h2o_concentration,
model.conc_mass_comp["H2O"],
overwrite=overwrite,
)
if model.is_property_constructed("hso4_dissociation"):
sf = self.get_scaling_factor(
model.conc_mol_comp["H"]
) * self.get_scaling_factor(model.conc_mol_comp["SO4"])
self.set_constraint_scaling_factor(
model.hso4_dissociation, sf, overwrite=overwrite
)
# -----------------------------------------------------------------------------
# Leach solution property package
[docs]
@declare_process_block_class("SulfuricAcidLeachingParameters")
class SulfuricAcidLeachingParameterData(PhysicalParameterBlock):
"""
Property package for leach solution generated by West Kentucky No. 13 coal
and H2SO4.
Includes the following components:
* Acid components: H2O, H, HSO4, SO4
* Rare Earths: Sc, Y, La, Ce, Pr, Nd, Sm, Gd, Dy
* Impurities: Al, Ca, Fe
First dissociation of H2SO4 is assumed to be complete.
Second dissociation governed by equilibrium (Ka2) - inherent reaction.
"""
[docs]
def build(self):
super().build()
self.liquid = Phase()
# Solvent
self.H2O = Component()
# Acid related species
self.H = Component()
self.HSO4 = Component()
self.SO4 = Component()
self.Cl = Component()
# REEs
self.Sc = Component()
self.Y = Component()
self.La = Component()
self.Ce = Component()
self.Pr = Component()
self.Nd = Component()
self.Sm = Component()
self.Gd = Component()
self.Dy = Component()
# Contaminants
self.Al = Component()
self.Ca = Component()
self.Fe = Component()
self.mw = Param(
self.component_list,
units=units.kg / units.mol,
initialize={
"H2O": 18e-3,
"H": 1e-3,
"HSO4": 97e-3,
"SO4": 96e-3,
"Cl": 35.453e-3,
"Sc": 44.946e-3,
"Y": 88.905e-3,
"La": 138.905e-3,
"Ce": 140.116e-3,
"Pr": 140.907e-3,
"Nd": 144.242e-3,
"Sm": 150.36e-3,
"Gd": 157.25e-3,
"Dy": 162.50e-3,
"Al": 26.982e-3,
"Ca": 40.078e-3,
"Fe": 55.845e-3,
},
)
# Inherent reaction for partial dissociation of HSO4
self._has_inherent_reactions = True
self.inherent_reaction_idx = Set(initialize=["Ka2"])
self.inherent_reaction_stoichiometry = {
("Ka2", "liquid", "H"): 1,
("Ka2", "liquid", "HSO4"): -1,
("Ka2", "liquid", "SO4"): 1,
("Ka2", "liquid", "H2O"): 0,
("Ka2", "liquid", "Cl"): 0,
("Ka2", "liquid", "Sc"): 0,
("Ka2", "liquid", "Y"): 0,
("Ka2", "liquid", "La"): 0,
("Ka2", "liquid", "Ce"): 0,
("Ka2", "liquid", "Pr"): 0,
("Ka2", "liquid", "Nd"): 0,
("Ka2", "liquid", "Sm"): 0,
("Ka2", "liquid", "Gd"): 0,
("Ka2", "liquid", "Dy"): 0,
("Ka2", "liquid", "Al"): 0,
("Ka2", "liquid", "Ca"): 0,
("Ka2", "liquid", "Fe"): 0,
}
self.Ka2 = Param(
initialize=10**-1.99,
mutable=True,
units=units.mol / units.L,
)
# Assume dilute acid, density of pure water
self.dens_mass = Param(
initialize=1,
units=units.kg / units.litre,
mutable=True,
)
# Heat capacity of water
self.cp_mol = Param(
mutable=True,
initialize=75.327,
doc="Molar heat capacity of water [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 = SulfuricAcidLeachingStateBlock
class _SulfuricAcidLeachingStateBlock(StateBlock):
default_scaler = SulfuricAcidLeachingPropertiesScaler
def fix_initialization_states(self):
"""
Fixes state variables for state blocks.
Returns:
None
"""
# Fix state variables
fix_state_vars(self)
# Deactivate inherent reaction
# and water density constraints
for sbd in self.values():
if not sbd.config.defined_state:
sbd.conc_mass_comp["H2O"].unfix()
sbd.conc_mass_comp["HSO4"].unfix()
[docs]
@declare_process_block_class(
"SulfuricAcidLeachingStateBlock", block_class=_SulfuricAcidLeachingStateBlock
)
class SulfuricAcidLeachingStateBlockData(StateBlockData):
"""
State block for leach solution of West Kentucky No. 13 coal by H2SO4.
"""
default_scaler = SulfuricAcidLeachingPropertiesScaler
[docs]
def build(self):
super().build()
self.flow_vol = Var(
units=units.L / units.hour,
initialize=10,
bounds=(1e-8, None),
)
self.conc_mass_comp = Var(
self.params.component_list,
units=units.mg / units.L,
initialize=1e-8,
bounds=(1e-20, 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,
)
# 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 H2O based on assumed density
self.h2o_concentration = Constraint(
expr=self.conc_mass_comp["H2O"] == 1e6 * units.mg / units.L
)
# Equilibrium for partial dissociation of HSO4
self.hso4_dissociation = Constraint(
expr=self.conc_mol_comp["HSO4"] * self.params.Ka2
== self.conc_mol_comp["H"] * self.conc_mol_comp["SO4"]
)
def _dens_mass(self):
add_object_reference(self, "dens_mass", self.params.dens_mass)
def _pH_phase(self):
self.pH_phase = Var(
self.params.phase_list, initialize=2, doc="pH of the solution"
)
@self.Constraint(self.phase_list)
def pH_constraint(b, p):
return 10 ** (-b.pH_phase[p]) == b.conc_mol_comp["H"] * units.L / units.mol
[docs]
def get_material_flow_terms(self, p, j):
# Note conversion to mol/hour
if j == "H2O":
# Assume constant density of 1 kg/L
return units.convert(
self.flow_vol * self.params.dens_mass / self.params.mw[j],
to_units=units.mol / units.hour,
)
else:
# Need to convert from moles to mass
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 == "H2O":
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 get_material_flow_basis(self):
return MaterialFlowBasis.molar
[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,
}