Skip to content

Commit

Permalink
Make sure top-level model is not garbage collected in UnitTestHarness (
Browse files Browse the repository at this point in the history
…#1317)

* testing if model is garbage collected

* update UnitTestHarness API

* use initialization_tester; better comments

* Carry top-level model through frame

* update documentation

* remove manual garbage collection

* make  a proper abstract base class for pylint
  • Loading branch information
bknueven authored Feb 29, 2024
1 parent d035b7e commit 1ee5494
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 36 deletions.
8 changes: 3 additions & 5 deletions docs/how_to_guides/how_to_use_unit_test_harness.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ assumes a test file is being created for an anaerobic digester.
solver = get_solver()

Next, setup the configure function which will create the flowsheet, specify the property and reaction packages,
specify the unit model configuration, set the operating conditions, add the unit model costing, and
set the scaling factors for any variables that are badly scaled. Then, iterate through any variables on the unit model that you'd like to confirm the value of.
specify the unit model configuration (named `fs.unit`), set the operating conditions, add the unit model costing, and
set the scaling factors for any variables that are badly scaled. Then, iterate through any variables on the unit model that you'd like to have the value of tested. Finally, return the top-level Pyomo model.
Failures may arise at this stage, at which point an error message will be displayed that prompts you
to adjust something in the configure function and/or address the discrepancy between the
expected value for a variable (user-input) and its actual value.
Expand Down Expand Up @@ -125,9 +125,6 @@ expected value for a variable (user-input) and its actual value.
)
iscale.set_scaling_factor(m.fs.unit.costing.capital_cost, 1e-6)

# Specify the unit model being tested
self.unit_model_block = m.fs.unit

# Check the expected unit model outputs
self.unit_solutions[m.fs.unit.liquid_outlet.pressure[0]] = 101325
self.unit_solutions[m.fs.unit.liquid_outlet.temperature[0]] = 308.15
Expand Down Expand Up @@ -217,3 +214,4 @@ expected value for a variable (user-input) and its actual value.
self.unit_solutions[m.fs.unit.hydraulic_retention_time[0]] = 1880470.588
self.unit_solutions[m.fs.unit.costing.capital_cost] = 2166581.415

return m
4 changes: 2 additions & 2 deletions watertap/unit_models/tests/test_anaerobic_digestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ def configure(self):
)
iscale.set_scaling_factor(m.fs.unit.costing.capital_cost, 1e-6)

self.unit_model_block = m.fs.unit

self.unit_solutions[m.fs.unit.liquid_outlet.pressure[0]] = 101325
self.unit_solutions[m.fs.unit.liquid_outlet.temperature[0]] = 308.15
self.unit_solutions[
Expand Down Expand Up @@ -209,3 +207,5 @@ def configure(self):
self.unit_solutions[m.fs.unit.electricity_consumption[0]] = 23.7291667
self.unit_solutions[m.fs.unit.hydraulic_retention_time[0]] = 1880470.588
self.unit_solutions[m.fs.unit.costing.capital_cost] = 2166581.415

return m
74 changes: 45 additions & 29 deletions watertap/unit_models/tests/unit_test_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
#################################################################################

import pytest
import abc

from pyomo.environ import Block, assert_optimal_termination, ComponentMap, value
from pyomo.util.check_units import assert_units_consistent
from idaes.core.util.model_statistics import (
degrees_of_freedom,
)
from idaes.core.solvers import get_solver
from idaes.core.util.exceptions import InitializationError
from idaes.core.util.testing import initialization_tester
import idaes.core.util.scaling as iscale


Expand All @@ -41,12 +42,14 @@ class UnitRuntimeError(RuntimeError):
"""


class UnitTestHarness:
class UnitTestHarness(abc.ABC):
def configure_class(self):
self.solver = None # string for solver, if None use WaterTAP default
self.optarg = (
None # dictionary for solver options, if None use WaterTAP default
)
# string for solver, if None use WaterTAP default
self.solver = None
# dictionary for solver options, if None use WaterTAP default
self.optarg = None

# solution map from var to value
self.unit_solutions = ComponentMap()

# arguments for badly scaled variables
Expand All @@ -58,7 +61,16 @@ def configure_class(self):
self.default_absolute_tolerance = 1e-12
self.default_relative_tolerance = 1e-6

self.configure()
model = self.configure()
if not hasattr(self, "unit_model_block"):
self.unit_model_block = model.find_component("fs.unit")
if self.unit_model_block is None:
raise RuntimeError(
f"The {self.__class__.__name__}.configure method should either "
"set the attribute `unit_model_block` or name it `fs.unit`."
)
# keep the model so it does not get garbage collected
self._model = model
blk = self.unit_model_block

# attaching objects to model to carry through in pytest frame
Expand All @@ -69,50 +81,54 @@ def configure_class(self):
blk._test_objs.optarg = self.optarg
blk._test_objs.unit_solutions = self.unit_solutions

@abc.abstractmethod
def configure(self):
"""
Placeholder method to allow user to setup test harness.
The configure function must set the attributes:
The configure method must set the attributes:
unit_solutions: ComponentMap of values for the specified variables
unit_model: pyomo unit model block (e.g. m.fs.unit), the block should
have zero degrees of freedom, i.e. fully specified
The unit model tested should be named `fs.unit`, or this method
should set the attribute `unit_model_block`.
unit_solutions: dictionary of property values for the specified state variables
Returns:
model: the top-level Pyomo model
"""

@pytest.fixture(scope="class")
def frame_unit(self):
def frame(self):
self.configure_class()
return self.unit_model_block
return self._model, self.unit_model_block

@pytest.mark.unit
def test_units_consistent(self, frame_unit):
assert_units_consistent(frame_unit)
def test_units_consistent(self, frame):
m, unit_model = frame
assert_units_consistent(unit_model)

@pytest.mark.unit
def test_dof(self, frame_unit):
if degrees_of_freedom(frame_unit) != 0:
def test_dof(self, frame):
m, unit_model = frame
if degrees_of_freedom(unit_model) != 0:
raise UnitAttributeError(
"The unit has {dof} degrees of freedom when 0 is required."
"".format(dof=degrees_of_freedom(frame_unit))
"".format(dof=degrees_of_freedom(unit_model))
)

@pytest.mark.component
def test_initialization(self, frame_unit):
blk = frame_unit

try:
blk.initialize(solver=blk._test_objs.solver, optarg=blk._test_objs.optarg)
except InitializationError:
raise InitializationError(
"The unit has failed to initialize successfully. Please check the output logs for more information."
)
def test_initialization(self, frame):
m, blk = frame
initialization_tester(
m,
unit=blk,
solver=blk._test_objs.solver,
optarg=blk._test_objs.optarg,
)

@pytest.mark.component
def test_unit_solutions(self, frame_unit):
def test_unit_solutions(self, frame):
self.configure_class()
blk = frame_unit
m, blk = frame
solutions = blk._test_objs.unit_solutions

# solve unit
Expand Down

0 comments on commit 1ee5494

Please sign in to comment.