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 13 commits into
base: main
Choose a base branch
from
42 changes: 40 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,31 @@ 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(
1 for _ in activated_equalities_generator(block)
) + number_grey_box_equalities(block)


def number_grey_box_equalities(block):
Copy link
Member

Choose a reason for hiding this comment

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

number_of_X is less ambiguous in English.
Can add -> int to indicate return type and, if it's possible, the expected type of block.
In docs: "Method to" is superfluous and at any rate this is a function.

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

"""
Method to return the number of equality constraints in GreyBox
Copy link
Member

Choose a reason for hiding this comment

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

I would say, "Compute total number of equality constraints for all GreyBox objects in this block."

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

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.
Args:
block : model to be studied
Returns:
Number of activated equality Constraint components in 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 +554,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 +1059,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])
Copy link
Member

Choose a reason for hiding this comment

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

Here, I think you need to make sure that these variables don't already appear in var_set. If the ExternalGreyBoxBlock is constructed with the inputs or outputs argument, then the block will contain references to the user-provided variables, which may have already been counted.

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