Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dewatering and thickener unit models BSM2 #1010

Merged
merged 23 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions watertap/unit_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@
from .electrodialysis_1D import Electrodialysis1D
from .gac import GAC
from .ion_exchange_0D import IonExchange0D
from .thickener import Thickener
from .dewatering import DewateringUnit
163 changes: 163 additions & 0 deletions watertap/unit_models/dewatering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
###############################################################################
# WaterTAP Copyright (c) 2021, The Regents of the University of California,
# through Lawrence Berkeley National Laboratory, Oak Ridge National
# Laboratory, National Renewable Energy Laboratory, and National Energy
# Technology Laboratory (subject to receipt of any required approvals from
# the U.S. Dept. of Energy). All rights reserved.
#
# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license
# information, respectively. These files are also available online at the URL
# "https://github.com/watertap-org/watertap/"
#
###############################################################################
"""
Dewatering unit model for BSM2. Based on IDAES separator unit

Model based on

J. Alex, L. Benedetti, J.B. Copp, K.V. Gernaey, U. Jeppsson,
I. Nopens, M.N. Pons, C. Rosen, J.P. Steyer and
P. A. Vanrolleghem
Benchmark Simulation Model no. 2 (BSM2)
"""
# Import IDAES cores
from idaes.core import (
declare_process_block_class,
)
from idaes.models.unit_models.separator import SeparatorData, SplittingType

from idaes.core.util.tables import create_stream_table_dataframe
import idaes.logger as idaeslog

from pyomo.environ import (
Param,
units as pyunits,
Set,
)

from idaes.core.util.exceptions import (
ConfigurationError,
)

__author__ = "Alejandro Garciadiego"


# Set up logger
_log = idaeslog.getLogger(__name__)


@declare_process_block_class("DewateringUnit")
class DewateringData(SeparatorData):
"""
Dewatering unit block for BSM2
"""

agarciadiego marked this conversation as resolved.
Show resolved Hide resolved
CONFIG = SeparatorData.CONFIG()
CONFIG.outlet_list = ["underflow", "overflow"]
CONFIG.split_basis = SplittingType.componentFlow

def build(self):
"""
Begin building model.
Args:
None
Returns:
None
"""

# Call UnitModel.build to set up dynamics
super(DewateringData, self).build()

if "underflow" and "overflow" not in self.config.outlet_list:
raise ConfigurationError(
"{} encountered unrecognised "
"outlet_list. This should not "
"occur - please use overflow "
"and underflow as outlets.".format(self.name)
)
agarciadiego marked this conversation as resolved.
Show resolved Hide resolved

self.p_dewat = Param(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would shoot for more elaborate naming schemes. My guess without referring back to the literature is that you went with matching symbols in the literature. While that seems reasonable for the popular ASM and ADM models, I wonder if it might be more useful to offer up our own names in these unit cases for improved clarity. This is a broad comment regarding any parameter/variable names, when I compare those names with the associated doc string and question their alignment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we are following ASM and ADM convention for names of our property, reaction package, etc. following naming convention from BSM2 paper would avoid confusion for users specially if they are following the paper we added as references.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough - the documentation will probably help things.

initialize=0.28,
units=pyunits.dimensionless,
mutable=True,
doc="Percentage of suspended solids in the underflow",
)

self.TSS_rem = Param(
initialize=0.98,
units=pyunits.dimensionless,
mutable=True,
doc="Percentage of suspended solids removed",
)

@self.Expression(self.flowsheet().time, doc="Suspended solid concentration")
def TSS(blk, t):
return 0.75 * (
blk.inlet.conc_mass_comp[t, "X_I"]
+ blk.inlet.conc_mass_comp[t, "X_P"]
+ blk.inlet.conc_mass_comp[t, "X_BH"]
+ blk.inlet.conc_mass_comp[t, "X_BA"]
+ blk.inlet.conc_mass_comp[t, "X_S"]
)

@self.Expression(self.flowsheet().time, doc="Dewatering factor")
def f_dewat(blk, t):
return blk.p_dewat * (10 / (blk.TSS[t]))

@self.Expression(self.flowsheet().time, doc="Remove factor")
def f_q_du(blk, t):
return blk.TSS_rem / (pyunits.kg / pyunits.m**3) / 100 / blk.f_dewat[t]

self.non_particulate_components = Set(
initialize=[
"S_I",
"S_S",
"S_O",
"S_NO",
"S_NH",
"S_ND",
"H2O",
"S_ALK",
]
)

self.particulate_components = Set(
initialize=["X_I", "X_S", "X_P", "X_BH", "X_BA", "X_ND"]
)

@self.Constraint(
self.flowsheet().time,
self.particulate_components,
doc="particulate fraction",
)
def overflow_particulate_fraction(blk, t, i):
return blk.split_fraction[t, "overflow", i] == 1 - blk.TSS_rem

@self.Constraint(
self.flowsheet().time,
self.non_particulate_components,
doc="soluble fraction",
)
def non_particulate_components(blk, t, i):
return blk.split_fraction[t, "overflow", i] == 1 - blk.f_q_du[t]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, for both units, I'm aware that you didn't add initialize_build and calcualte_scaling_factors methods, likely because the separator handles those. I still wonder though if we should include them here anyway, even if, for example, we just have loggers setup for initialization and run super().calculate_scaling_factors(). @andrewlee94 curious to hear your thoughts.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you inherit from another model or base class, then any methods you do not overload will use that of the parent. For initialization this might be OK as long as the additions in the new model are relatively simple. However, for scaling, the new model is adding new variables and constraints (otherwise it wouldn't be a new model), and the parent's scaling methods won't touch these leaving part of the model unscaled.

On a related note, be aware that IDAES plans to rebuild both initialization and scaling this year. The new scaling API is already in place if you want to start looking at it: https://idaes-pse.readthedocs.io/en/latest/reference_guides/initialization/index.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I figured we could technically get away with it since the Separator's initialization and calculate_scaling_factors are essentially being used here, but that it might just be good practice and make for better readability/consistency between models for [new] developers. Furthermore, there are probably some unscaled vars that are added here but simple enough that additional scaling doesn't seem necessary at this point. I think we could put a pin in it for now and revisit if issues arise when analyzing the BSM2 flowsheet later.

And thanks for the note on the new scaling API.

def _get_performance_contents(self, time_point=0):
var_dict = {}
for k in self.split_fraction.keys():
if k[0] == time_point:
var_dict[f"Split Fraction [{str(k[1:])}]"] = self.split_fraction[k]
return {"vars": var_dict}

def _get_stream_table_contents(self, time_point=0):
outlet_list = self.create_outlet_list()

io_dict = {}
if self.config.mixed_state_block is None:
io_dict["Inlet"] = self.mixed_state
else:
io_dict["Inlet"] = self.config.mixed_state_block

for o in outlet_list:
io_dict[o] = getattr(self, o + "_state")

return create_stream_table_dataframe(io_dict, time_point=time_point)
Loading