Skip to content

Commit

Permalink
Merge pull request Pyomo#3280 from shermanjasonaf/adjust-pyros-nl-wri…
Browse files Browse the repository at this point in the history
…ter-tol

Temporarily Adjust NL Writer Feasibility Tolerance within PyROS
  • Loading branch information
blnicho authored Jul 9, 2024
2 parents 86aa282 + 4341d2b commit 7375570
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 2 deletions.
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

0 comments on commit 7375570

Please sign in to comment.