Skip to content

Commit

Permalink
Merge branch 'main' into constraint-store-expr-only
Browse files Browse the repository at this point in the history
  • Loading branch information
jsiirola authored Jul 24, 2024
2 parents 08ccf40 + 92a9d3b commit f5ba8a2
Show file tree
Hide file tree
Showing 31 changed files with 1,664 additions and 200 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ jobs:
$BARON_DIR = "${env:TPL_DIR}/baron"
echo "$BARON_DIR" | `
Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
$URL = "https://www.minlp.com/downloads/xecs/baron/current/"
$URL = "https://minlp.com/downloads/xecs/baron/current/"
if ( "${{matrix.TARGET}}" -eq "win" ) {
$INSTALLER = "${env:DOWNLOAD_DIR}/baron_install.exe"
$URL += "baron-win64.exe"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_pr_and_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ jobs:
$BARON_DIR = "${env:TPL_DIR}/baron"
echo "$BARON_DIR" | `
Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
$URL = "https://www.minlp.com/downloads/xecs/baron/current/"
$URL = "https://minlp.com/downloads/xecs/baron/current/"
if ( "${{matrix.TARGET}}" -eq "win" ) {
$INSTALLER = "${env:DOWNLOAD_DIR}/baron_install.exe"
$URL += "baron-win64.exe"
Expand Down
25 changes: 20 additions & 5 deletions .jenkins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,28 @@ if test -z "$MODE" -o "$MODE" == test; then
coverage report -i || exit 1
coverage xml -i || exit 1
export OS=`uname`
if test -n "$CODECOV_TOKEN"; then
CODECOV_JOB_NAME=`echo ${JOB_NAME} | sed -r 's/^(.*autotest_)?Pyomo_([^\/]+).*/\2/'`.$BUILD_NUMBER.$python
if test -z "$PYOMO_SOURCE_SHA"; then
PYOMO_SOURCE_SHA=$GIT_COMMIT
fi
if test -n "$CODECOV_TOKEN" -a -n "$PYOMO_SOURCE_SHA"; then
CODECOV_JOB_NAME=$(echo ${JOB_NAME} \
| sed -r 's/^(.*autotest_)?Pyomo_([^\/]+).*/\2/').$BUILD_NUMBER.$python
if test -z "$CODECOV_REPO_OWNER"; then
CODECOV_REPO_OWNER="pyomo"
if test -n "$PYOMO_SOURCE_REPO"; then
CODECOV_REPO_OWNER=$(echo "$PYOMO_SOURCE_REPO" | cut -d '/' -f 4)
elif test -n "$GIT_URL"; then
CODECOV_REPO_OWNER=$(echo "$GIT_URL" | cut -d '/' -f 4)
else
CODECOV_REPO_OWNER=""
fi
fi
if test -z "CODECOV_SOURCE_BRANCH"; then
CODECOV_SOURCE_BRANCH="main"
if test -z "$CODECOV_SOURCE_BRANCH"; then
CODECOV_SOURCE_BRANCH=$(git branch -av --contains "$PYOMO_SOURCE_SHA" \
| grep "${PYOMO_SOURCE_SHA:0:7}" | grep "/origin/" \
| cut -d '/' -f 3 | cut -d' ' -f 1)
if test -z "$CODECOV_SOURCE_BRANCH"; then
CODECOV_SOURCE_BRANCH=main
fi
fi
i=0
while /bin/true; do
Expand Down
3 changes: 3 additions & 0 deletions examples/pyomo/tutorials/set.dat
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ set S[5] := 2 3;

set T[2] := 1 3;
set T[5] := 2 3;

set X[2] := 1;
set X[5] := 2 3;
8 changes: 6 additions & 2 deletions examples/pyomo/tutorials/set.out
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
23 Set Declarations
24 Set Declarations
A : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 3 : {1, 2, 3}
Expand Down Expand Up @@ -89,5 +89,9 @@
2 : 1 : Any : 5 : {1, 3, 5, 7, 9}
3 : 1 : Any : 5 : {1, 4, 7, 10, 13}
4 : 1 : Any : 5 : {1, 5, 9, 13, 17}
X : Size=2, Index=B, Ordered=Insertion
Key : Dimen : Domain : Size : Members
2 : 1 : S[2] : 1 : {1,}
5 : 1 : S[5] : 2 : {2, 3}

23 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S T U V
24 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S X T U V
7 changes: 7 additions & 0 deletions examples/pyomo/tutorials/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ def P_init(model, i, j):
#
model.S = Set(model.B, within=model.A)

#
# Validation of a set array can also be linked to another set array. If so, the
# elements under each index must also be found under the corresponding index in
# the validation set array:
#
model.X = Set(model.B, within=model.S)


#
# Validation of set arrays can also be performed with the _validate_ option.
Expand Down
2 changes: 1 addition & 1 deletion pyomo/contrib/appsi/solvers/highs.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ def _remove_constraints(self, cons: List[ConstraintData]):
indices_to_remove.append(con_ndx)
self._mutable_helpers.pop(con, None)
self._solver_model.deleteRows(
len(indices_to_remove), np.array(indices_to_remove)
len(indices_to_remove), np.sort(np.array(indices_to_remove))
)
con_ndx = 0
new_con_map = dict()
Expand Down
37 changes: 37 additions & 0 deletions pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,43 @@ def test_mutable_params_with_remove_vars(self):
res = opt.solve(m)
self.assertAlmostEqual(res.best_feasible_objective, -9)

def test_fix_and_unfix(self):
# Tests issue https://github.com/Pyomo/pyomo/issues/3127

m = pe.ConcreteModel()
m.x = pe.Var(domain=pe.Binary)
m.y = pe.Var(domain=pe.Binary)
m.fx = pe.Var(domain=pe.NonNegativeReals)
m.fy = pe.Var(domain=pe.NonNegativeReals)
m.c1 = pe.Constraint(expr=m.fx <= m.x)
m.c2 = pe.Constraint(expr=m.fy <= m.y)
m.c3 = pe.Constraint(expr=m.x + m.y <= 1)

m.obj = pe.Objective(expr=m.fx * 0.5 + m.fy * 0.4, sense=pe.maximize)

opt = Highs()

# solution 1 has m.x == 1 and m.y == 0
r = opt.solve(m)
self.assertAlmostEqual(m.fx.value, 1, places=5)
self.assertAlmostEqual(m.fy.value, 0, places=5)
self.assertAlmostEqual(r.best_feasible_objective, 0.5, places=5)

# solution 2 has m.x == 0 and m.y == 1
m.y.fix(1)
r = opt.solve(m)
self.assertAlmostEqual(m.fx.value, 0, places=5)
self.assertAlmostEqual(m.fy.value, 1, places=5)
self.assertAlmostEqual(r.best_feasible_objective, 0.4, places=5)

# solution 3 should be equal solution 1
m.y.unfix()
m.x.fix(1)
r = opt.solve(m)
self.assertAlmostEqual(m.fx.value, 1, places=5)
self.assertAlmostEqual(m.fy.value, 0, places=5)
self.assertAlmostEqual(r.best_feasible_objective, 0.5, places=5)

def test_capture_highs_output(self):
# tests issue #3003
#
Expand Down
11 changes: 10 additions & 1 deletion pyomo/contrib/incidence_analysis/scc_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,14 @@ def generate_strongly_connected_components(
)
)

assert len(variables) == len(constraints)
if len(variables) != len(constraints):
nvar = len(variables)
ncon = len(constraints)
raise RuntimeError(
"generate_strongly_connected_components only supports systems with the"
f" same numbers of variables and equality constraints. Got {nvar}"
f" variables and {ncon} constraints."
)
if igraph is None:
igraph = IncidenceGraphInterface()

Expand All @@ -78,6 +85,8 @@ def generate_strongly_connected_components(
subsets, include_fixed=include_fixed
):
# TODO: How does len scale for reference-to-list?
# If this assert fails, it may be due to a bug in block_triangularize
# or generate_subsystem_block.
assert len(block.vars) == len(block.cons)
yield (block, inputs)

Expand Down
17 changes: 17 additions & 0 deletions pyomo/contrib/incidence_analysis/tests/test_scc_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,5 +501,22 @@ def test_with_inequalities(self):
self.assertEqual(m.x[3].value, 1.0)


@unittest.skipUnless(scipy_available, "SciPy is not available")
@unittest.skipUnless(networkx_available, "NetworkX is not available")
class TestExceptions(unittest.TestCase):
def test_nonsquare_system(self):
m = pyo.ConcreteModel()
m.x = pyo.Var([1, 2], initialize=1)
m.eq = pyo.Constraint(expr=m.x[1] + m.x[2] == 1)

msg = "Got 2 variables and 1 constraints"
with self.assertRaisesRegex(RuntimeError, msg):
list(
generate_strongly_connected_components(
constraints=[m.eq], variables=[m.x[1], m.x[2]]
)
)


if __name__ == "__main__":
unittest.main()
73 changes: 72 additions & 1 deletion pyomo/contrib/pyros/tests/test_grcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from pyomo.contrib.pyros.util import get_vars_from_component
from pyomo.contrib.pyros.util import identify_objective_functions
from pyomo.common.collections import Bunch
from pyomo.repn.plugins import nl_writer as pyomo_nl_writer
import time
import math
from pyomo.contrib.pyros.util import time_code
Expand All @@ -68,7 +69,7 @@
from pyomo.common.dependencies import numpy as np, numpy_available
from pyomo.common.dependencies import scipy as sp, scipy_available
from pyomo.environ import maximize as pyo_max
from pyomo.common.errors import ApplicationError
from pyomo.common.errors import ApplicationError, InfeasibleConstraintException
from pyomo.opt import (
SolverResults,
SolverStatus,
Expand Down Expand Up @@ -4616,6 +4617,76 @@ def test_discrete_separation_subsolver_error(self):
),
)

@unittest.skipUnless(ipopt_available, "IPOPT is not available.")
def test_pyros_nl_writer_tol(self):
"""
Test PyROS subsolver call routine behavior
with respect to the NL writer tolerance is as
expected.
"""
m = ConcreteModel()
m.q = Param(initialize=1, mutable=True)
m.x1 = Var(initialize=1, bounds=(0, 1))
m.x2 = Var(initialize=2, bounds=(0, m.q))
m.obj = Objective(expr=m.x1 + m.x2)

# fixed just inside the PyROS-specified NL writer tolerance.
m.x1.fix(m.x1.upper + 9.9e-5)

current_nl_writer_tol = pyomo_nl_writer.TOL
ipopt_solver = SolverFactory("ipopt")
pyros_solver = SolverFactory("pyros")

pyros_solver.solve(
model=m,
first_stage_variables=[m.x1],
second_stage_variables=[m.x2],
uncertain_params=[m.q],
uncertainty_set=BoxSet([[0, 1]]),
local_solver=ipopt_solver,
global_solver=ipopt_solver,
decision_rule_order=0,
solve_master_globally=False,
bypass_global_separation=True,
)

self.assertEqual(
pyomo_nl_writer.TOL,
current_nl_writer_tol,
msg="Pyomo NL writer tolerance not restored as expected.",
)

# fixed just outside the PyROS-specified NL writer tolerance.
# this should be exceptional.
m.x1.fix(m.x1.upper + 1.01e-4)

err_msg = (
"model contains a trivially infeasible variable.*x1"
".*fixed.*outside bounds"
)
with self.assertRaisesRegex(InfeasibleConstraintException, err_msg):
pyros_solver.solve(
model=m,
first_stage_variables=[m.x1],
second_stage_variables=[m.x2],
uncertain_params=[m.q],
uncertainty_set=BoxSet([[0, 1]]),
local_solver=ipopt_solver,
global_solver=ipopt_solver,
decision_rule_order=0,
solve_master_globally=False,
bypass_global_separation=True,
)

self.assertEqual(
pyomo_nl_writer.TOL,
current_nl_writer_tol,
msg=(
"Pyomo NL writer tolerance not restored as expected "
"after exceptional test."
),
)

@unittest.skipUnless(
baron_license_is_valid, "Global NLP solver is not available and licensed."
)
Expand Down
22 changes: 21 additions & 1 deletion pyomo/contrib/pyros/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from pyomo.core.expr import value
from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression
from pyomo.repn.standard_repn import generate_standard_repn
from pyomo.repn.plugins import nl_writer as pyomo_nl_writer
from pyomo.core.expr.visitor import (
identify_variables,
identify_mutable_parameters,
Expand Down Expand Up @@ -377,7 +378,14 @@ def revert_solver_max_time_adjustment(
elif isinstance(solver, SolverFactory.get_class("baron")):
options_key = "MaxTime"
elif isinstance(solver, SolverFactory.get_class("ipopt")):
options_key = "max_cpu_time"
options_key = (
# IPOPT 3.14.0+ added support for specifying
# wall time limit explicitly; this is preferred
# over CPU time limit
"max_wall_time"
if solver.version() >= (3, 14, 0, 0)
else "max_cpu_time"
)
elif isinstance(solver, SolverFactory.get_class("scip")):
options_key = "limits/time"
else:
Expand Down Expand Up @@ -1809,6 +1817,16 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg):
timing_obj.start_timer(timer_name)
tt_timer.tic(msg=None)

# tentative: reduce risk of InfeasibleConstraintException
# occurring due to discrepancies between Pyomo NL writer
# tolerance and (default) subordinate solver (e.g. IPOPT)
# feasibility tolerances.
# e.g., a Var fixed outside bounds beyond the Pyomo NL writer
# tolerance, but still within the default IPOPT feasibility
# tolerance
current_nl_writer_tol = pyomo_nl_writer.TOL
pyomo_nl_writer.TOL = 1e-4

try:
results = solver.solve(
model,
Expand All @@ -1827,6 +1845,8 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg):
results.solver, TIC_TOC_SOLVE_TIME_ATTR, tt_timer.toc(msg=None, delta=True)
)
finally:
pyomo_nl_writer.TOL = current_nl_writer_tol

timing_obj.stop_timer(timer_name)
revert_solver_max_time_adjustment(
solver, orig_setting, custom_setting_present, config
Expand Down
5 changes: 3 additions & 2 deletions pyomo/core/base/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -1932,7 +1932,8 @@ class Set(IndexedComponent):
within : initialiser(set), optional
A set that defines the valid values that can be contained
in this set
in this set. If the latter is indexed, the former can be indexed or
non-indexed, in which case it applies to all indices.
domain : initializer(set), optional
A set that defines the valid values that can be contained
in this set
Expand Down Expand Up @@ -2218,7 +2219,7 @@ def _getitem_when_not_present(self, index):

domain = self._init_domain(_block, index, self)
if domain is not None:
domain.construct()
domain.parent_component().construct()
if _d is UnknownSetDimen and domain is not None and domain.dimen is not None:
_d = domain.dimen

Expand Down
19 changes: 13 additions & 6 deletions pyomo/core/expr/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,14 @@ def assertExpressionsEqual(test, a, b, include_named_exprs=True, places=None):
test.assertEqual(len(prefix_a), len(prefix_b))
for _a, _b in zip(prefix_a, prefix_b):
test.assertIs(_a.__class__, _b.__class__)
if places is None:
test.assertEqual(_a, _b)
# If _a is nan, check _b is nan
if _a != _a:
test.assertTrue(_b != _b)
else:
test.assertAlmostEqual(_a, _b, places=places)
if places is None:
test.assertEqual(_a, _b)
else:
test.assertAlmostEqual(_a, _b, places=places)
except (PyomoException, AssertionError):
test.fail(
f"Expressions not equal:\n\t"
Expand Down Expand Up @@ -292,10 +296,13 @@ def assertExpressionsStructurallyEqual(
for _a, _b in zip(prefix_a, prefix_b):
if _a.__class__ not in native_types and _b.__class__ not in native_types:
test.assertIs(_a.__class__, _b.__class__)
if places is None:
test.assertEqual(_a, _b)
if _a != _a:
test.assertTrue(_b != _b)
else:
test.assertAlmostEqual(_a, _b, places=places)
if places is None:
test.assertEqual(_a, _b)
else:
test.assertAlmostEqual(_a, _b, places=places)
except (PyomoException, AssertionError):
test.fail(
f"Expressions not structurally equal:\n\t"
Expand Down
Loading

0 comments on commit f5ba8a2

Please sign in to comment.