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

Checks for variables with None value in NLP #1564

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
74 changes: 73 additions & 1 deletion idaes/core/util/model_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
deactivated_objectives_set,
variables_in_activated_constraints_set,
variables_not_in_activated_constraints_set,
variables_with_none_value_in_activated_equalities_set,
number_activated_greybox_equalities,
number_deactivated_greybox_equalities,
activated_greybox_block_set,
Expand Down Expand Up @@ -603,7 +604,7 @@ def display_variables_at_or_outside_bounds(self, stream=None):
tolerance=self.config.variable_bounds_violation_tolerance,
)
],
title=f"The following variable(s) have values at or outside their bounds "
title="The following variable(s) have values at or outside their bounds "
f"(tol={self.config.variable_bounds_violation_tolerance:.1E}):",
header="=",
footer="=",
Expand Down Expand Up @@ -631,6 +632,54 @@ def display_variables_with_none_value(self, stream=None):
footer="=",
)

def display_variables_with_none_value_in_activated_constraints(self, stream=None):
"""
Prints a list of variables with values of None that are present in the
mathematical program generated to solve the model. This list includes only
variables in active constraints that are reachable through active blocks.

Args:
stream: an I/O object to write the list to (default = stdout)

Returns:
None

"""
if stream is None:
stream = sys.stdout

_write_report_section(
stream=stream,
lines_list=[
f"{v.name}"
for v in variables_with_none_value_in_activated_equalities_set(
self._model
)
],
title="The following variable(s) have a value of None:",
header="=",
footer="=",
)

def _verify_active_variables_initialized(self, stream=None):
"""
Validate that all variables are initialized (i.e., have values set to
something other than None) before doing further numerical analysis.
Stream argument provided for forward compatibility (in case we want
to print a list or something).
"""
n_uninit = len(
variables_with_none_value_in_activated_equalities_set(self._model)
)
if n_uninit > 0:
raise RuntimeError(
f"Found {n_uninit} variables with a value of None in the mathematical "
"program generated by the model. They must be initialized with non-None "
"values before numerical analysis can proceed. Run "
+ self.display_variables_with_none_value_in_activated_constraints.__name__
+ " to display a list of these variables."
)

def display_variables_with_value_near_zero(self, stream=None):
"""
Prints a list of variables with a value close to zero. The tolerance
Expand Down Expand Up @@ -942,6 +991,8 @@ def display_variables_with_extreme_jacobians(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -977,6 +1028,8 @@ def display_constraints_with_extreme_jacobians(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1013,6 +1066,8 @@ def display_extreme_jacobian_entries(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1046,6 +1101,8 @@ def display_near_parallel_constraints(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1078,6 +1135,8 @@ def display_near_parallel_variables(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1161,6 +1220,8 @@ def display_constraints_with_mismatched_terms(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1194,6 +1255,8 @@ def display_constraints_with_canceling_terms(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1232,6 +1295,8 @@ def display_problematic_constraint_terms(
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1321,6 +1386,11 @@ def display_constraints_with_no_free_variables(self, stream=None):
None

"""
# Although, in principle, this method doesn't require
# all variables to be initialized, its current
# implementation does.
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout

Expand Down Expand Up @@ -1803,6 +1873,8 @@ def report_numerical_issues(self, stream=None):
None

"""
self._verify_active_variables_initialized(stream=stream)

if stream is None:
stream = sys.stdout
jac, nlp = get_jacobian(self._model, scaled=False)
Expand Down
35 changes: 35 additions & 0 deletions idaes/core/util/model_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,41 @@ def number_active_variables_in_deactivated_blocks(block):
return len(active_variables_in_deactivated_blocks_set(block))


def variables_with_none_value_in_activated_equalities_set(block):
"""
Method to return a ComponentSet of all Var components which
have a value of None in the set of activated constraints.

Args:
block : model to be studied

Returns:
A ComponentSet including all Var components which
have a value of None in the set of activated constraints.
"""
var_set = ComponentSet()
for v in variables_in_activated_equalities_set(block):
if v.value is None:
var_set.add(v)
return var_set


def number_variables_with_none_value_in_activated_equalities(block):
"""
Method to return the number of Var components which
have a value of None in the set of activated constraints.

Args:
block : model to be studied

Returns:
Number of Var components which
have a value of None in the set of activated constraints.
"""

return len(variables_with_none_value_in_activated_equalities_set(block))


# -------------------------------------------------------------------------
# Reporting methods
def report_statistics(block, ostream=None):
Expand Down
145 changes: 134 additions & 11 deletions idaes/core/util/tests/test_model_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,129 @@ def test_vars_near_zero(model):
assert i is model.v1


class TestVariablesWithNoneValue:
def err_msg(self, j):
return (
f"Found {j} variables with a value of None in the mathematical "
"program generated by the model. They must be initialized with non-None "
"values before numerical analysis can proceed. Run "
+ DiagnosticsToolbox.display_variables_with_none_value_in_activated_constraints.__name__
+ " to display a list of these variables."
)

@pytest.fixture(scope="function")
def model(self):
m = ConcreteModel()
m.u = Var()
m.w = Var(initialize=1)
m.x = Var(initialize=1)
m.y = Var(range(3), initialize=[1, None, 2])
m.z = Var()

m.con_w = Constraint(expr=m.w == 0)
m.con_x = Constraint(expr=m.x == 1)

def rule_con_y(b, i):
return b.y[i] == 3

m.con_y = Constraint(range(3), rule=rule_con_y)
m.con_z = Constraint(expr=m.x + m.z == -1)

m.block1 = Block()
m.block1.a = Var()
m.block1.b = Var(initialize=7)
m.block1.c = Var(initialize=-5)
m.block1.d = Var()

m.block1.con_a = Constraint(expr=m.block1.a**2 + m.block1.b == 1)
m.block1.con_b = Constraint(expr=m.block1.b + m.block1.c == 3)
m.block1.con_c = Constraint(expr=m.block1.c + m.block1.a == -4)
return m

@pytest.mark.unit
def test_verify_active_variables_initialized(self, model):
diag_tbx = DiagnosticsToolbox(model)
with pytest.raises(
RuntimeError,
match=self.err_msg(3),
):
diag_tbx._verify_active_variables_initialized()

model.block1.deactivate()
with pytest.raises(
RuntimeError,
match=self.err_msg(2),
):
diag_tbx._verify_active_variables_initialized()

model.con_y[1].deactivate()
model.con_z.deactivate()
# No uninitialized variables in remaining constraints
diag_tbx._verify_active_variables_initialized()

@pytest.mark.unit
def test_error_handling(self, model):
diag_tbx = DiagnosticsToolbox(model)
methods = [
"display_variables_with_extreme_jacobians",
"display_constraints_with_extreme_jacobians",
"display_extreme_jacobian_entries",
"display_near_parallel_constraints",
"display_near_parallel_variables",
"display_constraints_with_mismatched_terms",
"display_constraints_with_canceling_terms",
"display_constraints_with_no_free_variables",
"report_numerical_issues",
]
for mthd in methods:
mthd_obj = getattr(diag_tbx, mthd)
with pytest.raises(
RuntimeError,
match=self.err_msg(3),
):
mthd_obj()

@pytest.mark.unit
def test_display_problematic_constraint_terms(self, model):
"""
This method needs to be split in a separate function
dallan-keylogic marked this conversation as resolved.
Show resolved Hide resolved
because it takes a constraint as an argument.
"""
diag_tbx = DiagnosticsToolbox(model)
with pytest.raises(
RuntimeError,
match=self.err_msg(3),
):
diag_tbx.display_problematic_constraint_terms(model.con_y[1])
Copy link
Contributor

@adam-a-a adam-a-a Feb 6, 2025

Choose a reason for hiding this comment

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

@dallan-keylogic Just curious--and we could easily loop through constraints ourselves--but is there another method that display problematic constraint terms for all active constraints? Unrelated to this PR, but figured I'd ask before checking.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or alternatively, do we have a method that simply displays a list of constraints that have been identified as having problematic terms?

Copy link
Member

Choose a reason for hiding this comment

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

@adam-a-a There are separate methods for finding all constraints with either mismatched terms or potential cancellations. This method is here to provide more detail on one specific constraint, as displaying all information for all constraints upfront would be overwhelming.

# Right now the error message should show whether or not
dallan-keylogic marked this conversation as resolved.
Show resolved Hide resolved
# a problematic constraint is called.
with pytest.raises(
RuntimeError,
match=self.err_msg(3),
):
diag_tbx.display_problematic_constraint_terms(model.con_y[0])

@pytest.mark.unit
def test_display_variables_with_none_value_in_activated_constraints(self, model):
diag_tbx = DiagnosticsToolbox(model)
stream = StringIO()

diag_tbx.display_variables_with_none_value_in_activated_constraints(
stream=stream,
)

expected = """====================================================================================
The following variable(s) have a value of None:

y[1]
z
block1.a

====================================================================================
"""
assert stream.getvalue() == expected


@pytest.mark.unit
def test_vars_with_none_value(model):
none_value = _vars_with_none_value(model)
Expand Down Expand Up @@ -976,8 +1099,8 @@ def test_display_overconstrained_set(self, model):
def test_display_variables_with_extreme_jacobians(self):
model = ConcreteModel()
model.v1 = Var(initialize=1e-8)
model.v2 = Var()
model.v3 = Var()
model.v2 = Var(initialize=1)
model.v3 = Var(initialize=1)

model.c1 = Constraint(expr=model.v1 == model.v2)
model.c2 = Constraint(expr=model.v1 == 1e-8 * model.v3)
Expand All @@ -1004,8 +1127,8 @@ def test_display_variables_with_extreme_jacobians(self):
def test_display_constraints_with_extreme_jacobians(self):
model = ConcreteModel()
model.v1 = Var(initialize=1e-8)
model.v2 = Var()
model.v3 = Var()
model.v2 = Var(initialize=1)
model.v3 = Var(initialize=1)

model.c1 = Constraint(expr=model.v1 == model.v2)
model.c2 = Constraint(expr=model.v1 == 1e-8 * model.v3)
Expand All @@ -1030,8 +1153,8 @@ def test_display_constraints_with_extreme_jacobians(self):
def test_display_extreme_jacobian_entries(self):
model = ConcreteModel()
model.v1 = Var(initialize=1e-8)
model.v2 = Var()
model.v3 = Var()
model.v2 = Var(initialize=1)
model.v3 = Var(initialize=1)

model.c1 = Constraint(expr=model.v1 == model.v2)
model.c2 = Constraint(expr=model.v1 == 1e-8 * model.v3)
Expand Down Expand Up @@ -1060,8 +1183,8 @@ def test_display_extreme_jacobian_entries(self):
def test_display_near_parallel_constraints(self):
model = ConcreteModel()
model.v1 = Var(initialize=1e-8)
model.v2 = Var()
model.v3 = Var()
model.v2 = Var(initialize=1)
model.v3 = Var(initialize=1)

model.c1 = Constraint(expr=model.v1 == model.v2)
model.c2 = Constraint(expr=model.v1 == 1e-8 * model.v3)
Expand All @@ -1087,9 +1210,9 @@ def test_display_near_parallel_constraints(self):
def test_display_near_parallel_variables(self):
model = ConcreteModel()
model.v1 = Var(initialize=1e-8)
model.v2 = Var()
model.v3 = Var()
model.v4 = Var()
model.v2 = Var(initialize=1)
model.v3 = Var(initialize=1)
model.v4 = Var(initialize=1)

model.c1 = Constraint(expr=1e-8 * model.v1 == 1e-8 * model.v2 - 1e-8 * model.v4)
model.c2 = Constraint(expr=1e-8 * model.v1 + 1e-8 * model.v4 == model.v3)
Expand Down
Loading
Loading