Skip to content

Commit cb52a02

Browse files
authored
Merge pull request #3709 from mrmundt/comp-test-suite
Add test to ensure `Results` object is populated fully
2 parents e173cb5 + 2f561a3 commit cb52a02

File tree

2 files changed

+91
-16
lines changed

2 files changed

+91
-16
lines changed

pyomo/contrib/solver/solvers/highs.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def _solve(self):
306306
self._solver_model.run()
307307
timer.stop('optimize')
308308

309-
return self._postsolve()
309+
return self._postsolve(ostreams[0])
310310

311311
def _process_domain_and_bounds(self, var_id):
312312
_v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[var_id]
@@ -664,7 +664,7 @@ def _set_objective(self, obj):
664664
)
665665
self._mutable_objective.update()
666666

667-
def _postsolve(self):
667+
def _postsolve(self, stream: io.StringIO):
668668
config = self._active_config
669669
timer = config.timer
670670
timer.start('load solution')
@@ -674,6 +674,10 @@ def _postsolve(self):
674674

675675
results = Results()
676676
results.solution_loader = PersistentSolutionLoader(self)
677+
results.solver_name = self.name
678+
results.solver_version = self.version()
679+
results.solver_config = config
680+
results.solver_log = stream.getvalue()
677681
results.timing_info.highs_time = highs.getRunTime()
678682

679683
self._sol = highs.getSolution()
@@ -746,6 +750,7 @@ def _postsolve(self):
746750
results.objective_bound = None
747751
else:
748752
results.objective_bound = info.mip_dual_bound
753+
results.iteration_count = info.simplex_iteration_count
749754

750755
if config.load_solutions:
751756
if has_feasible_solution:

pyomo/contrib/solver/tests/solvers/test_solvers.py

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# This software is distributed under the 3-clause BSD License.
1010
# ___________________________________________________________________________
1111

12+
import datetime
1213
import random
1314
import math
1415
from typing import Type
@@ -17,6 +18,14 @@
1718
from pyomo import gdp
1819
from pyomo.common.dependencies import attempt_import
1920
import pyomo.common.unittest as unittest
21+
22+
from pyomo.contrib.solver.common.base import SolverBase
23+
from pyomo.contrib.solver.common.config import SolverConfig
24+
from pyomo.contrib.solver.common.factory import SolverFactory
25+
from pyomo.contrib.solver.solvers.gurobi_persistent import GurobiPersistent
26+
from pyomo.contrib.solver.solvers.gurobi_direct import GurobiDirect
27+
from pyomo.contrib.solver.solvers.highs import Highs
28+
from pyomo.contrib.solver.solvers.ipopt import Ipopt
2029
from pyomo.contrib.solver.common.results import (
2130
TerminationCondition,
2231
SolutionStatus,
@@ -27,12 +36,6 @@
2736
NoSolutionError,
2837
NoReducedCostsError,
2938
)
30-
from pyomo.contrib.solver.common.base import SolverBase
31-
from pyomo.contrib.solver.common.factory import SolverFactory
32-
from pyomo.contrib.solver.solvers.ipopt import Ipopt
33-
from pyomo.contrib.solver.solvers.gurobi_persistent import GurobiPersistent
34-
from pyomo.contrib.solver.solvers.gurobi_direct import GurobiDirect
35-
from pyomo.contrib.solver.solvers.highs import Highs
3639
from pyomo.core.expr.numeric_expr import LinearExpression
3740
from pyomo.core.expr.compare import assertExpressionsEqual
3841

@@ -99,7 +102,7 @@ def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bo
99102
if any(name.startswith(i) for i in nl_solvers_set):
100103
if use_presolve:
101104
raise unittest.SkipTest(
102-
f'cannot yet get duals if NLWriter presolve is on'
105+
'cannot yet get duals if NLWriter presolve is on'
103106
)
104107
else:
105108
opt.config.writer_config.linear_presolve = False
@@ -153,7 +156,7 @@ def test_inequality(
153156
if any(name.startswith(i) for i in nl_solvers_set):
154157
if use_presolve:
155158
raise unittest.SkipTest(
156-
f'cannot yet get duals if NLWriter presolve is on'
159+
'cannot yet get duals if NLWriter presolve is on'
157160
)
158161
else:
159162
opt.config.writer_config.linear_presolve = False
@@ -213,7 +216,7 @@ def test_bounds(self, name: str, opt_class: Type[SolverBase], use_presolve: bool
213216
if any(name.startswith(i) for i in nl_solvers_set):
214217
if use_presolve:
215218
raise unittest.SkipTest(
216-
f'cannot yet get duals if NLWriter presolve is on'
219+
'cannot yet get duals if NLWriter presolve is on'
217220
)
218221
else:
219222
opt.config.writer_config.linear_presolve = False
@@ -268,7 +271,7 @@ def test_range(self, name: str, opt_class: Type[SolverBase], use_presolve: bool)
268271
if any(name.startswith(i) for i in nl_solvers_set):
269272
if use_presolve:
270273
raise unittest.SkipTest(
271-
f'cannot yet get duals if NLWriter presolve is on'
274+
'cannot yet get duals if NLWriter presolve is on'
272275
)
273276
else:
274277
opt.config.writer_config.linear_presolve = False
@@ -322,7 +325,7 @@ def test_equality_max(
322325
if any(name.startswith(i) for i in nl_solvers_set):
323326
if use_presolve:
324327
raise unittest.SkipTest(
325-
f'cannot yet get duals if NLWriter presolve is on'
328+
'cannot yet get duals if NLWriter presolve is on'
326329
)
327330
else:
328331
opt.config.writer_config.linear_presolve = False
@@ -376,7 +379,7 @@ def test_inequality_max(
376379
if any(name.startswith(i) for i in nl_solvers_set):
377380
if use_presolve:
378381
raise unittest.SkipTest(
379-
f'cannot yet get duals if NLWriter presolve is on'
382+
'cannot yet get duals if NLWriter presolve is on'
380383
)
381384
else:
382385
opt.config.writer_config.linear_presolve = False
@@ -438,7 +441,7 @@ def test_bounds_max(
438441
if any(name.startswith(i) for i in nl_solvers_set):
439442
if use_presolve:
440443
raise unittest.SkipTest(
441-
f'cannot yet get duals if NLWriter presolve is on'
444+
'cannot yet get duals if NLWriter presolve is on'
442445
)
443446
else:
444447
opt.config.writer_config.linear_presolve = False
@@ -495,7 +498,7 @@ def test_range_max(
495498
if any(name.startswith(i) for i in nl_solvers_set):
496499
if use_presolve:
497500
raise unittest.SkipTest(
498-
f'cannot yet get duals if NLWriter presolve is on'
501+
'cannot yet get duals if NLWriter presolve is on'
499502
)
500503
else:
501504
opt.config.writer_config.linear_presolve = False
@@ -544,6 +547,73 @@ class TestSolvers(unittest.TestCase):
544547
def test_config_overwrite(self, name: str, opt_class: Type[SolverBase]):
545548
self.assertIsNot(SolverBase.CONFIG, opt_class.CONFIG)
546549

550+
@parameterized.expand(input=_load_tests(all_solvers))
551+
def test_results_object_populated(
552+
self, name: str, opt_class: Type[SolverBase], use_presolve: bool
553+
):
554+
opt: SolverBase = opt_class()
555+
if not opt.available():
556+
raise unittest.SkipTest(f'Solver {opt.name} not available.')
557+
if any(name.startswith(i) for i in nl_solvers_set):
558+
if use_presolve:
559+
opt.config.writer_config.linear_presolve = True
560+
else:
561+
opt.config.writer_config.linear_presolve = False
562+
m = pyo.ConcreteModel()
563+
m.x = pyo.Var(bounds=(2, None))
564+
m.obj = pyo.Objective(expr=m.x)
565+
res = opt.solve(m, load_solutions=False)
566+
pyo.assert_optimal_termination(res)
567+
568+
# Initial gut check - is it the right type?
569+
self.assertIsInstance(res, Results)
570+
571+
# termination_condition is set to a valid enum and not unknown
572+
self.assertIsInstance(res.termination_condition, TerminationCondition)
573+
self.assertNotEqual(res.termination_condition, TerminationCondition.unknown)
574+
575+
# solution_status is a valid enum and indicates a usable solution
576+
self.assertIsInstance(res.solution_status, SolutionStatus)
577+
self.assertIn(
578+
res.solution_status, {SolutionStatus.feasible, SolutionStatus.optimal}
579+
)
580+
581+
# solver_name is a nonempty string
582+
self.assertIsInstance(res.solver_name, str)
583+
self.assertTrue(res.solver_name.strip())
584+
585+
# solver_version is a tuple of ints
586+
self.assertIsInstance(res.solver_version, tuple)
587+
for v in res.solver_version:
588+
self.assertIsInstance(v, int)
589+
590+
# iteration_count is nonnegative
591+
self.assertGreaterEqual(res.iteration_count, 0)
592+
593+
# timing_info should exist
594+
self.assertIsNotNone(res.timing_info)
595+
596+
# start_timestamp must be a valid datetime
597+
self.assertIsInstance(res.timing_info.start_timestamp, datetime.datetime)
598+
599+
# wall_time must be a float (=> 0)
600+
self.assertIsInstance(res.timing_info.wall_time, float)
601+
self.assertGreaterEqual(res.timing_info.wall_time, 0.0)
602+
603+
# incumbent_objective should be populated for a feasible/optimal solve
604+
self.assertIsNotNone(res.incumbent_objective)
605+
606+
# Should have a solution loader available
607+
self.assertTrue(hasattr(res, "solution_loader"))
608+
609+
# Should have a copy of the config used
610+
self.assertIsInstance(res.solver_config, SolverConfig)
611+
612+
# All solvers should be implementing some sort of TeeStream,
613+
# so they should be able to capture anything logged to the console
614+
self.assertIsNotNone(res.solver_log)
615+
self.assertIsInstance(res.solver_log, str)
616+
547617
@parameterized.expand(input=_load_tests(all_solvers))
548618
def test_remove_variable_and_objective(
549619
self, name: str, opt_class: Type[SolverBase], use_presolve

0 commit comments

Comments
 (0)