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

Add support for Greybox model DOF counitng in degrees_of_freedom function #1512

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
43 changes: 41 additions & 2 deletions idaes/core/util/model_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pyomo.core.expr import identify_variables
from pyomo.common.collections import ComponentSet
from pyomo.common.deprecation import deprecation_warning
from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock

import idaes.logger as idaeslog

Expand Down Expand Up @@ -377,7 +378,32 @@ def number_activated_equalities(block):
Returns:
Number of activated equality Constraint components in block
"""
return sum(1 for _ in activated_equalities_generator(block))
return sum(
Copy link
Contributor

Choose a reason for hiding this comment

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

The docstring of this function needs to be updated to explain what's going on with greybox models.

1 for _ in activated_equalities_generator(block)
) + number_grey_box_equalities(block)


def number_grey_box_equalities(block) -> int:
"""
Function to compute total number of equality constraints for all GreyBox objects in this block.
Copy link
Contributor

Choose a reason for hiding this comment

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

"total number of equality constraints" -> "total number of implied equality constraints"

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, it might be fine leaving as is. I didn't realize GreyBox models have an n_equality_constraints() attribute. The distinction here is probably that they don't exist as Constraint objects.


A GreyBox model is always assumed to be 0DOFs where each output[i]==f(inputs)
where f is GreyBox model, this should be true regardless if
GreyBox model is doing internal optimization or not, as every output
is calculated through a the GreyBox internal model using provided inputs.
Copy link
Contributor

Choose a reason for hiding this comment

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

"through a the GreyBox" -> "through the GreyBox"

Also, is this 0DOFs property of GreyBox models always true, or is it true only if the user has correctly configured their GreyBox model?


Args:
block : pyomo concrete model or pyomo block

Returns:
Number of equality constraints in all GreyBox objects on the provided block
"""
equalities = 0
for grey_box in _iter_indexed_block_data_objects(
block, ctype=ExternalGreyBoxBlock, active=True, descend_into=True
):
equalities += len(grey_box.outputs)
return equalities
Copy link
Member

Choose a reason for hiding this comment

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

As written, this will be incorrect for grey box models that have a nonzero number of "explicitly defined equality constraints" (as opposed to the equality constraints that are created to define the outputs). I think all that's needed to correct this is:

equalities += grey_box.get_external_model().n_equality_constraints()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, did not think about this scenario! Updated to test graybox with suggestion, this now includes an example constraint that binds 2 inputs together.

        def input_names(self):
            return ["a1", "a2", "a3"]

        def output_names(self):
            return ["o1", "o2"]

        def equality_constraint_names(self):
            return ["a12"]

        def evaluate_equality_constraints(self):
            a1 = self._input_values[0]
            a2 = self._input_values[1]
            return [a1 * 0.5 - a2]



def deactivated_equalities_generator(block):
Expand Down Expand Up @@ -529,7 +555,7 @@ def deactivated_inequalities_generator(block):
block : model to be studied

Returns:
A generator which returns all indeactivated equality Constraint
A generator which returns all in deactivated equality Constraint
Copy link
Member

Choose a reason for hiding this comment

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

The "in" should be removed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

components block
"""
for c in total_inequalities_generator(block):
Expand Down Expand Up @@ -1034,6 +1060,19 @@ def unfixed_variables_in_activated_equalities_set(block):
for v in variables_in_activated_equalities_set(block):
if not v.fixed:
var_set.add(v)

# Checks for greyboxes, and if they exist will add
# input and output vars to var_set if they are free
# inputs and outputs are defined names for greybox class and should always exist
for grey_box in _iter_indexed_block_data_objects(
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be good to have a number_of_greybox_variables function to do this so that users can check that separately and so that we can test the code in isolation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added one for variables, and unfixed variables.

block, ctype=ExternalGreyBoxBlock, active=True, descend_into=True
):
for in_var in grey_box.inputs:
if not grey_box.inputs[in_var].fixed:
var_set.add(grey_box.inputs[in_var])
for out_var in grey_box.outputs:
if not grey_box.outputs[out_var].fixed:
var_set.add(grey_box.outputs[out_var])
avdudchenko marked this conversation as resolved.
Show resolved Hide resolved
return var_set


Expand Down
40 changes: 40 additions & 0 deletions idaes/core/util/tests/test_model_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@

from idaes.core.util.model_statistics import *
from idaes.core.util.model_statistics import _iter_indexed_block_data_objects
from pyomo.contrib.pynumero.interfaces.external_grey_box import (
ExternalGreyBoxBlock,
ExternalGreyBoxModel,
)


@pytest.mark.unit
Expand Down Expand Up @@ -685,6 +689,42 @@ def test_degrees_of_freedom(m):
assert degrees_of_freedom(m.b2) == -1


@pytest.mark.unit
Copy link
Member

Choose a reason for hiding this comment

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

We should also have test for the functions to collect the number of greybox variables and constraints.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added tests

def test_degrees_of_freedom_with_graybox():
"""non functional graybox model added to m fixture, to test DOFs

GreyBoxModel has 3 inputs and 2 outputs calculated an unknown function"""

class BasicGrayBox(ExternalGreyBoxModel):
def input_names(self):
return ["a1", "a2", "a3"]

def output_names(self):
return ["o1", "o2"]

m = ConcreteModel()

m.gb = ExternalGreyBoxBlock(external_model=BasicGrayBox())
# verify DOFS works on stand alone greybox
assert degrees_of_freedom(m) == 3
m.gb.inputs.fix()
assert degrees_of_freedom(m) == 0
m.gb.outputs.fix()
assert degrees_of_freedom(m) == -2
m.gb.outputs.unfix()

# verify DOFs works on greybox connected to other vars on a model via constraints
m.a1 = Var(initialize=1)
m.a1.fix()
m.gb.inputs["a1"].unfix()
m.a1_eq = Constraint(expr=m.a1 == m.gb.inputs["a1"])
assert degrees_of_freedom(m) == 0
m.o1 = Var(initialize=1)
m.o1_eq = Constraint(expr=m.o1 == m.gb.outputs["o1"])
m.o1.fix()
assert degrees_of_freedom(m) == -1


@pytest.mark.unit
def test_large_residuals_set(m):
# Initialize derivative var values so no errors occur
Expand Down
Loading