Skip to content

Modify two PyROS solver tests #3694

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

Merged
merged 4 commits into from
Aug 12, 2025
Merged
Changes from all 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
156 changes: 67 additions & 89 deletions pyomo/contrib/pyros/tests/test_grcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pyomo.common.log import LoggingIntercept
from pyomo.common.tee import capture_output
from pyomo.core.base.set_types import NonNegativeIntegers
from pyomo.core.base.units_container import pint_available
from pyomo.repn.plugins import nl_writer as pyomo_nl_writer
import pyomo.repn.ampl as pyomo_ampl_repn
from pyomo.common.dependencies import (
Expand Down Expand Up @@ -61,6 +62,7 @@
value,
maximize,
minimize,
units,
)

from pyomo.contrib.pyros.solve_data import ROSolveResults
Expand Down Expand Up @@ -641,57 +643,46 @@ def test_identifying_violating_param_realization(self):
msg="Robust infeasible model terminated in 0 iterations (nominal case).",
)

@unittest.skipUnless(
baron_license_is_valid, "Global NLP solver is not available and licensed."
)
@unittest.skipUnless(
baron_version < (23, 1, 5) or baron_version >= (23, 6, 23),
"Test known to fail for BARON 23.1.5 and versions preceding 23.6.23",
)
@unittest.skipUnless(ipopt_available, "IPOPT not available.")
def test_terminate_with_max_iter(self):
m = build_leyffer_two_cons()

# Define the uncertainty set
interval = BoxSet(bounds=[(0.25, 2)])
m = ConcreteModel()
m.q = Param(initialize=0, mutable=True)
m.x = Var(initialize=None, bounds=[-2, 2])
m.con = Constraint(expr=m.x >= m.q)
m.obj = Objective(expr=m.x)

# Instantiate the PyROS solver
ipopt = SolverFactory("ipopt")
pyros_solver = SolverFactory("pyros")

# Define subsolvers utilized in the algorithm
local_subsolver = SolverFactory('baron')
global_subsolver = SolverFactory("baron")

# Call the PyROS solver
results = pyros_solver.solve(
pyros_args = dict(
model=m,
first_stage_variables=[m.x1],
second_stage_variables=[m.x2],
uncertain_params=[m.u],
uncertainty_set=interval,
local_solver=local_subsolver,
global_solver=global_subsolver,
options={
"objective_focus": ObjectiveType.worst_case,
"solve_master_globally": True,
"max_iter": 1,
"decision_rule_order": 2,
},
first_stage_variables=[m.x],
second_stage_variables=[],
uncertain_params=[m.q],
uncertainty_set=BoxSet([[0, 1]]),
local_solver=ipopt,
global_solver=ipopt,
decision_rule_order=2,
)

# should require more than one iteration to solve,
# so max_iter status expected
res1 = pyros_solver.solve(**pyros_args, max_iter=1)
self.assertEqual(
results.pyros_termination_condition,
pyrosTerminationCondition.max_iter,
msg="Returned termination condition is not return max_iter.",
res1.pyros_termination_condition, pyrosTerminationCondition.max_iter
)
self.assertEqual(res1.iterations, 1)
self.assertAlmostEqual(res1.final_objective_value, 0)
self.assertEqual(m.x.value, None)

# should require only 2 iterations to solve,
# so robust feasible solution expected
res2 = pyros_solver.solve(**pyros_args, max_iter=2)
self.assertEqual(
results.iterations,
1,
msg=(
f"Number of iterations in results object is {results.iterations}, "
f"but expected value 1."
),
res2.pyros_termination_condition, pyrosTerminationCondition.robust_feasible
)
self.assertEqual(res2.iterations, 2)
self.assertAlmostEqual(res2.final_objective_value, 1)
self.assertAlmostEqual(m.x.value, 1)

@unittest.skipUnless(
baron_license_is_valid, "Global NLP solver is not available and licensed."
Expand Down Expand Up @@ -2848,75 +2839,62 @@ def test_multiple_objs(self):
)


class TestMasterFeasibilityUnitConsistency(unittest.TestCase):
class TestAvoidUnitConsistencyChecks(unittest.TestCase):
"""
Test cases for models with unit-laden model components.
"""

@unittest.skipUnless(
scip_available and scip_license_is_valid, "SCIP is not available and licensed."
)
def test_two_stg_mod_with_axis_aligned_set(self):
"""
Test two-stage model with `AxisAlignedEllipsoidalSet`
as the uncertainty set.
"""
@parameterized.expand([[True], [False]])
@unittest.skipUnless(ipopt_available, "IPOPT is not available.")
@unittest.skipUnless(pint_available, "Package 'pint' is not available")
def test_avoid_unit_consistency_checks(self, use_discrete):
m = ConcreteModel()
m.x1 = Var(initialize=0, bounds=(0, None))
m.x2 = Var(initialize=0, bounds=(0, None), units=u.m)
m.x3 = Var(initialize=0, bounds=(None, None))
m.u1 = Param(initialize=1.125, mutable=True, units=u.s)
m.u2 = Param(initialize=1, mutable=True, units=u.m**2)

m.con1 = Constraint(expr=m.x1 * m.u1 ** (0.5) - m.x2 * m.u1 <= 2)
m.con2 = Constraint(expr=m.x1**2 - m.x2**2 * m.u1 == m.x3)
m.q = Param(initialize=0, mutable=True, units=units.s)
m.x = Var(bounds=[-2, 2], units=units.m)
m.z = Var(bounds=[-2, 2])
m.y = Var(units=units.m**2)
# notice: units/dimensions in the objective and constraint
# expressions are inconsistent
m.eq = Constraint(expr=m.y == m.x + m.z + m.q)
m.con = Constraint(expr=m.x >= m.q)
m.obj = Objective(expr=m.x + m.z)

m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - m.u2) ** 2)

# Define the uncertainty set
# we take the parameter `u2` to be 'fixed'
ellipsoid = AxisAlignedEllipsoidalSet(center=[1.125, 1], half_lengths=[1, 0])

# Instantiate the PyROS solver
ipopt = SolverFactory("ipopt")
pyros_solver = SolverFactory("pyros")

# Define subsolvers utilized in the algorithm
local_subsolver = SolverFactory("scip")
global_subsolver = SolverFactory("scip")

# Call the PyROS solver
# note: second-stage variable and uncertain params have units
# separate tests for discrete and non-discrete
# to ensure discrete separation is also covered
uncertainty_set = (
DiscreteScenarioSet([[0], [1]]) if use_discrete else BoxSet([[0, 1]])
)
results = pyros_solver.solve(
model=m,
first_stage_variables=[m.x1],
second_stage_variables=[m.x2],
uncertain_params=[m.u1, m.u2],
uncertainty_set=ellipsoid,
local_solver=local_subsolver,
global_solver=global_subsolver,
first_stage_variables=[m.x],
second_stage_variables=[m.z],
uncertain_params=[m.q],
uncertainty_set=uncertainty_set,
# choose nonstatic DR to cover DR polishing problem
decision_rule_order=1,
local_solver=ipopt,
global_solver=ipopt,
options={
"objective_focus": ObjectiveType.worst_case,
"solve_master_globally": True,
},
)

# check successful termination
# and that more than one iteration required
self.assertEqual(
results.pyros_termination_condition,
pyrosTerminationCondition.robust_optimal,
msg="Did not identify robust optimal solution to problem instance.",
)
self.assertGreater(
results.iterations,
1,
msg=(
"PyROS requires no more than one iteration to solve the model."
" Hence master feasibility problem construction not tested."
" Consider implementing a more challenging model for this"
" test case."
),
)
self.assertEqual(results.iterations, 2)
self.assertAlmostEqual(results.final_objective_value, -1)
self.assertAlmostEqual(m.x.value, 1)
self.assertAlmostEqual(m.z.value, -2)
# note: worst-case realization is q=0, so expect
# y = 1 + (-2) + 0 = -1
# due to the equality constraint
self.assertAlmostEqual(m.y.value, -1)


class TestSubsolverTiming(unittest.TestCase):
Expand Down
Loading