Skip to content
Open
Show file tree
Hide file tree
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
9 changes: 7 additions & 2 deletions pyomo/contrib/solver/solvers/highs.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def _solve(self):
self._solver_model.run()
timer.stop('optimize')

return self._postsolve()
return self._postsolve(ostreams[0])

def _process_domain_and_bounds(self, var_id):
_v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[var_id]
Expand Down Expand Up @@ -664,7 +664,7 @@ def _set_objective(self, obj):
)
self._mutable_objective.update()

def _postsolve(self):
def _postsolve(self, stream: io.StringIO):
config = self._active_config
timer = config.timer
timer.start('load solution')
Expand All @@ -674,6 +674,10 @@ def _postsolve(self):

results = Results()
results.solution_loader = PersistentSolutionLoader(self)
results.solver_name = self.name
results.solver_version = self.version()
results.solver_config = config
results.solver_log = stream.getvalue()
results.timing_info.highs_time = highs.getRunTime()

self._sol = highs.getSolution()
Expand Down Expand Up @@ -746,6 +750,7 @@ def _postsolve(self):
results.objective_bound = None
else:
results.objective_bound = info.mip_dual_bound
results.iteration_count = info.simplex_iteration_count

if config.load_solutions:
if has_feasible_solution:
Expand Down
98 changes: 84 additions & 14 deletions pyomo/contrib/solver/tests/solvers/test_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

import datetime
import random
import math
from typing import Type
Expand All @@ -17,6 +18,14 @@
from pyomo import gdp
from pyomo.common.dependencies import attempt_import
import pyomo.common.unittest as unittest

from pyomo.contrib.solver.common.base import SolverBase
from pyomo.contrib.solver.common.config import SolverConfig
from pyomo.contrib.solver.common.factory import SolverFactory
from pyomo.contrib.solver.solvers.gurobi_persistent import GurobiPersistent
from pyomo.contrib.solver.solvers.gurobi_direct import GurobiDirect
from pyomo.contrib.solver.solvers.highs import Highs
from pyomo.contrib.solver.solvers.ipopt import Ipopt
from pyomo.contrib.solver.common.results import (
TerminationCondition,
SolutionStatus,
Expand All @@ -27,12 +36,6 @@
NoSolutionError,
NoReducedCostsError,
)
from pyomo.contrib.solver.common.base import SolverBase
from pyomo.contrib.solver.common.factory import SolverFactory
from pyomo.contrib.solver.solvers.ipopt import Ipopt
from pyomo.contrib.solver.solvers.gurobi_persistent import GurobiPersistent
from pyomo.contrib.solver.solvers.gurobi_direct import GurobiDirect
from pyomo.contrib.solver.solvers.highs import Highs
from pyomo.core.expr.numeric_expr import LinearExpression
from pyomo.core.expr.compare import assertExpressionsEqual

Expand Down Expand Up @@ -99,7 +102,7 @@ def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bo
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -153,7 +156,7 @@ def test_inequality(
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -213,7 +216,7 @@ def test_bounds(self, name: str, opt_class: Type[SolverBase], use_presolve: bool
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -268,7 +271,7 @@ def test_range(self, name: str, opt_class: Type[SolverBase], use_presolve: bool)
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -322,7 +325,7 @@ def test_equality_max(
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -376,7 +379,7 @@ def test_inequality_max(
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -438,7 +441,7 @@ def test_bounds_max(
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -495,7 +498,7 @@ def test_range_max(
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
raise unittest.SkipTest(
f'cannot yet get duals if NLWriter presolve is on'
'cannot yet get duals if NLWriter presolve is on'
)
else:
opt.config.writer_config.linear_presolve = False
Expand Down Expand Up @@ -544,6 +547,73 @@ class TestSolvers(unittest.TestCase):
def test_config_overwrite(self, name: str, opt_class: Type[SolverBase]):
self.assertIsNot(SolverBase.CONFIG, opt_class.CONFIG)

@parameterized.expand(input=_load_tests(all_solvers))
def test_results_object_populated(
self, name: str, opt_class: Type[SolverBase], use_presolve: bool
):
opt: SolverBase = opt_class()
if not opt.available():
raise unittest.SkipTest(f'Solver {opt.name} not available.')
if any(name.startswith(i) for i in nl_solvers_set):
if use_presolve:
opt.config.writer_config.linear_presolve = True
else:
opt.config.writer_config.linear_presolve = False
m = pyo.ConcreteModel()
m.x = pyo.Var(bounds=(2, None))
m.obj = pyo.Objective(expr=m.x)
res = opt.solve(m, load_solutions=False)
pyo.assert_optimal_termination(res)

# Initial gut check - is it the right type?
self.assertIsInstance(res, Results)

# termination_condition is set to a valid enum and not unknown
self.assertIsInstance(res.termination_condition, TerminationCondition)
self.assertNotEqual(res.termination_condition, TerminationCondition.unknown)

# solution_status is a valid enum and indicates a usable solution
self.assertIsInstance(res.solution_status, SolutionStatus)
self.assertIn(
res.solution_status, {SolutionStatus.feasible, SolutionStatus.optimal}
)

# solver_name is a nonempty string
self.assertIsInstance(res.solver_name, str)
self.assertTrue(res.solver_name.strip())

# solver_version is a tuple of ints
self.assertIsInstance(res.solver_version, tuple)
for v in res.solver_version:
self.assertIsInstance(v, int)

# iteration_count is nonnegative
self.assertGreaterEqual(res.iteration_count, 0)

# timing_info should exist
self.assertIsNotNone(res.timing_info)

# start_timestamp must be a valid datetime
self.assertIsInstance(res.timing_info.start_timestamp, datetime.datetime)

# wall_time must be a float (=> 0)
self.assertIsInstance(res.timing_info.wall_time, float)
self.assertGreaterEqual(res.timing_info.wall_time, 0.0)

# incumbent_objective should be populated for a feasible/optimal solve
self.assertIsNotNone(res.incumbent_objective)

# Should have a solution loader available
self.assertTrue(hasattr(res, "solution_loader"))

# Should have a copy of the config used
self.assertIsInstance(res.solver_config, SolverConfig)

# All solvers should be implementing some sort of TeeStream,
# so they should be able to capture anything logged to the console
self.assertIsNotNone(res.solver_log)
self.assertIsInstance(res.solver_log, str)

@parameterized.expand(input=_load_tests(all_solvers))
def test_remove_variable_and_objective(
self, name: str, opt_class: Type[SolverBase], use_presolve
Expand Down
Loading