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

Fix bubblepoint function and unit inconsistency in OLI API #1388

Merged
merged 20 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
21 changes: 18 additions & 3 deletions watertap/tools/oli_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ def generate_dbs_file(
dbs_file_inputs["modelName"] = "OLI_analysis"

# TODO: unknown bug where only "liquid1" phase is found in Flash analysis
valid_phases = ["liquid1", "vapor", "solid", "liquid2"]
self.valid_phases = ["liquid1", "vapor", "solid", "liquid2"]
if phases is not None:
invalid_phases = [p for p in phases if p not in valid_phases]
invalid_phases = [p for p in phases if p not in self.valid_phases]
if invalid_phases:
raise RuntimeError(
"Failed DBS file generation. "
Expand Down Expand Up @@ -333,7 +333,22 @@ def _get_flash_mode(self, dbs_file_id, flash_method, burst_job_tag=None):
headers = self.credential_manager.headers
base_url = self.credential_manager.engine_url
valid_get_flashes = ["corrosion-contact-surface", "chemistry-info"]
valid_post_flashes = ["isothermal", "corrosion-rates", "wateranalysis"]
valid_post_flashes = ["isothermal", "corrosion-rates", "wateranalysis", "bubblepoint"]

if flash_method in [
"bubblepoint",
#TODO: uncomment the methods below only after trying and testing
# "dewpoint",
# "vapor-amount",
# "vapor-fraction",
# "isochoric",
]:
dbs_summary = self.get_dbs_file_summary(dbs_file_id)
phase_list = dbs_summary['chemistry_info']['result']['phases']

if "vapor" not in phase_list:
raise RuntimeError("A vapor function ('{flash_method}') was called without included 'vapor' as a phase in the model")

if flash_method in valid_get_flashes:
mode = "GET"
url = f"{base_url}/file/{dbs_file_id}/{flash_method}"
Expand Down
27 changes: 24 additions & 3 deletions watertap/tools/oli_api/flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
# derivative works, incorporate into other computer software, distribute, and sublicense such enhancements
# or derivative works thereof, in binary and source code form.
###############################################################################
__author__ = "Oluwamayowa Amusat, Alexander Dudchenko, Paul Vecchiarelli"
__author__ = "Oluwamayowa Amusat, Alexander Dudchenko, Paul Vecchiarelli, Adam Atia"


import logging
Expand All @@ -66,6 +66,8 @@
output_unit_set,
)

from numpy import reshape, sqrt

_logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter(
Expand Down Expand Up @@ -469,6 +471,7 @@ def configure_flash_analysis(
"vapor-fraction",
"isochoric",
]:

if calculated_variable is not None:
if calculated_variable not in ["temperature", "pressure"]:
raise RuntimeError(
Expand Down Expand Up @@ -1062,6 +1065,7 @@ def _find_props(data, path=None):
raise RuntimeError(f"Unexpected type for data: {type(data)}")

def _get_nested_data(data, keys):

for key in keys:
data = data[key]
return data
Expand All @@ -1080,6 +1084,7 @@ def _extract_values(data, keys):
if "unit" in values:
unit = values["unit"] if values["unit"] else "dimensionless"
extracted_values.update({"units": unit})

elif all(k in values for k in ["found", "phase"]):
extracted_values = values
else:
Expand All @@ -1089,14 +1094,29 @@ def _extract_values(data, keys):
"units": unit,
"values": values["value"],
}
else:
# elif "values" not in values.keys()
# extracted_values =
adam-a-a marked this conversation as resolved.
Show resolved Hide resolved
elif "values" in values:
extracted_values = {
k: {
"units": unit,
"values": values["values"][k],
}
for k, v in values["values"].items()
}
elif "data" in values:
#intended for vaporDiffusivityMatrix
mat_dim = int(sqrt(len(values["data"])))
diffmat = reshape(values["data"], newshape=(mat_dim, mat_dim))

extracted_values = {f'({values["speciesNames"][i]},{values["speciesNames"][j]})': {
"units": values['unit'],
"values": diffmat[i][j]
}
for i in range(len(diffmat)) for j in range(i,len(diffmat))
}
else:
raise NotImplementedError(f"results structure not accounted for. results:\n{values}")
else:
raise RuntimeError(f"Unexpected type for data: {type(values)}")
return extracted_values
Expand Down Expand Up @@ -1126,7 +1146,8 @@ def _create_input_dict(props, result):
if isinstance(prop[-1], int):
prop_tag = _get_nested_data(result, prop)["name"]
else:
_logger.warning(f"Unexpected result in result")
_logger.warning(f"Unexpected result:\n{result}\n\ninput_dict:\n{input_dict}")

label = f"{prop_tag}_{phase_tag}" if phase_tag else prop_tag
input_dict[k][label] = _extract_values(result, prop)
return input_dict
Expand Down
6 changes: 6 additions & 0 deletions watertap/tools/oli_api/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ def test_get_dbs_file_summary(oliapi_instance: OLIApi, local_dbs_file: Path):
oliapi_instance.get_user_dbs_file_ids()
dbs_file_id = oliapi_instance.upload_dbs_file(local_dbs_file)
oliapi_instance.get_dbs_file_summary(dbs_file_id)

@pytest.mark.unit
def test_valid_phases(oliapi_instance: OLIApi):
valid_phases=["liquid1", "vapor", "solid", "liquid2"]
for v in oliapi_instance.valid_phases:
assert v in valid_phases
30 changes: 30 additions & 0 deletions watertap/tools/oli_api/tests/test_flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,33 @@ def test_isothermal_flash_survey(
dbs_file_id,
isothermal_input,
)

@pytest.mark.unit
def test_bubble_point(
flash_instance: Flash, source_water: dict, oliapi_instance: OLIApi, tmp_path: Path
):
dbs_file_id = oliapi_instance.session_dbs_files[-1]
inflows = flash_instance.get_apparent_species_from_true(
stream_input,
oliapi_instance,
dbs_file_id,
)
stream_input = flash_instance.configure_water_analysis(source_water)
inflows = flash_instance.get_apparent_species_from_true(
stream_input,
oliapi_instance,
dbs_file_id,
)
bubblepoint_input = flash_instance.configure_flash_analysis(
inflows=inflows, flash_method="bubblepoint",
calculated_variable="pressure",
)

saturation_pressure = flash_instance.run_flash(
"bubblepoint",
oliapi,
dbs_file_id,
bubblepoint_input,
)

pytest.approx(saturation_pressure['result']['calculatedVariables']['values'][0], rel=1e-3) == 32.04094
75 changes: 59 additions & 16 deletions watertap/tools/oli_api/util/fixed_keys_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
# "https://github.com/watertap-org/watertap/"
#################################################################################

__author__ = "Paul Vecchiarelli, Ben Knueven"
__author__ = "Paul Vecchiarelli, Ben Knueven, Adam Atia"

from collections import UserDict
from pyomo.environ import units as pyunits
from pyomo.core.base.units_container import _PyomoUnit
from collections.abc import Iterable


class FixedKeysDict(UserDict):
Expand All @@ -25,8 +27,45 @@ def __setitem__(self, k, v):
raise RuntimeError(f" Key {k} not in dictionary.")
# also check for valid value if a list of values is given in the default dictionary
else:
self.data[k] = v

# if user setting value in pyomo units
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should have a sub-class of FixedKeysDict just for the units?

if hasattr(v, "is_expression_type"):
if isinstance(v, _PyomoUnit) or v.is_expression_type():
# if user assigns pyomo units as value to oli_unit, save the str to oli_unit and update pyomo_unit with pyomo units
if "oli_unit" in k:
self.data[k] = str(v)
self.data["pyomo_unit"] = v
# if user assigns pyomo units to pyomo_unit, update oli_unit with str representation of units
if "pyomo_unit" in k:
self.data[k] = v
self.data["oli_unit"] = str(v)
# check if data[k] is iterable first, otherwise checking if oli_unit in data[k] throws exception
elif isinstance(self.data[k], Iterable):
# check if user provides str and that assignment wouldn't overwrite the oli_unit key:value pair
if isinstance(v, str) and ("oli_unit" not in self.data[k]):
# if user assigns str to oli_unit, update pyomo_units with PyomoUnits representation of str
if "oli_unit" in k:
self.data[k] = v
self.data["pyomo_unit"] = getattr(pyunits, v)
else:
pass
elif isinstance(v, str) and ("oli_unit" in self.data[k]):
self.data["oli_unit"] = v
self.data["pyomo_unit"] = getattr(pyunits, v)
elif not isinstance(v, str):
raise RuntimeError(f"Setting {v} as the value for {k} is not permitted as a value for oli and pyomo units. Please enter units as a string type or pint units.")
else:
pass
elif not isinstance(self.data[k], Iterable):
if isinstance(v, str) and "pyomo_unit" in k:
self.data[k] = getattr(pyunits, v)
self.data["oli_unit"] = v
elif not isinstance(v, str):
raise RuntimeError(f"Setting {v} as the value for {k} is not permitted as a value for oli and pyomo units. Please enter units as a string type or pint units.")
else:
pass
else:
self.data[k] = v

def __delitem__(self, k):
raise Exception(" Deleting keys not supported for this object.")

Expand All @@ -45,8 +84,7 @@ def pprint(self):
print(f" {key}\n - {value}\n")


input_unit_set = FixedKeysDict(
{
input_unit_set_temp = {
"molecularConcentration": {
"oli_unit": "mg/L",
"pyomo_unit": pyunits.mg / pyunits.L,
Expand Down Expand Up @@ -89,7 +127,9 @@ def pprint(self):
"pyomo_unit": pyunits.mol / pyunits.mol,
},
}
)

input_unit_set = FixedKeysDict({k:FixedKeysDict(v) for k,v in input_unit_set_temp.items()})
default_unit_set = FixedKeysDict({k:FixedKeysDict(v) for k,v in input_unit_set_temp.items()})

optional_properties = FixedKeysDict(
{
Expand Down Expand Up @@ -137,15 +177,18 @@ def pprint(self):
# and reducing hard-coding by using default_input_unit_set references
output_unit_set = FixedKeysDict(
{
"enthalpy": input_unit_set["enthalpy"]["oli_unit"],
"mass": input_unit_set["mass"]["oli_unit"],
"pt": input_unit_set["pressure"]["oli_unit"],
"total": input_unit_set["mass"]["oli_unit"],
"liq1_phs_comp": input_unit_set["mass"]["oli_unit"],
"solid_phs_comp": input_unit_set["mass"]["oli_unit"],
"vapor_phs_comp": input_unit_set["mass"]["oli_unit"],
"liq2_phs_comp": input_unit_set["mass"]["oli_unit"],
"combined_phs_comp": input_unit_set["mass"]["oli_unit"],
"molecularConcentration": input_unit_set["molecularConcentration"]["oli_unit"],
"enthalpy": default_unit_set["enthalpy"]["oli_unit"],
"mass": default_unit_set["mass"]["oli_unit"],
"pt": default_unit_set["pressure"]["oli_unit"],
"total": default_unit_set["mass"]["oli_unit"],
"liq1_phs_comp": default_unit_set["mass"]["oli_unit"],
"solid_phs_comp": default_unit_set["mass"]["oli_unit"],
"vapor_phs_comp": default_unit_set["mass"]["oli_unit"],
"liq2_phs_comp": default_unit_set["mass"]["oli_unit"],
"combined_phs_comp": default_unit_set["mass"]["oli_unit"],
"molecularConcentration": default_unit_set["molecularConcentration"]["oli_unit"],
}
)

if __name__ == "__main__":
unit_set=input_unit_set_temp
49 changes: 46 additions & 3 deletions watertap/tools/oli_api/util/tests/test_fixed_keys_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
#################################################################################

import pytest

from watertap.tools.oli_api.util.fixed_keys_dict import output_unit_set

from pyomo.environ import units as pyunits
from pyomo.util.check_units import assert_units_equivalent
from watertap.tools.oli_api.util.fixed_keys_dict import output_unit_set, input_unit_set
from watertap.tools.oli_api.flash import Flash

@pytest.mark.unit
def test_fixed_keys_dict():
Expand All @@ -27,3 +28,45 @@ def test_fixed_keys_dict():
output_unit_set._check_value("mass", ["not_kg"])

output_unit_set.pprint()


@pytest.mark.unit
def test_input_unit_set():
unit_set = input_unit_set
# check defaults for one of the properties
assert unit_set["molecularConcentration"]["oli_unit"] == "mg/L"
assert str(unit_set["molecularConcentration"]["pyomo_unit"]) == "mg/L".lower()
assert_units_equivalent((unit_set["molecularConcentration"]["pyomo_unit"]), pyunits.mg/pyunits.L)
assert hasattr((unit_set["molecularConcentration"]["pyomo_unit"]), "is_expression_type")
assert str(unit_set["molecularConcentration"]["pyomo_unit"]) == "mg/L".lower()

# reset oli_unit with a different unit, provided as string, and check that pyomo_unit follows along
unit_set["molecularConcentration"]["oli_unit"] = "mol/L"
assert unit_set["molecularConcentration"]["oli_unit"] == "mol/L"
assert str(unit_set["molecularConcentration"]["pyomo_unit"]) == "mol/L".lower()
assert_units_equivalent((unit_set["molecularConcentration"]["pyomo_unit"]), pyunits.mol/pyunits.L)

# reset pyomo_unit with a different unit, provided as pint units, and check that oli_unit follows along
unit_set["molecularConcentration"]["pyomo_unit"] = pyunits.mg
assert_units_equivalent((unit_set["molecularConcentration"]["pyomo_unit"]), pyunits.mg)
assert unit_set["molecularConcentration"]["oli_unit"] == "mg"

# reset pyomo_unit with a different unit, provided as string units, and check that oli_unit follows along
unit_set["molecularConcentration"]["pyomo_unit"] = "mg/L"
assert_units_equivalent((unit_set["molecularConcentration"]["pyomo_unit"]), pyunits.mg/pyunits.L)
assert unit_set["molecularConcentration"]["oli_unit"] == "mg/L"

# reset oli_unit with a different unit, provided as pint units, and check that pyomo_unit follows along
unit_set["molecularConcentration"]["oli_unit"] = pyunits.mol/pyunits.L
assert unit_set["molecularConcentration"]["oli_unit"] == "mol/L".lower()
assert str(unit_set["molecularConcentration"]["pyomo_unit"]) == "mol/L".lower()
assert_units_equivalent((unit_set["molecularConcentration"]["pyomo_unit"]), pyunits.mol/pyunits.L)

with pytest.raises(RuntimeError, match="Setting 1 as the value for oli_unit is not permitted as a value for oli and pyomo units. Please enter units as a string type or pint units."):
unit_set["molecularConcentration"]["oli_unit"] = 1

with pytest.raises(RuntimeError, match="Setting 1 as the value for pyomo_unit is not permitted as a value for oli and pyomo units. Please enter units as a string type or pint units."):
unit_set["molecularConcentration"]["pyomo_unit"] = 1

with pytest.raises(RuntimeError, match="Setting 1 as the value for molecularConcentration is not permitted as a value for oli and pyomo units. Please enter units as a string type or pint units."):
unit_set["molecularConcentration"] = 1
Loading