Skip to content
Open
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
1 change: 1 addition & 0 deletions watertap/core/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
assert_no_degrees_of_freedom,
assert_degrees_of_freedom,
)
from .property_helpers import get_property_metadata
32 changes: 32 additions & 0 deletions watertap/core/util/property_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#################################################################################
# WaterTAP Copyright (c) 2020-2024, 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/"
#################################################################################

import pandas as pd
from idaes.core.util.config import is_physical_parameter_block


def get_property_metadata(prop_pkg):
"""Get all supported properties from a WaterTAP/IDAES property package as a Pandas DataFrame."""
try:
assert is_physical_parameter_block(prop_pkg)
except:
raise TypeError("get_property_metadata expected a PhysicalParameterBlock.")
metadata = prop_pkg.get_metadata()
Copy link
Collaborator

Choose a reason for hiding this comment

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

can this fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's the method (below). I suppose we can add a check for the _metadata attribute and raise an exception if it doesn't exist. Do you know of a better check to ensure the property model is a property model? Another idea is to check whether the prop_pkg input is a ParameterBlock.

    @classmethod
    def get_metadata(cls):
        """Get property parameter metadata.

        If the metadata is not defined, this will instantiate a new
        metadata object and call `define_metadata()` to set it up.

        If the metadata is already defined, it will be simply returned.

        Returns:
            PropertyClassMetadata: The metadata
        """
        if cls._metadata is None:
            pcm = PropertyClassMetadata()
            cls.define_metadata(pcm)
            cls._metadata = pcm

            # Check that the metadata was actually populated
            # Check requires looking at private attributes
            # pylint: disable-next=protected-access
            if pcm._properties is None or pcm._default_units is None:
                raise PropertyPackageError(
                    "Property package did not populate all expected metadata."
                )
        return cls._metadata

pd.set_option("display.max_rows", None)
df = pd.DataFrame(
{
"Description": [v._doc for v in metadata.properties],
"Name": [v._name for v in metadata.properties],
"Units": [str(v._units) for v in metadata.properties],
}
)
return df
60 changes: 60 additions & 0 deletions watertap/core/util/tests/test_property_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#################################################################################
# WaterTAP Copyright (c) 2020-2024, 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/"
#################################################################################

import pytest
import pandas as pd
from idaes.core.base.property_base import (
PhysicalParameterBlock as DummyPhysicalParameterBlock,
)
from watertap.core.util.property_helpers import get_property_metadata


# Dummy classes to mimic WaterTAP metadata structure
class DummyProp:
def __init__(self, name, units, doc):
self._name = name
self._units = units
self._doc = doc


class DummyMetadata:
def __init__(self):
self.properties = [
DummyProp("flow_mass", "kg/s", "Mass flow rate"),
DummyProp("temperature", "K", "Stream temperature"),
]


class DummyPropPkg(DummyPhysicalParameterBlock):
def __init__(self):
self.component = ["dummy_component"]

def get_metadata(self):
return DummyMetadata()


@pytest.mark.unit
def test_get_property_metadata():
pkg = DummyPropPkg()
df = get_property_metadata(pkg)

# Check type
assert isinstance(df, pd.DataFrame)

# Check columns
expected_cols = ["Description", "Name", "Units"]
assert list(df.columns) == expected_cols

# Check content
assert "flow_mass" in df["Name"].values
assert "temperature" in df["Name"].values
assert "kg/s" in df["Units"].values
157 changes: 146 additions & 11 deletions watertap/property_models/seawater_prop_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
"""
Initial property package for seawater system
"""

# Import Python libraries
import idaes.logger as idaeslog

# Import Pyomo libraries
from pyomo.environ import (
Constraint,
Expand Down Expand Up @@ -43,6 +39,7 @@
MaterialBalanceType,
EnergyBalanceType,
)
from idaes.core.base.property_set import PropertyMetadata, PropertySetBase
from idaes.core.base.components import Solute, Solvent
from idaes.core.base.phases import LiquidPhase
from idaes.core.util.constants import Constants
Expand All @@ -51,7 +48,7 @@
revert_state_vars,
solve_indexed_blocks,
)
from watertap.core.solvers import get_solver
import idaes.logger as idaeslog
from idaes.core.util.model_statistics import (
degrees_of_freedom,
number_unfixed_variables,
Expand All @@ -62,7 +59,11 @@
PropertyPackageError,
)
import idaes.core.util.scaling as iscale

# Import WaterTAP libraries
from watertap.core.solvers import get_solver
from watertap.core.util.scaling import transform_property_constraints
from watertap.core.util.property_helpers import get_property_metadata

# Set up logger
_log = idaeslog.getLogger(__name__)
Expand Down Expand Up @@ -736,9 +737,24 @@ def build(self):
self.set_default_scaling("diffus_phase_comp", 1e9)
self.set_default_scaling("boiling_point_elevation_phase", 1e0, index="Liq")

def list_properties(self):
"""
Return seawater property package metadata as a pandas DataFrame.
"""
df = get_property_metadata(self).reset_index(drop=True)
return df

def print_properties(self):
"""
Print seawater property package metadata to the console.
"""
df = get_property_metadata(self).reset_index(drop=True)
print(df.to_string(index=False)) # Pretty print without index

@classmethod
def define_metadata(cls, obj):
"""Define properties supported and units."""
obj.define_property_set(SeawaterPropertySet)
obj.add_properties(
{
"flow_mass_phase_comp": {"method": None},
Expand All @@ -759,11 +775,6 @@ def define_metadata(cls, obj):
"cp_mass_phase": {"method": "_cp_mass_phase"},
"therm_cond_phase": {"method": "_therm_cond_phase"},
"diffus_phase_comp": {"method": "_diffus_phase_comp"},
}
)

obj.define_custom_properties(
{
"dens_mass_solvent": {"method": "_dens_mass_solvent"},
"osm_coeff": {"method": "_osm_coeff"},
"enth_flow": {"method": "_enth_flow"},
Expand Down Expand Up @@ -804,7 +815,7 @@ def fix_initialization_states(self):
# Constraint on water concentration at outlet - unfix in these cases
for b in self.values():
if b.config.defined_state is False:
b.conc_mol_comp["H2O"].unfix()
b.flow_mass_phase_comp["Liq", "H2O"].unfix()

def initialize(
self,
Expand Down Expand Up @@ -1789,3 +1800,127 @@ def calculate_scaling_factors(self):

# transforming constraints
transform_property_constraints(self)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I realize this is normal in IDAES but I find this way of creating data very verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Me too, but given the current IDAES functionality and the way metadata is handled and reported back, this was the cleaner way of producing the list of property names actually supported on this property model, without the noise. Open to suggestions if there's a slicker approach, but this is meant a quick fix, with planned refinement in subsequent PRs (this quarter).

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this is redundant, don't the pyomo vars in the property block contain all of the same information? Would it not be easier to make a list of the "vars" you want to display and simply grab it?


class SeawaterPropertySet(PropertySetBase):
"""
This object defines properties within the seawater property model.
"""

flow_mass_phase_comp = PropertyMetadata(
name="flow_mass_phase_comp",
doc="Mass flow rate",
units=pyunits.kg / pyunits.s,
)
temperature = PropertyMetadata(
name="temperature",
doc="Temperature",
units=pyunits.K,
)
pressure = PropertyMetadata(
name="pressure",
doc="Pressure",
units=pyunits.Pa,
)
mass_frac_phase_comp = PropertyMetadata(
name="mass_frac",
doc="Mass fraction",
units=pyunits.dimensionless,
)
dens_mass_phase = PropertyMetadata(
name="dens_mass_phase",
doc="Mass density of solution",
units=pyunits.kg * pyunits.m**-3,
)

flow_vol = PropertyMetadata(
name="flow_vol",
doc="Total volumetric flow rate",
units=pyunits.m**3 / pyunits.s,
)
flow_vol_phase = PropertyMetadata(
name="flow_vol_phase",
doc="Volumetric flow rate of phase",
units=pyunits.m**3 / pyunits.s,
)
conc_mass_phase_comp = PropertyMetadata(
name="conc_mass_phase_como",
doc="Mass concentration",
units=pyunits.kg * pyunits.m**-3,
)
flow_mol_phase_comp = PropertyMetadata(
name="flow_mol",
doc="Molar flowrate",
units=pyunits.mol / pyunits.s,
)
mole_frac_phase_comp = PropertyMetadata(
name="mole_frac_phase_comp",
doc="Mole fraction",
units=pyunits.dimensionless,
)
molality_phase_comp = PropertyMetadata(
name="molality_phase_comp",
doc="Molality",
units=pyunits.mole / pyunits.kg,
)
visc_d_phase = PropertyMetadata(
name="visc_d_phase",
doc="Dynamic viscosity",
units=pyunits.Pa * pyunits.s,
)
pressure_osm_phase = PropertyMetadata(
name="pressure_osm_phase",
doc="Osmotic pressure",
units=pyunits.Pa,
)
enth_mass_phase = PropertyMetadata(
name="enth_mass_phase",
doc="Specific enthalpy",
units=pyunits.J * pyunits.kg**-1,
)
pressure_sat = PropertyMetadata(
name="pressure_sat",
doc="Vapor pressure",
units=pyunits.Pa,
)
cp_mass_phase = PropertyMetadata(
name="cp_mass",
doc="Specific heat capacity",
units=pyunits.J / (pyunits.kg * pyunits.K),
)
therm_cond_phase = PropertyMetadata(
name="therm_cond_phase",
doc="Thermal conductivity",
units=pyunits.W / (pyunits.m * pyunits.K),
)
diffus_phase_comp = PropertyMetadata(
name="diffus_phase_comp",
doc="Diffusivity",
units=pyunits.m**2 / pyunits.s,
)

dens_mass_solvent = PropertyMetadata(
name="dens_mass_solvent",
doc="Mass density of pure water",
units=pyunits.kg * pyunits.m**-3,
)
osm_coeff = PropertyMetadata(
name="osm_coeff",
doc="Osmotic coefficient",
units=pyunits.dimensionless,
)
enth_flow = PropertyMetadata(
name="enth_flow",
doc="Enthalpy flow",
units=pyunits.J / pyunits.s,
)
dh_vap_mass = PropertyMetadata(
name="dh_vap_mass",
doc="Latent heat of vaporization",
units=pyunits.J * pyunits.kg**-1,
)
boiling_point_elevation_phase = PropertyMetadata(
name="boiling_point_elevation_phase",
doc="Boiling point elevation temperature",
units=pyunits.K,
)
60 changes: 60 additions & 0 deletions watertap/property_models/tests/test_seawater_prop_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
# "https://github.com/watertap-org/watertap/"
#################################################################################
import pytest
from pyomo.environ import ConcreteModel
from idaes.core import FlowsheetBlock
import re
import pandas as pd
from pandas.testing import assert_frame_equal
import watertap.property_models.seawater_prop_pack as props
from idaes.models.properties.tests.test_harness import (
PropertyTestHarness as PropertyTestHarness_idaes,
Expand Down Expand Up @@ -332,3 +337,58 @@ def configure(self):
("flow_mass_phase_comp", ("Liq", "TDS")): 1239.69,
("enth_mass_phase", "Liq"): 3.8562e5,
}


@pytest.mark.unit
def test_list_properties(capsys):
m = ConcreteModel()
m.fs = FlowsheetBlock(dynamic=False)
m.fs.props = props.SeawaterParameterBlock()

# clear any existing captured output, call list_properties, then capture its output
# capsys.readouterr()
df = m.fs.props.list_properties()
# captured = capsys.readouterr().out

# # Build DataFrame from captured table by splitting on two or more spaces
# lines = [ln for ln in captured.splitlines() if ln.strip() != ""]
# print(lines)
# # find header line
# header_idx = 0
# header_cols = re.split(r"\s{2,}", lines[header_idx+1].strip())
# data_lines = lines[header_idx + 2 :] # skip header and separator line
# rows = [re.split(r"\s{2,}", ln.strip()) for ln in data_lines]

# df = pd.DataFrame(rows, columns=header_cols)

# Expected dataframe rows (as parsed into columns)
expected_cols = ["Description", "Name", "Units"]
expected_rows = [
["Boiling point elevation temperature", "boiling_point_elevation_phase", "K"],
["Mass concentration", "conc_mass_phase_comp", "kg*m**(-3)"],
["Specific heat capacity", "cp_mass_phase", "J/(kg*K)"],
["Mass density of solution", "dens_mass_phase", "kg*m**(-3)"],
["Mass density of pure water", "dens_mass_solvent", "kg*m**(-3)"],
["Latent heat of vaporization", "dh_vap_mass", "J*kg**(-1)"],
["Diffusivity", "diffus_phase_comp", "m**2/s"],
["Enthalpy flow", "enth_flow", "J/s"],
["Specific enthalpy", "enth_mass_phase", "J*kg**(-1)"],
["Mass flow rate", "flow_mass_phase_comp", "kg/s"],
["Molar flowrate", "flow_mol_phase_comp", "mol/s"],
["Total volumetric flow rate", "flow_vol", "m**3/s"],
["Volumetric flow rate of phase", "flow_vol_phase", "m**3/s"],
["Mass fraction", "mass_frac_phase_comp", "dimensionless"],
["Molality", "molality_phase_comp", "mol/kg"],
["Mole fraction", "mole_frac_phase_comp", "dimensionless"],
["Osmotic coefficient", "osm_coeff", "dimensionless"],
["Pressure", "pressure", "Pa"],
["Osmotic pressure", "pressure_osm_phase", "Pa"],
["Vapor pressure", "pressure_sat", "Pa"],
["Temperature", "temperature", "K"],
["Thermal conductivity", "therm_cond_phase", "W/(m*K)"],
["Dynamic viscosity", "visc_d_phase", "Pa*s"],
]
expected_df = pd.DataFrame(expected_rows, columns=expected_cols)

# Compare dataframes
assert_frame_equal(df.reset_index(drop=True), expected_df.reset_index(drop=True))
Loading