diff --git a/pyomo/contrib/solver/solvers/knitro/base.py b/pyomo/contrib/solver/solvers/knitro/base.py index 305144c23de..06b117fa308 100644 --- a/pyomo/contrib/solver/solvers/knitro/base.py +++ b/pyomo/contrib/solver/solvers/knitro/base.py @@ -119,6 +119,13 @@ def _restore_var_values(self) -> None: var.set_value(self._saved_var_values[id(var)]) StaleFlagManager.mark_all_as_stale() + def _warm_start(self) -> None: + variables = [] + for var in self._get_vars(): + if var.value is not None: + variables.append(var) + self._engine.set_initial_values(variables) + @abstractmethod def _presolve( self, model: BlockData, config: KnitroConfig, timer: HierarchicalTimer diff --git a/pyomo/contrib/solver/solvers/knitro/config.py b/pyomo/contrib/solver/solvers/knitro/config.py index b7fc4135a30..abd6a51e30c 100644 --- a/pyomo/contrib/solver/solvers/knitro/config.py +++ b/pyomo/contrib/solver/solvers/knitro/config.py @@ -28,6 +28,18 @@ def __init__( visibility=visibility, ) + self.knitro_warm_start: bool = self.declare( + "knitro_warm_start", + ConfigValue( + domain=Bool, + default=False, + doc=( + "If True, KNITRO solver will use the current values " + "of variables as starting points for the optimization." + ), + ), + ) + self.rebuild_model_on_remove_var: bool = self.declare( "rebuild_model_on_remove_var", ConfigValue( diff --git a/pyomo/contrib/solver/solvers/knitro/direct.py b/pyomo/contrib/solver/solvers/knitro/direct.py index 8df25d115ac..cc565368511 100644 --- a/pyomo/contrib/solver/solvers/knitro/direct.py +++ b/pyomo/contrib/solver/solvers/knitro/direct.py @@ -48,6 +48,11 @@ def _solve(self, config: KnitroConfig, timer: HierarchicalTimer) -> None: self._engine.set_options(**config.solver_options) timer.stop("load_options") + if config.knitro_warm_start: + timer.start("knitro_warm_start") + self._warm_start() + timer.stop("knitro_warm_start") + timer.start("solve") self._engine.solve() timer.stop("solve") diff --git a/pyomo/contrib/solver/solvers/knitro/engine.py b/pyomo/contrib/solver/solvers/knitro/engine.py index 83d8c19f134..8046c6dcea0 100644 --- a/pyomo/contrib/solver/solvers/knitro/engine.py +++ b/pyomo/contrib/solver/solvers/knitro/engine.py @@ -310,6 +310,11 @@ def get_idxs( idx_map = self.maps[item_type] return [idx_map[id(item)] for item in items] + def set_initial_values(self, variables: Iterable[VarData]) -> None: + values = [value(var) for var in variables] + idxs = self.get_idxs(VarData, variables) + self.execute(knitro.KN_set_var_primal_init_values, idxs, values) + def get_values( self, item_type: type[ItemType], diff --git a/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py b/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py index 8bbb6e4d4ab..b18042e039a 100644 --- a/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py +++ b/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py @@ -36,13 +36,16 @@ def test_default_instantiation(self): self.assertIsNone(config.timer) self.assertIsNone(config.threads) self.assertIsNone(config.time_limit) + self.assertFalse(config.knitro_warm_start) def test_custom_instantiation(self): config = KnitroConfig(description="A description") config.tee = True + config.knitro_warm_start = True self.assertTrue(config.tee) self.assertEqual(config._description, "A description") self.assertIsNone(config.time_limit) + self.assertTrue(config.knitro_warm_start) @unittest.skipIf(not avail, "KNITRO solver is not available") @@ -486,3 +489,89 @@ def test_solve_HS071(self): self.assertAlmostEqual(pyo.value(m.x[2]), 4.743, 3) self.assertAlmostEqual(pyo.value(m.x[3]), 3.821, 3) self.assertAlmostEqual(pyo.value(m.x[4]), 1.379, 3) + + +@unittest.skipIf(not avail, "KNITRO solver is not available") +class TestKnitroWarmStart(unittest.TestCase): + """Test cases for KNITRO warm start (knitro_warm_start) functionality.""" + + def setUp(self): + self.opt = KnitroDirectSolver() + + def test_warm_start_reduces_iterations(self): + """Test that providing a good starting point reduces the number of iterations.""" + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(-5, 5)) + m.y = pyo.Var(bounds=(-5, 5)) + m.obj = pyo.Objective( + expr=(1.0 - m.x) ** 2 + 100.0 * (m.y - m.x**2) ** 2, sense=pyo.minimize + ) + + m.x.set_value(None) + m.y.set_value(None) + res_no_start = self.opt.solve(m, knitro_warm_start=False) + iters_no_start = res_no_start.extra_info.number_iters + + m.x.set_value(0.9) + m.y.set_value(0.9) + res_with_start = self.opt.solve(m, knitro_warm_start=True) + iters_with_start = res_with_start.extra_info.number_iters + + self.assertAlmostEqual(pyo.value(m.x), 1.0, 3) + self.assertAlmostEqual(pyo.value(m.y), 1.0, 3) + + self.assertLessEqual(iters_with_start, iters_no_start) + + def test_warm_start_uses_initial_values(self): + """Test that warm start uses the current variable values.""" + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(0, 10)) + m.y = pyo.Var(bounds=(0, 10)) + m.obj = pyo.Objective(expr=(m.x - 3) ** 2 + (m.y - 4) ** 2, sense=pyo.minimize) + m.x.set_value(3.0) + m.y.set_value(4.0) + res = self.opt.solve(m, knitro_warm_start=True) + self.assertAlmostEqual(pyo.value(m.x), 3.0, 5) + self.assertAlmostEqual(pyo.value(m.y), 4.0, 5) + self.assertAlmostEqual(res.incumbent_objective, 0.0, 5) + self.assertLessEqual(res.extra_info.number_iters, 1) + + def test_warm_start_with_subset_variables(self): + """Test warm start when only a subset of variables have initial values.""" + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(0, 10)) + m.y = pyo.Var(bounds=(0, 10)) + m.obj = pyo.Objective(expr=(m.x - 3) ** 2 + (m.y - 4) ** 2, sense=pyo.minimize) + m.x.set_value(3.0) + m.y.set_value(None) + res = self.opt.solve(m, knitro_warm_start=True) + self.assertAlmostEqual(pyo.value(m.x), 3.0, 5) + self.assertAlmostEqual(pyo.value(m.y), 4.0, 5) + self.assertAlmostEqual(res.incumbent_objective, 0.0, 5) + + def test_warm_start_disabled(self): + """Test that knitro_warm_start=False disables warm start.""" + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(0, 10)) + m.y = pyo.Var(bounds=(0, 10)) + m.obj = pyo.Objective(expr=(m.x - 3) ** 2 + (m.y - 4) ** 2, sense=pyo.minimize) + m.x.set_value(3.0) + m.y.set_value(4.0) + res = self.opt.solve(m, knitro_warm_start=False) + self.assertAlmostEqual(pyo.value(m.x), 3.0, 5) + self.assertAlmostEqual(pyo.value(m.y), 4.0, 5) + self.assertAlmostEqual(res.incumbent_objective, 0.0, 5) + + def test_warm_start_with_constraints(self): + """Test warm start with constrained optimization.""" + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(0, None)) + m.y = pyo.Var(bounds=(0, None)) + m.obj = pyo.Objective(expr=m.x + m.y, sense=pyo.minimize) + m.c1 = pyo.Constraint(expr=m.x + 2 * m.y >= 4) + m.c2 = pyo.Constraint(expr=2 * m.x + m.y >= 4) + m.x.set_value(1.3) + m.y.set_value(1.3) + self.opt.solve(m, knitro_warm_start=True) + self.assertAlmostEqual(pyo.value(m.x), 4.0 / 3.0, 3) + self.assertAlmostEqual(pyo.value(m.y), 4.0 / 3.0, 3)