Skip to content

Commit

Permalink
Merge pull request #2953 from jsiirola/lp-nl-cast-floats
Browse files Browse the repository at this point in the history
Ensure constants written correctly to LP/NL files
  • Loading branch information
mrmundt authored Aug 23, 2023
2 parents 9a5ef6b + 219c505 commit 984586f
Show file tree
Hide file tree
Showing 18 changed files with 509 additions and 234 deletions.
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 __call__(self, exception=True):
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 @@ def body(self):
"""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)
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 @@ def lower(self):
@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 @@ def upper(self):
@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 @@ def lb(self):
@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

0 comments on commit 984586f

Please sign in to comment.