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

Ensure constants written correctly to LP/NL files #2953

Merged
merged 21 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
97fc9a5
Ensure numeric constants in LP/NL files are emitted as ints/floats
jsiirola Aug 16, 2023
42ab3c1
Apply black
jsiirola Aug 17, 2023
a6c238f
Update Var/Constraint so .lb/ub always return valid native_numeric_types
jsiirola Aug 17, 2023
7d131d1
Revert some changes to the LP/NL writers that duplicated work
jsiirola Aug 17, 2023
e9815ed
Add numpy.ndarray to native_types (but not native_numeric_types)
jsiirola Aug 17, 2023
52ea558
Test ndarray in expressions and bounds
jsiirola Aug 17, 2023
805c235
Fix typos
jsiirola Aug 17, 2023
2ac96b9
Resolve decrepancy between AML and Kernel: lb/ub should return None f…
jsiirola Aug 18, 2023
0beea4a
Minor performance (avoid assignemnt to locals)
jsiirola Aug 18, 2023
996ac2b
Defer float resolution of suffix values until we know it is a 'valid'…
jsiirola Aug 21, 2023
a5687bf
Merge branch 'main' into lp-nl-cast-floats
jsiirola Aug 21, 2023
2713754
Fix undefined symbol
jsiirola Aug 21, 2023
0015980
Support InvalidValue in assertStructuredAlmostEqual
jsiirola Aug 23, 2023
1b6320c
InvalidValue.__repr__, __format__, __float__ should raise InvalidValu…
jsiirola Aug 23, 2023
30978f3
Mark PyROS test as expected failure (until it can be refactored)
jsiirola Aug 23, 2023
592c747
Apply black
jsiirola Aug 23, 2023
a6b4f3a
Improve information in InvalidNumber exception
jsiirola Aug 23, 2023
13dfde4
Merge branch 'main' into lp-nl-cast-floats
jsiirola Aug 23, 2023
5f03e1c
Fix test for current numpy distributions
jsiirola Aug 23, 2023
a75356e
Fix typos in exception messages; add tests of exceptions
jsiirola Aug 23, 2023
219c505
Apply black
jsiirola Aug 23, 2023
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
1 change: 1 addition & 0 deletions pyomo/common/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ def _finalize_matplotlib(module, available):
def _finalize_numpy(np, available):
if not available:
return
numeric_types.native_types.add(np.ndarray)
numeric_types.RegisterLogicalType(np.bool_)
for t in (
np.int_,
Expand Down
5 changes: 3 additions & 2 deletions pyomo/common/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import pytest as pytest

from pyomo.common.collections import Mapping, Sequence
from pyomo.common.errors import InvalidValueError
from pyomo.common.tee import capture_output

from unittest import mock
Expand All @@ -52,11 +53,11 @@ def _floatOrCall(val):
"""
try:
return float(val)
except TypeError:
except (TypeError, InvalidValueError):
pass
try:
return float(val())
except TypeError:
except (TypeError, InvalidValueError):
pass
try:
return val.value
Expand Down
4 changes: 4 additions & 0 deletions pyomo/contrib/pyros/tests/test_grcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4380,9 +4380,13 @@ def test_separation_subsolver_error(self):
),
)

# FIXME: This test is expected to fail now, as writing out invalid
# models generates an exception in the problem writer (and is never
# actually sent to the solver)
@unittest.skipUnless(
baron_license_is_valid, "Global NLP solver is not available and licensed."
)
@unittest.expectedFailure
def test_discrete_separation_subsolver_error(self):
"""
Test PyROS for two-stage problem with discrete type set,
Expand Down
91 changes: 42 additions & 49 deletions pyomo/core/base/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,12 @@
def has_lb(self):
"""Returns :const:`False` when the lower bound is
:const:`None` or negative infinity"""
return self.lower is not None
return self.lb is not None

def has_ub(self):
"""Returns :const:`False` when the upper bound is
:const:`None` or positive infinity"""
return self.upper is not None
return self.ub is not None

def lslack(self):
"""
Expand Down Expand Up @@ -338,50 +338,39 @@
"""Access the body of a constraint expression."""
if self._body is not None:
return self._body
else:
# The incoming RangedInequality had a potentially variable
# bound. The "body" is fine, but the bounds may not be
# (although the responsibility for those checks lies with the
# lower/upper properties)
body = self._expr.arg(1)
if body.__class__ in native_types and body is not None:
return as_numeric(body)
return body

def _lb(self):
if self._body is not None:
bound = self._lower
elif self._expr is None:
return None
else:
bound = self._expr.arg(0)
if not is_fixed(bound):
raise ValueError(
"Constraint '%s' is a Ranged Inequality with a "
"variable %s bound. Cannot normalize the "
"constraint or send it to a solver." % (self.name, 'lower')
)
return bound

def _ub(self):
if self._body is not None:
bound = self._upper
elif self._expr is None:
# The incoming RangedInequality had a potentially variable
# bound. The "body" is fine, but the bounds may not be
# (although the responsibility for those checks lies with the
# lower/upper properties)
body = self._expr.arg(1)
if body.__class__ in native_types and body is not None:
return as_numeric(body)

Check warning on line 347 in pyomo/core/base/constraint.py

View check run for this annotation

Codecov / codecov/patch

pyomo/core/base/constraint.py#L347

Added line #L347 was not covered by tests
return body

def _get_range_bound(self, range_arg):
# Equalities and simple inequalities can always be (directly)
# reformulated at construction time to force constant bounds.
# The only time we need to defer the determination of bounds is
# for ranged inequalities that contain non-constant bounds (so
# we *know* that the expr will have 3 args)
#
# It is possible that there is no expression at all (so catch that)
if self._expr is None:
return None
else:
bound = self._expr.arg(2)
if not is_fixed(bound):
raise ValueError(
"Constraint '%s' is a Ranged Inequality with a "
"variable %s bound. Cannot normalize the "
"constraint or send it to a solver." % (self.name, 'upper')
)
bound = self._expr.arg(range_arg)
if not is_fixed(bound):
raise ValueError(
"Constraint '%s' is a Ranged Inequality with a "
"variable %s bound. Cannot normalize the "
"constraint or send it to a solver."
% (self.name, {0: 'lower', 2: 'upper'}[range_arg])
)
return bound

@property
def lower(self):
"""Access the lower bound of a constraint expression."""
bound = self._lb()
bound = self._lower if self._body is not None else self._get_range_bound(0)
# Historically, constraint.lower was guaranteed to return a type
# derived from Pyomo NumericValue (or None). Replicate that
# functionality, although clients should in almost all cases
Expand All @@ -395,7 +384,7 @@
@property
def upper(self):
"""Access the upper bound of a constraint expression."""
bound = self._ub()
bound = self._upper if self._body is not None else self._get_range_bound(2)
# Historically, constraint.upper was guaranteed to return a type
# derived from Pyomo NumericValue (or None). Replicate that
# functionality, although clients should in almost all cases
Expand All @@ -409,13 +398,15 @@
@property
def lb(self):
"""Access the value of the lower bound of a constraint expression."""
bound = self._lb()
if bound.__class__ not in native_types:
bound = value(bound)
bound = self._lower if self._body is not None else self._get_range_bound(0)
if bound.__class__ not in native_numeric_types:
if bound is None:
return None
bound = float(value(bound))
if bound in _nonfinite_values or bound != bound:
# Note that "bound != bound" catches float('nan')
if bound == -_inf:
bound = None
return None
else:
raise ValueError(
"Constraint '%s' created with an invalid non-finite "
Expand All @@ -426,13 +417,15 @@
@property
def ub(self):
"""Access the value of the upper bound of a constraint expression."""
bound = self._ub()
if bound.__class__ not in native_types:
bound = value(bound)
bound = self._upper if self._body is not None else self._get_range_bound(2)
if bound.__class__ not in native_numeric_types:
if bound is None:
return None
bound = float(value(bound))
if bound in _nonfinite_values or bound != bound:
# Note that "bound != bound" catches float('nan')
if bound == _inf:
bound = None
return None
else:
raise ValueError(
"Constraint '%s' created with an invalid non-finite "
Expand Down
123 changes: 81 additions & 42 deletions pyomo/core/base/var.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@

_inf = float('inf')
_ninf = -_inf
_no_lower_bound = {None, _ninf}
_no_upper_bound = {None, _inf}
_nonfinite_values = {_inf, _ninf}
_known_global_real_domains = dict(
[(_, True) for _ in real_global_set_ids]
+ [(_, False) for _ in integer_global_set_ids]
Expand Down Expand Up @@ -110,12 +109,12 @@ class _VarData(ComponentData, NumericValue):
def has_lb(self):
"""Returns :const:`False` when the lower bound is
:const:`None` or negative infinity"""
return self.lb not in _no_lower_bound
return self.lb is not None

def has_ub(self):
"""Returns :const:`False` when the upper bound is
:const:`None` or positive infinity"""
return self.ub not in _no_upper_bound
return self.ub is not None

# TODO: deprecate this? Properties are generally preferred over "set*()"
def setlb(self, val):
Expand Down Expand Up @@ -450,54 +449,94 @@ def domain(self, domain):
def bounds(self):
# Custom implementation of _VarData.bounds to avoid unnecessary
# expression generation and duplicate calls to domain.bounds()
domain_bounds = self.domain.bounds()
if self._lb is None:
lb = domain_bounds[0]
else:
lb = self._lb
if lb.__class__ not in native_types:
lb = lb()
if domain_bounds[0] is not None:
lb = max(lb, domain_bounds[0])
if self._ub is None:
ub = domain_bounds[1]
else:
ub = self._ub
if ub.__class__ not in native_types:
ub = ub()
if domain_bounds[1] is not None:
ub = min(ub, domain_bounds[1])
return None if lb == _ninf else lb, None if ub == _inf else ub
domain_lb, domain_ub = self.domain.bounds()
# lb is the tighter of the domain and bounds
lb = self._lb
if lb.__class__ not in native_numeric_types:
if lb is not None:
lb = float(value(lb))
if lb in _nonfinite_values or lb != lb:
if lb == _ninf:
lb = None
else:
raise ValueError(
"Var '%s' created with an invalid non-finite "
"lower bound (%s)." % (self.name, lb)
)
if domain_lb is not None:
if lb is None:
lb = domain_lb
else:
lb = max(lb, domain_lb)
# ub is the tighter of the domain and bounds
ub = self._ub
if ub.__class__ not in native_numeric_types:
if ub is not None:
ub = float(value(ub))
if ub in _nonfinite_values or ub != ub:
if ub == _inf:
ub = None
else:
raise ValueError(
"Var '%s' created with an invalid non-finite "
"upper bound (%s)." % (self.name, ub)
)
if domain_ub is not None:
if ub is None:
ub = domain_ub
else:
ub = min(ub, domain_ub)
return lb, ub

@_VarData.lb.getter
def lb(self):
# Custom implementation of _VarData.lb to avoid unnecessary
# expression generation
dlb, _ = self.domain.bounds()
if self._lb is None:
lb = dlb
else:
lb = self._lb
if lb.__class__ not in native_types:
lb = lb()
if dlb is not None:
lb = max(lb, dlb)
return None if lb == _ninf else lb
domain_lb, domain_ub = self.domain.bounds()
# lb is the tighter of the domain and bounds
lb = self._lb
if lb.__class__ not in native_numeric_types:
if lb is not None:
lb = float(value(lb))
if lb in _nonfinite_values or lb != lb:
if lb == _ninf:
lb = None
else:
raise ValueError(
"Var '%s' created with an invalid non-finite "
"lower bound (%s)." % (self.name, lb)
)
if domain_lb is not None:
if lb is None:
lb = domain_lb
else:
lb = max(lb, domain_lb)
return lb

@_VarData.ub.getter
def ub(self):
# Custom implementation of _VarData.ub to avoid unnecessary
# expression generation
_, dub = self.domain.bounds()
if self._ub is None:
ub = dub
else:
ub = self._ub
if ub.__class__ not in native_types:
ub = ub()
if dub is not None:
ub = min(ub, dub)
return None if ub == _inf else ub
domain_lb, domain_ub = self.domain.bounds()
# ub is the tighter of the domain and bounds
ub = self._ub
if ub.__class__ not in native_numeric_types:
if ub is not None:
ub = float(value(ub))
if ub in _nonfinite_values or ub != ub:
if ub == _inf:
ub = None
else:
raise ValueError(
"Var '%s' created with an invalid non-finite "
"upper bound (%s)." % (self.name, ub)
)
if domain_ub is not None:
if ub is None:
ub = domain_ub
else:
ub = min(ub, domain_ub)
return ub

@property
def lower(self):
Expand Down
10 changes: 8 additions & 2 deletions pyomo/core/kernel/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ def upper(self, ub):
@property
def lb(self):
"""The value of the lower bound of the constraint"""
return value(self._lb)
lb = value(self.lower)
if lb == _neg_inf:
return None
return lb

@lb.setter
def lb(self, lb):
Expand All @@ -227,7 +230,10 @@ def lb(self, lb):
@property
def ub(self):
"""The value of the upper bound of the constraint"""
return value(self._ub)
ub = value(self.upper)
if ub == _pos_inf:
return None
return ub

@ub.setter
def ub(self, ub):
Expand Down
Loading