Skip to content

Commit 9234f32

Browse files
Merge branch 'main' into feature/3381_quadratic_obj_highs
2 parents 04eb847 + 0b58294 commit 9234f32

File tree

19 files changed

+937
-200
lines changed

19 files changed

+937
-200
lines changed

Diff for: doc/OnlineDocs/explanation/experimental/solvers.rst

+34-5
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ be used with other Pyomo tools / capabilities.
8585
:skipif: not ipopt_available
8686
:hide:
8787

88-
...
8988
2 Var Declarations
9089
...
9190
3 Declarations: x y obj
@@ -117,7 +116,6 @@ future methods of specifying solver options are supported:
117116
:skipif: not ipopt_available
118117
:hide:
119118

120-
...
121119
2 Var Declarations
122120
...
123121
3 Declarations: x y obj
@@ -154,7 +152,7 @@ Here we use the new interface by importing it directly:
154152
:skipif: not ipopt_available
155153
:hide:
156154

157-
solution_loader: ...
155+
termination_condition: ...
158156
...
159157
3 Declarations: x y obj
160158

@@ -190,7 +188,7 @@ Here we use the new interface by retrieving it from the new ``SolverFactory``:
190188
:skipif: not ipopt_available
191189
:hide:
192190

193-
solution_loader: ...
191+
termination_condition: ...
194192
...
195193
3 Declarations: x y obj
196194

@@ -227,7 +225,7 @@ replace the existing (legacy) SolverFactory and utilities with the new
227225
:skipif: not ipopt_available
228226
:hide:
229227

230-
solution_loader: ...
228+
termination_condition: ...
231229
...
232230
3 Declarations: x y obj
233231

@@ -313,6 +311,37 @@ which can be manipulated similar to a standard ``dict`` in Python.
313311
:members:
314312
:undoc-members:
315313

314+
The new interface has condensed :py:class:`~pyomo.opt.results.solver.SolverStatus`,
315+
:py:class:`~pyomo.opt.results.solver.TerminationCondition`,
316+
and :py:class:`~pyomo.opt.results.solution.SolutionStatus` into
317+
:py:class:`~pyomo.contrib.solver.common.results.TerminationCondition`
318+
and :py:class:`~pyomo.contrib.solver.common.results.SolutionStatus` to
319+
reduce complexity. As a result, several legacy
320+
:py:class:`~pyomo.opt.results.solver.SolutionStatus` values are
321+
no longer achievable. These are detailed in the table below.
322+
323+
.. list-table:: Mapping from unachievable :py:class:`~pyomo.opt.results.solver.SolutionStatus`
324+
to future statuses
325+
:header-rows: 1
326+
327+
* - Legacy :py:class:`~pyomo.opt.results.solver.SolutionStatus`
328+
- :py:class:`~pyomo.contrib.solver.common.results.TerminationCondition`
329+
- :py:class:`~pyomo.contrib.solver.common.results.SolutionStatus`
330+
* - other
331+
- unknown
332+
- noSolution
333+
* - unsure
334+
- unknown
335+
- noSolution
336+
* - locallyOptimal
337+
- convergenceCriteriaSatisfied
338+
- optimal
339+
* - globallyOptimal
340+
- convergenceCriteriaSatisfied
341+
- optimal
342+
* - bestSoFar
343+
- convergenceCriteriaSatisfied
344+
- feasible
316345

317346
Termination Conditions
318347
^^^^^^^^^^^^^^^^^^^^^^

Diff for: pyomo/common/tee.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,11 @@ def __init__(self, fd=1, output=None, synchronize=True):
120120

121121
def __enter__(self):
122122
if self.std:
123+
# We used to flush original_file here. We have removed that
124+
# because the std* streams are flushed by capture_output.
125+
# Flushing again here caused intermittent errors due to
126+
# closed file handles on OSX
123127
self.original_file = getattr(sys, self.std)
124-
# important: flush the current file buffer when redirecting
125-
self.original_file.flush()
126128
# Duplicate the original standard file descriptor(file
127129
# descriptor 1 or 2) to a different file descriptor number
128130
self.original_fd = os.dup(self.fd)

Diff for: pyomo/common/tests/test_tee.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,18 @@ def test_exit_on_del(self):
589589
remaining_attempts = 4
590590
while len(stack) and remaining_attempts:
591591
gc.collect()
592+
time.sleep(((4 - remaining_attempts) / 4.0) ** 2)
592593
remaining_attempts -= 1
593-
self.assertEqual(len(stack), 0)
594+
try:
595+
self.assertEqual(len(stack), 0)
596+
except:
597+
# We still want to unwind the context managers if the test fails:
598+
while stack:
599+
try:
600+
stack.pop().__exit__(None, None, None)
601+
except:
602+
pass
603+
raise
594604

595605
def test_deadlock(self):
596606
class MockStream(object):

Diff for: pyomo/contrib/pyros/CHANGELOG.txt

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ PyROS CHANGELOG
33
===============
44

55

6+
-------------------------------------------------------------------------------
7+
PyROS 1.3.7 06 Mar 2025
8+
-------------------------------------------------------------------------------
9+
- Modify reformulation of state-variable independent second-stage
10+
equality constraints for problems with discrete uncertainty sets
11+
12+
613
-------------------------------------------------------------------------------
714
PyROS 1.3.6 06 Mar 2025
815
-------------------------------------------------------------------------------

Diff for: pyomo/contrib/pyros/pyros.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
)
3434

3535

36-
__version__ = "1.3.6"
36+
__version__ = "1.3.7"
3737

3838

3939
default_pyros_solver_logger = setup_pyros_logger()

Diff for: pyomo/contrib/pyros/tests/test_grcs.py

+183
Original file line numberDiff line numberDiff line change
@@ -1998,6 +1998,189 @@ def test_pyros_certain_params_ipopt_degrees_of_freedom(self):
19981998
self.assertEqual(m.x.value, 1)
19991999

20002000

2001+
@unittest.skipUnless(baron_available, "BARON not available")
2002+
class TestReformulateSecondStageEqualitiesDiscrete(unittest.TestCase):
2003+
"""
2004+
Test behavior of PyROS solver when the uncertainty set is
2005+
discrete and there are second-stage
2006+
equality constraints that are state-variable independent,
2007+
and therefore, subject to reformulation.
2008+
"""
2009+
2010+
def build_single_stage_model(self):
2011+
m = ConcreteModel()
2012+
m.x = Var(range(3), bounds=[-2, 2], initialize=0)
2013+
m.q = Param(range(3), initialize=0, mutable=True)
2014+
m.c = Param(range(3), initialize={0: 1, 1: 0, 2: 1})
2015+
m.obj = Objective(expr=sum(m.x[i] * m.c[i] for i in m.x), sense=maximize)
2016+
# when uncertainty set is discrete, the
2017+
# preprocessor should write out this constraint for
2018+
# each scenario as a first-stage constraint
2019+
m.xq_con = Constraint(expr=sum(m.x[i] * m.q[i] for i in m.x) == 0)
2020+
return m
2021+
2022+
def build_two_stage_model(self):
2023+
m = ConcreteModel()
2024+
m.x = Var(bounds=[None, None], initialize=0)
2025+
m.z = Var(bounds=[-2, 2], initialize=0)
2026+
m.q = Param(initialize=2, mutable=True)
2027+
m.obj = Objective(expr=m.x + m.z, sense=maximize)
2028+
# when uncertainty set is discrete, the
2029+
# preprocessor should write out this constraint for
2030+
# each scenario as a first-stage constraint
2031+
m.xz_con = Constraint(expr=m.x + m.q * m.z == 0)
2032+
return m
2033+
2034+
def test_single_stage_discrete_set_fullrank(self):
2035+
m = self.build_single_stage_model()
2036+
uncertainty_set = DiscreteScenarioSet(
2037+
# reformulating second-stage equality for these scenarios
2038+
# should result in first-stage equalities finally being
2039+
# (full-column-rank matrix) @ (x) == 0
2040+
# so x=0 is sole robust feasible solution
2041+
scenarios=[
2042+
[0] * len(m.q),
2043+
[1] * len(m.q),
2044+
list(range(1, len(m.q) + 1)),
2045+
[(idx + 1) ** 2 for idx in m.q],
2046+
]
2047+
)
2048+
baron = SolverFactory("baron")
2049+
res = SolverFactory("pyros").solve(
2050+
model=m,
2051+
first_stage_variables=m.x,
2052+
second_stage_variables=[],
2053+
uncertain_params=m.q,
2054+
uncertainty_set=uncertainty_set,
2055+
local_solver=baron,
2056+
global_solver=baron,
2057+
solve_master_globally=True,
2058+
bypass_local_separation=True,
2059+
objective_focus="worst_case",
2060+
)
2061+
self.assertEqual(
2062+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2063+
)
2064+
self.assertEqual(res.iterations, 1)
2065+
self.assertAlmostEqual(res.final_objective_value, 0, places=4)
2066+
self.assertAlmostEqual(m.x[0].value, 0, places=4)
2067+
self.assertAlmostEqual(m.x[1].value, 0, places=4)
2068+
self.assertAlmostEqual(m.x[2].value, 0, places=4)
2069+
2070+
def test_single_stage_discrete_set_rank2(self):
2071+
m = self.build_single_stage_model()
2072+
uncertainty_set = DiscreteScenarioSet(
2073+
# reformulating second-stage equality for these scenarios
2074+
# should make the optimal solution unique
2075+
scenarios=[[0] * len(m.q), [1] * len(m.q), [(idx + 1) ** 2 for idx in m.q]]
2076+
)
2077+
baron = SolverFactory("baron")
2078+
res = SolverFactory("pyros").solve(
2079+
model=m,
2080+
first_stage_variables=m.x,
2081+
second_stage_variables=[],
2082+
uncertain_params=m.q,
2083+
uncertainty_set=uncertainty_set,
2084+
local_solver=baron,
2085+
global_solver=baron,
2086+
solve_master_globally=True,
2087+
bypass_local_separation=True,
2088+
objective_focus="worst_case",
2089+
)
2090+
self.assertEqual(
2091+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2092+
)
2093+
self.assertEqual(res.iterations, 1)
2094+
self.assertAlmostEqual(res.final_objective_value, 2, places=4)
2095+
# optimal solution is unique
2096+
self.assertAlmostEqual(m.x[0].value, 5 / 4, places=4)
2097+
self.assertAlmostEqual(m.x[1].value, -2, places=4)
2098+
self.assertAlmostEqual(m.x[2].value, 3 / 4, places=4)
2099+
2100+
def test_single_stage_discrete_set_rank1(self):
2101+
m = self.build_single_stage_model()
2102+
uncertainty_set = DiscreteScenarioSet(
2103+
scenarios=[[0] * len(m.q), [2] * len(m.q), [3] * len(m.q)]
2104+
)
2105+
baron = SolverFactory("baron")
2106+
res = SolverFactory("pyros").solve(
2107+
model=m,
2108+
first_stage_variables=m.x,
2109+
second_stage_variables=[],
2110+
uncertain_params=m.q,
2111+
uncertainty_set=uncertainty_set,
2112+
local_solver=baron,
2113+
global_solver=baron,
2114+
solve_master_globally=True,
2115+
bypass_local_separation=True,
2116+
objective_focus="worst_case",
2117+
)
2118+
self.assertEqual(
2119+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2120+
)
2121+
self.assertEqual(res.iterations, 1)
2122+
self.assertAlmostEqual(res.final_objective_value, 2, places=4)
2123+
# subject to these scenarios, the optimal solution is non-unique,
2124+
# but should satisfy this check
2125+
self.assertAlmostEqual(m.x[1].value, -2, places=4)
2126+
2127+
def test_two_stage_discrete_set_rank2_affine_dr(self):
2128+
m = self.build_two_stage_model()
2129+
uncertainty_set = DiscreteScenarioSet([[2], [3]])
2130+
baron = SolverFactory("baron")
2131+
res = SolverFactory("pyros").solve(
2132+
model=m,
2133+
first_stage_variables=m.x,
2134+
second_stage_variables=m.z,
2135+
uncertain_params=m.q,
2136+
uncertainty_set=uncertainty_set,
2137+
local_solver=baron,
2138+
global_solver=baron,
2139+
solve_master_globally=True,
2140+
bypass_local_separation=True,
2141+
decision_rule_order=1,
2142+
objective_focus="worst_case",
2143+
)
2144+
self.assertEqual(
2145+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2146+
)
2147+
self.assertEqual(res.iterations, 1)
2148+
self.assertAlmostEqual(res.final_objective_value, 0, places=4)
2149+
# note: this solution is suboptimal (in the context of nonstatic DRs),
2150+
# but follows from the current efficiency for DRs
2151+
# (i.e. in first iteration, static DRs required)
2152+
self.assertAlmostEqual(m.x.value, 0, places=4)
2153+
self.assertAlmostEqual(m.z.value, 0, places=4)
2154+
2155+
def test_two_stage_discrete_set_fullrank_affine_dr(self):
2156+
m = self.build_two_stage_model()
2157+
uncertainty_set = DiscreteScenarioSet([[2], [3], [4]])
2158+
baron = SolverFactory("baron")
2159+
res = SolverFactory("pyros").solve(
2160+
model=m,
2161+
first_stage_variables=m.x,
2162+
second_stage_variables=m.z,
2163+
uncertain_params=m.q,
2164+
uncertainty_set=uncertainty_set,
2165+
local_solver=baron,
2166+
global_solver=baron,
2167+
solve_master_globally=True,
2168+
bypass_local_separation=True,
2169+
decision_rule_order=1,
2170+
objective_focus="worst_case",
2171+
)
2172+
self.assertEqual(
2173+
res.pyros_termination_condition, pyrosTerminationCondition.robust_optimal
2174+
)
2175+
self.assertEqual(res.iterations, 1)
2176+
self.assertAlmostEqual(res.final_objective_value, 0, places=4)
2177+
# the second-stage equalities are a full rank linear system
2178+
# in x and the DR variables, with RHS 0, so all
2179+
# variables must be 0
2180+
self.assertAlmostEqual(m.x.value, 0, places=4)
2181+
self.assertAlmostEqual(m.z.value, 0, places=4)
2182+
2183+
20012184
@unittest.skipUnless(ipopt_available, "IPOPT not available.")
20022185
class TestPyROSVarsAsUncertainParams(unittest.TestCase):
20032186
"""

0 commit comments

Comments
 (0)