From 97d7509b0aab1cb43380c5ad2608031d7d4bffb2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 25 Sep 2023 12:00:06 -0600 Subject: [PATCH] Capture output from HiGHS when initializing the model (fixes #3003) --- pyomo/contrib/appsi/solvers/highs.py | 28 ++++++++---- .../solvers/tests/test_highs_persistent.py | 45 +++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 36b75f13763..3d498f9388e 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -345,15 +345,25 @@ def set_instance(self, model): f'Solver {c.__module__}.{c.__qualname__} is not available ' f'({self.available()}).' ) - self._reinit() - self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() - - self._solver_model = highspy.Highs() - self.add_block(model) - if self._objective is None: - self.set_objective(None) + + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.stream_solver: + ostreams.append(sys.stdout) + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=True): + self._reinit() + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + + self._solver_model = highspy.Highs() + self.add_block(model) + if self._objective is None: + self.set_objective(None) def _add_constraints(self, cons: List[_GeneralConstraintData]): self._sol = None diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index de348507195..6451db18087 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -1,5 +1,11 @@ +import subprocess +import sys + import pyomo.common.unittest as unittest import pyomo.environ as pe + +from pyomo.common.log import LoggingIntercept +from pyomo.common.tee import capture_output from pyomo.contrib.appsi.solvers.highs import Highs from pyomo.contrib.appsi.base import TerminationCondition @@ -62,3 +68,42 @@ def test_mutable_params_with_remove_vars(self): m.p2.value = 9 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, -9) + + def test_capture_highs_output(self): + # tests issue #3003 + # + # Note that the "Running HiGHS" message is only emitted the + # first time that a model is instantiated. We need to test this + # in a subprocess to trigger that output. + model = [ + 'import pyomo.environ as pe', + 'm = pe.ConcreteModel()', + 'm.x = pe.Var(domain=pe.NonNegativeReals)', + 'm.y = pe.Var(domain=pe.NonNegativeReals)', + 'm.obj = pe.Objective(expr=m.x + m.y, sense=pe.maximize)', + 'm.c1 = pe.Constraint(expr=m.x <= 10)', + 'm.c2 = pe.Constraint(expr=m.y <= 5)', + 'from pyomo.contrib.appsi.solvers.highs import Highs', + 'result = Highs().solve(m)', + 'print(m.x.value, m.y.value)', + ] + + with LoggingIntercept() as LOG, capture_output(capture_fd=True) as OUT: + subprocess.run([sys.executable, '-c', ';'.join(model)]) + self.assertEqual(LOG.getvalue(), "") + self.assertEqual(OUT.getvalue(), "10.0 5.0\n") + + model[-2:-1] = [ + 'opt = Highs()', + 'opt.config.stream_solver = True', + 'result = opt.solve(m)', + ] + with LoggingIntercept() as LOG, capture_output(capture_fd=True) as OUT: + subprocess.run([sys.executable, '-c', ';'.join(model)]) + self.assertEqual(LOG.getvalue(), "") + # This is emitted by the model set-up + self.assertIn("Running HiGHS", OUT.getvalue()) + # This is emitted by the solve() + self.assertIn("HiGHS run time", OUT.getvalue()) + ref = "10.0 5.0\n" + self.assertEqual(ref, OUT.getvalue()[-len(ref) :])