Skip to content
16 changes: 9 additions & 7 deletions pyomo/contrib/solver/solvers/gurobi_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from pyomo.common.config import ConfigValue
from pyomo.common.dependencies import attempt_import
from pyomo.common.enums import ObjectiveSense
from pyomo.common.errors import MouseTrap, ApplicationError
from pyomo.common.errors import ApplicationError
from pyomo.common.shutdown import python_is_shutting_down
from pyomo.common.tee import capture_output, TeeStream
from pyomo.common.timing import HierarchicalTimer
Expand Down Expand Up @@ -53,13 +53,13 @@ class GurobiConfigMixin:
"""

def __init__(self):
self.use_mipstart: bool = self.declare(
'use_mipstart',
self.warm_start: bool = self.declare(
'warm_start',
ConfigValue(
default=False,
domain=bool,
description="If True, the current values of the integer variables "
"will be passed to Gurobi.",
description="If True, the current values of the variables "
"will be passed to Gurobi as a warm start.",
),
)

Expand Down Expand Up @@ -360,8 +360,10 @@ def solve(self, model, **kwds) -> Results:
if config.abs_gap is not None:
gurobi_model.setParam('MIPGapAbs', config.abs_gap)

if config.use_mipstart:
raise MouseTrap("MIPSTART not yet supported")
if config.warm_start:
for pyo_var, grb_var in zip(repn.columns, x.tolist()):
if pyo_var.value is not None:
grb_var.setAttr('Start', pyo_var.value)

for key, option in options.items():
gurobi_model.setParam(key, option)
Expand Down
57 changes: 33 additions & 24 deletions pyomo/contrib/solver/solvers/gurobi_direct_minlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import datetime
import io
from operator import attrgetter, itemgetter
import os

from pyomo.common.dependencies import attempt_import
from pyomo.common.collections import ComponentMap, ComponentSet
from pyomo.common.config import ConfigDict, ConfigValue
from pyomo.common.errors import InvalidValueError
from pyomo.common.numeric_types import native_complex_types
from pyomo.common.tee import capture_output, TeeStream
from pyomo.common.timing import HierarchicalTimer

from pyomo.contrib.solver.common.factory import SolverFactory
Expand Down Expand Up @@ -605,38 +607,45 @@ def solve(self, model, **kwds):

StaleFlagManager.mark_all_as_stale()

timer.start('compile_model')

writer = GurobiMINLPWriter()
grb_model, var_map, pyo_obj, grb_cons, pyo_cons = writer.write(
model, symbolic_solver_labels=config.symbolic_solver_labels
)

timer.stop('compile_model')

ostreams = [io.StringIO()] + config.tee

# set options
options = config.solver_options
orig_cwd = os.getcwd()
try:
if config.working_dir:
os.chdir(config.working_dir)
with capture_output(TeeStream(*ostreams), capture_fd=False):
timer.start('compile_model')
grb_model, var_map, pyo_obj, grb_cons, pyo_cons = writer.write(
model, symbolic_solver_labels=config.symbolic_solver_labels
)
timer.stop('compile_model')

# set options
options = config.solver_options

grb_model.setParam('LogToConsole', 1)
grb_model.setParam('LogToConsole', 1)

if config.threads is not None:
grb_model.setParam('Threads', config.threads)
if config.time_limit is not None:
grb_model.setParam('TimeLimit', config.time_limit)
if config.rel_gap is not None:
grb_model.setParam('MIPGap', config.rel_gap)
if config.abs_gap is not None:
grb_model.setParam('MIPGapAbs', config.abs_gap)
if config.threads is not None:
grb_model.setParam('Threads', config.threads)
if config.time_limit is not None:
grb_model.setParam('TimeLimit', config.time_limit)
if config.rel_gap is not None:
grb_model.setParam('MIPGap', config.rel_gap)
if config.abs_gap is not None:
grb_model.setParam('MIPGapAbs', config.abs_gap)

if config.use_mipstart:
raise MouseTrap("MIPSTART not yet supported")
if config.warm_start:
for pyo_var, grb_var in var_map.items():
if pyo_var.value is not None:
grb_var.setAttr('Start', pyo_var.value)

for key, option in options.items():
grb_model.setParam(key, option)
for key, option in options.items():
grb_model.setParam(key, option)

grbsol = grb_model.optimize()
grbsol = grb_model.optimize()
finally:
os.chdir(orig_cwd)

res = self._postsolve(
timer,
Expand Down
12 changes: 10 additions & 2 deletions pyomo/contrib/solver/solvers/gurobi_persistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,14 +303,22 @@ def _solve(self):
if config.abs_gap is not None:
self._solver_model.setParam('MIPGapAbs', config.abs_gap)

if config.use_mipstart:
if config.warm_start:
for (
pyomo_var_id,
gurobi_var,
) in self._pyomo_var_to_solver_var_map.items():
pyomo_var = self._vars[pyomo_var_id][0]
if pyomo_var.is_integer() and pyomo_var.value is not None:
if pyomo_var.value is not None:
self.set_var_attr(pyomo_var, 'Start', pyomo_var.value)
# elif config.warm_start_integer_vars:
# for (
# pyomo_var_id,
# gurobi_var,
# ) in self._pyomo_var_to_solver_var_map.items():
# pyomo_var = self._vars[pyomo_var_id][0]
# if pyomo_var.is_integer() and pyomo_var.value is not None:
# self.set_var_attr(pyomo_var, 'Start', pyomo_var.value)
Comment on lines +314 to +321
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we could do this in all the interfaces if we want a separate option for just warm starting integer vars, but dunno if we need to add the complexity?


for key, option in options.items():
self._solver_model.setParam(key, option)
Expand Down
113 changes: 113 additions & 0 deletions pyomo/contrib/solver/tests/solvers/test_gurobi_warm_start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2025
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

import logging
from pyomo.common.log import LoggingIntercept
import pyomo.common.unittest as unittest
from pyomo.contrib.solver.common.factory import SolverFactory

from pyomo.environ import (
ConcreteModel,
Var,
Constraint,
value,
Binary,
NonNegativeReals,
Objective,
Set,
)

gurobi_direct = SolverFactory('gurobi_direct')
gurobi_direct_minlp = SolverFactory('gurobi_direct_minlp')
gurobi_persistent = SolverFactory('gurobi_persistent')


class TestGurobiWarmStart(unittest.TestCase):
def make_model(self):
m = ConcreteModel()
m.S = Set(initialize=[1, 2, 3, 4, 5])
m.y = Var(m.S, domain=Binary)
m.x = Var(m.S, domain=NonNegativeReals)
m.obj = Objective(expr=sum(m.x[i] for i in m.S))

@m.Constraint(m.S)
def cons(m, i):
if i % 2 == 0:
return m.x[i] + i * m.y[i] >= 3 * i
else:
return m.x[i] - i * m.y[i] >= 3 * i

# define a suboptimal MIP start
for i in m.S:
m.y[i] = 1
# objective will be 4 + 4 + 12 + 8 + 20 = 48

return m

def check_optimal_soln(self, m):
# check that we got the optimal solution:
# y[1] = 0, x[1] = 3
# y[2] = 1, x[2] = 4
# y[3] = 0, x[3] = 9
# y[4] = 1, x[4] = 8
# y[5] = 0, x[5] = 15
x = {1: 3, 2: 4, 3: 9, 4: 8, 5: 15}
self.assertEqual(value(m.obj), 39)
for i in m.S:
if i % 2 == 0:
self.assertEqual(value(m.y[i]), 1)
else:
self.assertEqual(value(m.y[i]), 0)
self.assertEqual(value(m.x[i]), x[i])

@unittest.skipUnless(gurobi_direct.available(), "needs Gurobi Direct interface")
def test_gurobi_direct_warm_start(self):
m = self.make_model()

gurobi_direct.config.warm_start = True
logger = logging.getLogger('tee')
with LoggingIntercept(module='tee', level=logging.INFO) as LOG:
gurobi_direct.solve(m, tee=logger)
self.assertIn(
"User MIP start produced solution with objective 48", LOG.getvalue()
)
self.check_optimal_soln(m)

@unittest.skipUnless(
gurobi_direct_minlp.available(), "needs Gurobi Direct MINLP interface"
)
def test_gurobi_minlp_warmstart(self):
m = self.make_model()

gurobi_direct_minlp.config.warm_start = True
logger = logging.getLogger('tee')
with LoggingIntercept(module='tee', level=logging.INFO) as LOG:
gurobi_direct_minlp.solve(m, tee=logger)
self.assertIn(
"User MIP start produced solution with objective 48", LOG.getvalue()
)
self.check_optimal_soln(m)

@unittest.skipUnless(
gurobi_persistent.available(), "needs Gurobi persistent interface"
)
def test_gurobi_persistent_warmstart(self):
m = self.make_model()

gurobi_persistent.config.warm_start = True
gurobi_persistent.set_instance(m)
logger = logging.getLogger('tee')
with LoggingIntercept(module='tee', level=logging.INFO) as LOG:
gurobi_persistent.solve(m, tee=logger)
self.assertIn(
"User MIP start produced solution with objective 48", LOG.getvalue()
)
self.check_optimal_soln(m)
Loading