diff --git a/pyomo/contrib/solver/solvers/highs.py b/pyomo/contrib/solver/solvers/highs.py index fb83117bb81..d1bcbae8ab6 100644 --- a/pyomo/contrib/solver/solvers/highs.py +++ b/pyomo/contrib/solver/solvers/highs.py @@ -750,7 +750,30 @@ def _postsolve(self, stream: io.StringIO): results.objective_bound = None else: results.objective_bound = info.mip_dual_bound - results.iteration_count = info.simplex_iteration_count + + if info.valid: + # The method that ran will have a non-negative iteration count + # and the others will be 0 or -1. + counts = [ + info.simplex_iteration_count, + info.ipm_iteration_count, + info.mip_node_count, + info.pdlp_iteration_count, + info.qp_iteration_count, + ] + positive_iters = [c for c in counts if c > 0] + if not positive_iters: + assert any( + (c == 0 for c in counts) + ), "At least one iteration count should have a non-negative value" + results.iteration_count = 0 + else: + assert ( + len(positive_iters) == 1 + ), "Only one iteration count should have a positive value" + results.iteration_count = positive_iters[0] + else: + results.iteration_count = 0 if config.load_solutions: if has_feasible_solution: diff --git a/pyomo/contrib/solver/tests/solvers/test_highs.py b/pyomo/contrib/solver/tests/solvers/test_highs.py index f59a0bfa42d..7deb4a3f310 100644 --- a/pyomo/contrib/solver/tests/solvers/test_highs.py +++ b/pyomo/contrib/solver/tests/solvers/test_highs.py @@ -13,6 +13,7 @@ import pyomo.environ as pyo from pyomo.contrib.solver.solvers.highs import Highs +from pyomo.contrib.solver.common.results import SolutionStatus opt = Highs() if not opt.available(): @@ -109,3 +110,62 @@ def test_fix_and_unfix(self): self.assertAlmostEqual(m.fx.value, 1, places=5) self.assertAlmostEqual(m.fy.value, 0, places=5) self.assertAlmostEqual(r.objective_bound, 0.5, places=5) + + +class TestHighsMiniDemos(unittest.TestCase): + def test_lp_methods(self): + for method in ("simplex", "ipm", "pdlp"): + # Build LP + m = pyo.ConcreteModel() + m.x = pyo.Var(domain=pyo.NonNegativeReals) + m.y = pyo.Var(domain=pyo.NonNegativeReals) + m.c = pyo.Constraint(expr=m.x + m.y >= 1) + m.obj = pyo.Objective(expr=m.x + m.y, sense=pyo.minimize) + + solver = Highs() + solver.config.solver_options["solver"] = ( + method # 'simplex' | 'ipm' | 'pdlp' + ) + solver.config.solver_options["presolve"] = "off" + results = solver.solve(m) + self.assertTrue(results.solution_status == SolutionStatus.optimal) + self.assertTrue(results.iteration_count > 0) + + def test_mip_demo(self): + # Build MIP + m = pyo.ConcreteModel() + m.a = pyo.Var(domain=pyo.Binary) + m.b = pyo.Var(domain=pyo.Binary) + m.cap = pyo.Constraint(expr=m.a + m.b <= 1) + m.obj = pyo.Objective(expr=3 * m.a + 2 * m.b, sense=pyo.maximize) + + solver = Highs() + solver.config.solver_options["presolve"] = "off" + results = solver.solve(m) + self.assertTrue(results.solution_status == SolutionStatus.optimal) + self.assertTrue(results.iteration_count > 0) + + def test_mip_pmedian(self): + # Build MIP + from pyomo.core.tests.examples.pmedian_concrete import create_model + + M = create_model() + + solver = Highs() + results = solver.solve(M) + self.assertEqual(results.solution_status, SolutionStatus.optimal) + self.assertTrue(results.iteration_count > 0) + + def test_qp_demo(self): + # Build convex QP + m = pyo.ConcreteModel() + m.x = pyo.Var(domain=pyo.Reals, bounds=(0, None)) + m.y = pyo.Var(domain=pyo.Reals, bounds=(0, None)) + m.c = pyo.Constraint(expr=m.x + m.y >= 1) + m.obj = pyo.Objective(expr=(m.x - 1) ** 2 + (m.y - 2) ** 2, sense=pyo.minimize) + + solver = Highs() + solver.config.solver_options["presolve"] = "off" + results = solver.solve(m) + self.assertTrue(results.solution_status == SolutionStatus.optimal) + self.assertTrue(results.iteration_count > 0)