Source code for prommis.precipitate.precipitator

#####################################################################################################
# “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"""
Preliminary Precipitator Unit Model
===================================

Authors: Alejandro Garciadiego, Douglas Allan

The Precipitator Unit Model represents an Equilibrium reactor unit model with fixed partition coefficients.

Configuration Arguments
-----------------------

The precipitator unit model needs an aqueous property package which includes stoichiometric values for solids being
created in the precipitator and fixed separation coefficients of the solids.

Model Structure
---------------

The Precitator unit model has hard coded stream names (``aqueous`` and ``precipitate`` respectively). The Precipitator
model also has one inlet and two outlets named ``aqueous_inlet``, ``aqueous_outlet`` and ``precipitate_outlet`` respectively.

Additional Constraints
----------------------

The Precipitator unit adds two additional constraint to define the stochiometry and separation.

.. math:: n_{t,prec,c} = \frac{n_{t,aq_in,c} - n_{t,aq_out,c}}{S_{comp}}

where :math:`n_{t,prec,c}` is the outlet precipitation of component c, :math:`n_{t,aq_in,c}` is the inlet of component c in
the aqueous phase, :math:`n_{t,aq_in,c}` is the outlet of component c in the aqueous phase at time :math:`t`, divided by the
stoichiometric parameter of component c :math:`S_{comp}`

.. math:: n_{t,aq_out,c} = n_{t,aq_in,c} * (1 - split_{c})

where :math:`split_{c}` is the fixed recovery fraction of component c; this factor can be a parameter or ideally a variable
solved by a surrogate or a model equation.

"""

# Import Pyomo libraries
from pyomo.common.collections import ComponentMap
from pyomo.common.config import Bool, ConfigBlock, ConfigValue

import idaes.logger as idaeslog

# Import IDAES cores
from idaes.core import (
    ControlVolume0DBlock,
    UnitModelBlockData,
    declare_process_block_class,
    useDefault,
)
from idaes.core.scaling import CustomScalerBase
from idaes.core.util.config import (
    is_physical_parameter_block,
    is_reaction_parameter_block,
)
from idaes.core.util.exceptions import ConfigurationError

_log = idaeslog.getLogger(__name__)


[docs] class PrecipitatorScaler(CustomScalerBase): """ Scaler for the Precipitator unit model. """
[docs] def variable_scaling_routine( self, model, overwrite: bool = False, submodel_scalers: ComponentMap = None ): """ Variable scaling routine for Precipitator. Args: model: instance of Precipitator to be scaled overwrite: whether to overwrite existing scaling factors submodel_scalers: ComponentMap of Scalers to use for sub-models, keyed by submodel local name Returns: None """ self.call_submodel_scaler_method( submodel=model.cv_aqueous, submodel_scalers=submodel_scalers, method="variable_scaling_routine", overwrite=overwrite, ) self.call_submodel_scaler_method( submodel=model.precipitate_state_block, submodel_scalers=submodel_scalers, method="variable_scaling_routine", overwrite=overwrite, )
# No unit model level variables
[docs] def constraint_scaling_routine( self, model, overwrite: bool = False, submodel_scalers: ComponentMap = None ): """ Constraint scaling routine for Precipitator. Args: model: instance of Precipitator to be scaled overwrite: whether to overwrite existing scaling factors submodel_scalers: ComponentMap of Scalers to use for sub-models, keyed by submodel local name Returns: None """ self.call_submodel_scaler_method( submodel=model.cv_aqueous, submodel_scalers=submodel_scalers, method="constraint_scaling_routine", overwrite=overwrite, ) self.call_submodel_scaler_method( submodel=model.precipitate_state_block, submodel_scalers=submodel_scalers, method="constraint_scaling_routine", overwrite=overwrite, ) # TODO remove when old precipitate liquid properties are removed if hasattr(model, "vol_balance"): for condata in model.vol_balance.values(): self.scale_constraint_by_nominal_value(condata, overwrite=overwrite) for (t, j), condata in model.precipitate_generation.items(): self.scale_constraint_by_component( condata, model.precipitate_state_block[t].flow_mol_comp[j], overwrite=overwrite, ) for condata in model.aqueous_depletion.values(): self.scale_constraint_by_nominal_value(condata, overwrite=overwrite)
@declare_process_block_class("Precipitator") class PrecipitatorData(UnitModelBlockData): """ """ default_scaler = PrecipitatorScaler CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "property_package_aqueous", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for aqueous control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""", ), ) CONFIG.declare( "property_package_args_aqueous", ConfigBlock( implicit=True, description="Arguments to use for constructing aqueous property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "property_package_precipitate", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for precipitate control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""", ), ) CONFIG.declare( "property_package_args_precipitate", ConfigBlock( implicit=True, description="Arguments to use for constructing precipitate property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "has_equilibrium_reactions", ConfigValue( default=False, domain=Bool, description="Equilibrium reaction construction flag", doc="""Indicates whether terms for equilibrium controlled reactions should be constructed, **default** - True. **Valid values:** { **True** - include equilibrium reaction terms, **False** - exclude equilibrium reaction terms.}""", ), ) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=Bool, description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""", ), ) CONFIG.declare( "has_heat_of_reaction", ConfigValue( default=False, domain=Bool, description="Heat of reaction term construction flag", doc="""Indicates whether terms for heat of reaction terms should be constructed, **default** - False. **Valid values:** { **True** - include heat of reaction terms, **False** - exclude heat of reaction terms.}""", ), ) CONFIG.declare( "reaction_package", ConfigValue( default=None, domain=is_reaction_parameter_block, description="Reaction package to use for control volume", doc="""Reaction parameter object used to define reaction calculations, **default** - None. **Valid values:** { **None** - no reaction package, **ReactionParameterBlock** - a ReactionParameterBlock object.}""", ), ) CONFIG.declare( "reaction_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing reaction packages", doc="""A ConfigBlock with arguments to be passed to a reaction block(s) and used when constructing these, **default** - None. **Valid values:** { see reaction package for documentation.}""", ), ) # TODO remove when old precipitate liquid properties are removed CONFIG.declare( "make_volume_balance_constraint", ConfigValue( default=False, domain=Bool, description="Flag whether to create volume balance constraint", doc="Flag whether to create legacy volume balance constraint", ), ) def build(self): """ Build method for precipitator unit model. """ # Call UnitModel.build to setup dynamics super(PrecipitatorData, self).build() # Add Control Volume self.cv_aqueous = ControlVolume0DBlock( dynamic=False, has_holdup=False, property_package=self.config.property_package_aqueous, property_package_args=self.config.property_package_args_aqueous, ) # Add inlet and outlet state blocks to control volume self.cv_aqueous.add_state_blocks(has_phase_equilibrium=False) # --------------------------------------------------------------------- # Add single state block for vapor phase tmp_dict = dict(**self.config.property_package_args_precipitate) tmp_dict["has_phase_equilibrium"] = False tmp_dict["defined_state"] = False self.precipitate_state_block = ( self.config.property_package_precipitate.build_state_block( self.flowsheet().time, doc="Vapor phase properties", **tmp_dict ) ) # --------------------------------------------------------------------- # Check flow basis is compatible # TODO : Could add code to convert flow bases, but not now t_init = self.flowsheet().time.first() if ( self.precipitate_state_block[t_init].get_material_flow_basis() != self.cv_aqueous.properties_out[t_init].get_material_flow_basis() ): raise ConfigurationError( f"{self.name} Solid and aqueous property packages must use the " f"same material flow basis." ) # add ports self.add_inlet_port(block=self.cv_aqueous, name="aqueous_inlet") self.add_outlet_port(block=self.cv_aqueous, name="aqueous_outlet") self.add_outlet_port( block=self.precipitate_state_block, name="precipitate_outlet" ) prop_aq = self.config.property_package_aqueous prop_s = self.config.property_package_precipitate # TODO remove when old precipitate liquid properties are removed if self.config.make_volume_balance_constraint: @self.Constraint(self.flowsheet().time, doc="volume balance equation.") def vol_balance(blk, t): return blk.cv_aqueous.properties_out[t].flow_vol == ( blk.cv_aqueous.properties_in[t].flow_vol ) @self.Constraint( self.flowsheet().time, prop_s.component_list, doc="Mass balance equations precipitate.", ) def precipitate_generation(blk, t, comp): return blk.precipitate_state_block[t].flow_mol_comp[comp] == ( ( blk.cv_aqueous.properties_in[t].flow_mol_comp[prop_s.react[comp]] - blk.cv_aqueous.properties_out[t].flow_mol_comp[prop_s.react[comp]] ) / prop_s.stoich[comp] ) @self.Constraint( self.flowsheet().time, prop_aq.dissolved_elements, doc="Mass balance equations aqueous.", ) def aqueous_depletion(blk, t, comp): return blk.cv_aqueous.properties_out[t].conc_mass_comp[ comp ] * blk.cv_aqueous.properties_out[t].flow_vol == ( blk.cv_aqueous.properties_in[t].conc_mass_comp[comp] * blk.cv_aqueous.properties_in[t].flow_vol * (1 - prop_aq.split[comp] / 100) )