diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index acd78284103..fbab1d70feb 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -17,6 +17,7 @@ from .solvers.gurobi_direct_minlp import GurobiDirectMINLP from .solvers.highs import Highs from .solvers.knitro.direct import KnitroDirectSolver +from .solvers.knitro.persistent import KnitroPersistentSolver def load(): @@ -34,9 +35,9 @@ def load(): doc="Direct (scipy-based) interface to Gurobi", )(GurobiDirect) SolverFactory.register( - name='gurobi_direct_minlp', - legacy_name='gurobi_direct_minlp', - doc='Direct interface to Gurobi accommodating general MINLP', + name="gurobi_direct_minlp", + legacy_name="gurobi_direct_minlp", + doc="Direct interface to Gurobi accommodating general MINLP", )(GurobiDirectMINLP) SolverFactory.register( name="highs", legacy_name="highs", doc="Persistent interface to HiGHS" @@ -46,3 +47,8 @@ def load(): legacy_name="knitro_direct", doc="Direct interface to KNITRO solver", )(KnitroDirectSolver) + SolverFactory.register( + name="knitro_persistent", + legacy_name="knitro_persistent", + doc="Persistent interface to KNITRO solver", + )(KnitroPersistentSolver) diff --git a/pyomo/contrib/solver/solvers/knitro/base.py b/pyomo/contrib/solver/solvers/knitro/base.py index 003ee8cab2e..e06bcf08761 100644 --- a/pyomo/contrib/solver/solvers/knitro/base.py +++ b/pyomo/contrib/solver/solvers/knitro/base.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from abc import abstractmethod -from collections.abc import Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence from datetime import datetime, timezone from io import StringIO from typing import Optional @@ -49,8 +49,6 @@ class KnitroSolverBase(SolutionProvider, PackageChecker, SolverBase): - CONFIG = KnitroConfig() - config: KnitroConfig _engine: Engine _model_data: KnitroModelData @@ -96,7 +94,7 @@ def solve(self, model: BlockData, **kwds) -> Results: return results def _build_config(self, **kwds) -> KnitroConfig: - return self.config(value=kwds, preserve_implicit=True) # type: ignore + return self.config(value=kwds, preserve_implicit=True) def _validate_problem(self) -> None: if len(self._model_data.objs) > 1: @@ -194,7 +192,7 @@ def get_num_solutions(self) -> int: def _get_vars(self) -> list[VarData]: return self._model_data.variables - def _get_items(self, item_type: type[ItemType]) -> Sequence[ItemType]: + def _get_items(self, item_type: type[ItemType]) -> Iterable[ItemType]: maps = { VarData: self._model_data.variables, ConstraintData: self._model_data.cons, diff --git a/pyomo/contrib/solver/solvers/knitro/config.py b/pyomo/contrib/solver/solvers/knitro/config.py index c306059a247..981bbc7ef08 100644 --- a/pyomo/contrib/solver/solvers/knitro/config.py +++ b/pyomo/contrib/solver/solvers/knitro/config.py @@ -10,10 +10,12 @@ # ___________________________________________________________________________ from pyomo.common.config import Bool, ConfigValue -from pyomo.contrib.solver.common.config import SolverConfig +from pyomo.contrib.solver.common.config import PersistentSolverConfig, SolverConfig class KnitroConfig(SolverConfig): + """Configuration for the direct Knitro solver interface.""" + def __init__( self, description=None, @@ -30,31 +32,55 @@ def __init__( visibility=visibility, ) - self.rebuild_model_on_remove_var: bool = self.declare( - "rebuild_model_on_remove_var", + self.restore_variable_values_after_solve: bool = self.declare( + "restore_variable_values_after_solve", ConfigValue( domain=Bool, default=False, doc=( - "KNITRO solver does not allow variable removal. We can " - "either make the variable a continuous free variable or " - "rebuild the whole model when variable removal is " - "attempted. When `rebuild_model_on_remove_var` is set to " - "True, the model will be rebuilt." + "To evaluate non-linear constraints, KNITRO solver sets " + "explicit values on variables. This option controls " + "whether to restore the original variable values after " + "solving." ), ), ) - self.restore_variable_values_after_solve: bool = self.declare( - "restore_variable_values_after_solve", + +class KnitroPersistentConfig(KnitroConfig, PersistentSolverConfig): + """Configuration for the persistent Knitro solver interface. + + Extends KnitroConfig with persistent solver capabilities including + auto_updates configuration. + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ) -> None: + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.rebuild_model_on_remove_var: bool = self.declare( + "rebuild_model_on_remove_var", ConfigValue( domain=Bool, default=False, doc=( - "To evaluate non-linear constraints, KNITRO solver sets " - "explicit values on variables. This option controls " - "whether to restore the original variable values after " - "solving." + "KNITRO solver does not allow variable removal. We can " + "either make the variable a continuous free variable or " + "rebuild the whole model when variable removal is " + "attempted. When `rebuild_model_on_remove_var` is set to " + "True, the model will be rebuilt." ), ), ) diff --git a/pyomo/contrib/solver/solvers/knitro/direct.py b/pyomo/contrib/solver/solvers/knitro/direct.py index 8bab37032e3..ce118e09579 100644 --- a/pyomo/contrib/solver/solvers/knitro/direct.py +++ b/pyomo/contrib/solver/solvers/knitro/direct.py @@ -17,6 +17,9 @@ class KnitroDirectSolver(KnitroSolverBase): + CONFIG = KnitroConfig() + config: KnitroConfig + def _presolve( self, model: BlockData, config: KnitroConfig, timer: HierarchicalTimer ) -> None: diff --git a/pyomo/contrib/solver/solvers/knitro/persistent.py b/pyomo/contrib/solver/solvers/knitro/persistent.py new file mode 100644 index 00000000000..9166753e4c9 --- /dev/null +++ b/pyomo/contrib/solver/solvers/knitro/persistent.py @@ -0,0 +1,133 @@ +# ___________________________________________________________________________ +# +# 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. +# ___________________________________________________________________________ + +from pyomo.common.timing import HierarchicalTimer +from pyomo.contrib.solver.common.base import PersistentSolverBase +from pyomo.contrib.solver.solvers.knitro.base import KnitroSolverBase +from pyomo.contrib.solver.solvers.knitro.config import KnitroPersistentConfig +from pyomo.contrib.solver.solvers.knitro.utils import KnitroModelData +from pyomo.core.base.block import BlockData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.objective import ObjectiveData +from pyomo.core.base.param import ParamData +from pyomo.core.base.var import VarData + + +class KnitroPersistentSolver(KnitroSolverBase, PersistentSolverBase): + CONFIG = KnitroPersistentConfig() + config: KnitroPersistentConfig + + _model: BlockData | None + _staged_model_data: KnitroModelData + + def __init__(self, **kwds) -> None: + PersistentSolverBase.__init__(self, **kwds) + KnitroSolverBase.__init__(self, **kwds) + self._model = None + self._staged_model_data = KnitroModelData() + + def _presolve( + self, model: BlockData, config: KnitroPersistentConfig, timer: HierarchicalTimer + ) -> None: + if self._model is not model: + self.set_instance(model) + self._staged_model_data.clear() + + if self._staged_model_data: + self._update() + + def _solve(self, config: KnitroPersistentConfig, timer: HierarchicalTimer) -> None: + self._engine.set_outlev() + if config.threads is not None: + self._engine.set_num_threads(config.threads) + if config.time_limit is not None: + self._engine.set_time_limit(config.time_limit) + + timer.start("load_options") + self._engine.set_options(**config.solver_options) + timer.stop("load_options") + + timer.start("solve") + self._engine.solve() + timer.stop("solve") + + def set_instance(self, model: BlockData): + if self._model is model: + return + self._model = model + self._model_data.set_block(model) + self._engine.renew() + self._engine.add_vars(self._model_data.variables) + self._engine.add_cons(self._model_data.cons) + if self._model_data.objs: + self._engine.set_obj(self._model_data.objs[0]) + + def add_block(self, block: BlockData): + self._staged_model_data.add_block(block, clear_objs=True) + + def add_variables(self, variables: list[VarData]): + self._staged_model_data.add_vars(variables) + + def add_constraints(self, cons: list[ConstraintData]): + self._staged_model_data.add_cons(cons, existing_vars=self._model_data.variables) + + def set_objective(self, obj: ObjectiveData): + self._staged_model_data.objs.clear() + self._staged_model_data.objs.append(obj) + + def _update(self): + self._model_data.add_vars(self._staged_model_data.variables) + self._model_data.add_cons(self._staged_model_data.cons) + + self._engine.add_vars(self._staged_model_data.variables) + self._engine.add_cons(self._staged_model_data.cons) + + if self._staged_model_data.objs: + self._model_data.objs.clear() + self._model_data.objs.extend(self._staged_model_data.objs) + self._engine.set_obj(self._model_data.objs[0]) + + self._staged_model_data.clear() + + def remove_variables(self, variables: list[VarData]) -> None: + raise NotImplementedError( + "KnitroPersistentSolver does not support removing variables." + ) + + def remove_constraints(self, cons: list[ConstraintData]) -> None: + raise NotImplementedError( + "KnitroPersistentSolver does not support removing constraints." + ) + + def update_variables(self, variables: list[VarData]) -> None: + raise NotImplementedError( + "KnitroPersistentSolver does not support updating variables." + ) + + def update_parameters(self) -> None: + raise NotImplementedError( + "KnitroPersistentSolver does not support updating parameters." + ) + + def add_parameters(self, params: list[ParamData]) -> None: + raise NotImplementedError( + "KnitroPersistentSolver does not support adding parameters." + ) + + def remove_parameters(self, params: list[ParamData]) -> None: + raise NotImplementedError( + "KnitroPersistentSolver does not support removing parameters." + ) + + def remove_block(self, block: BlockData) -> None: + raise NotImplementedError( + "KnitroPersistentSolver does not support removing blocks." + ) diff --git a/pyomo/contrib/solver/solvers/knitro/utils.py b/pyomo/contrib/solver/solvers/knitro/utils.py index bba08f73216..20569609b2f 100644 --- a/pyomo/contrib/solver/solvers/knitro/utils.py +++ b/pyomo/contrib/solver/solvers/knitro/utils.py @@ -10,7 +10,6 @@ # ___________________________________________________________________________ from collections.abc import Iterable, Mapping, MutableSet, Sequence -from typing import Optional from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.numeric_types import value @@ -72,21 +71,19 @@ class KnitroModelData: objs: list[ObjectiveData] cons: list[ConstraintData] - variables: list[VarData] _vars: MutableSet[VarData] - def __init__(self, block: Optional[BlockData] = None) -> None: + def __init__(self, block: BlockData | None = None) -> None: """Initialize a Problem instance. Args: - block (Optional[BlockData]): Pyomo block to initialize from. If None, + block (BlockData | None): Pyomo block to initialize from. If None, creates an empty problem that can be populated later. """ self._vars = ComponentSet() self.objs = [] self.cons = [] - self.variables = [] if block is not None: self.add_block(block) @@ -94,7 +91,6 @@ def clear(self) -> None: """Clear all objectives, constraints, and variables from the problem.""" self.objs.clear() self.cons.clear() - self.variables.clear() self._vars.clear() def set_block(self, block: BlockData) -> None: @@ -107,32 +103,77 @@ def set_block(self, block: BlockData) -> None: self.clear() self.add_block(block) - def add_block(self, block: BlockData) -> None: + def add_block(self, block: BlockData, *, clear_objs: bool = False) -> None: """Add objectives, constraints, and variables from a block to the problem. Args: block (BlockData): The Pyomo block to extract data from. + clear_objs (bool): Whether to clear the objectives before adding new ones. """ new_objs = get_active_objectives(block) new_cons = get_active_constraints(block) + if clear_objs and self.objs: + self.objs.clear() self.objs.extend(new_objs) self.cons.extend(new_cons) # Collect variables from objectives for obj in new_objs: - _, variables, _, _ = collect_vars_and_named_exprs(obj.expr) # type: ignore + _, variables, _, _ = collect_vars_and_named_exprs(obj.expr) for var in variables: self._vars.add(var) # Collect variables from constraints for con in new_cons: - _, variables, _, _ = collect_vars_and_named_exprs(con.body) # type: ignore + _, variables, _, _ = collect_vars_and_named_exprs(con.body) for var in variables: self._vars.add(var) - # Update the variables list with unique variables only - self.variables = list(self._vars) + def add_vars(self, variables: Iterable[VarData]) -> None: + """Add variables to the problem. + + Args: + variables (list[VarData]): The list of variables to add. + + """ + for var in variables: + self._vars.add(var) + + def add_cons( + self, + cons: Iterable[ConstraintData], + *, + existing_vars: MutableSet[VarData] | None = None, + ) -> None: + """Add constraints to the problem. + + Args: + cons (list[ConstraintData]): The list of constraints to add. + existing_vars (MutableSet[VarData] | None): Existing variable set to check + for already-tracked variables. New variables will be added to this + instance's internal set. + + """ + self.cons.extend(cons) + for con in cons: + _, variables, _, _ = collect_vars_and_named_exprs(con.body) + for var in variables: + if existing_vars is None or var not in existing_vars: + self._vars.add(var) + + @property + def variables(self) -> MutableSet[VarData]: + """Get the list of variables in the problem. + + Returns: + MutableSet[VarData]: The set of variables. + + """ + return self._vars + + def __bool__(self) -> bool: + return bool(self.objs or self.cons or self.variables) def set_var_values( diff --git a/pyomo/contrib/solver/tests/solvers/test_knitro_persistent.py b/pyomo/contrib/solver/tests/solvers/test_knitro_persistent.py new file mode 100644 index 00000000000..068dc25b81c --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_knitro_persistent.py @@ -0,0 +1,115 @@ +# ___________________________________________________________________________ +# +# 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 pyomo.common.unittest as unittest +import pyomo.environ as pyo +from pyomo.contrib.solver.solvers.knitro.persistent import KnitroPersistentSolver + +avail = KnitroPersistentSolver().available() + + +@unittest.skipIf(not avail, "KNITRO solver is not available") +class TestKnitroPersistentSolver(unittest.TestCase): + def setUp(self): + self.opt = KnitroPersistentSolver() + + def test_basics(self): + self.assertTrue(self.opt.is_persistent()) + self.assertEqual(self.opt.name, "knitro_persistent") + self.assertTrue(self.opt.available()) + + def test_solve(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(initialize=1.5, bounds=(-5, 5)) + m.y = pyo.Var(initialize=1.5, bounds=(-5, 5)) + m.obj = pyo.Objective( + expr=(1.0 - m.x) + 100.0 * (m.y - m.x), sense=pyo.minimize + ) + self.opt.set_instance(m) + res = self.opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -1004) + self.assertAlmostEqual(m.x.value, 5) + self.assertAlmostEqual(m.y.value, -5) + + def test_incremental_add_variables(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(initialize=1.5, bounds=(-5, 5)) + self.opt.add_variables([m.x]) + + # Add variable y incrementally + m.y = pyo.Var(initialize=1.5, bounds=(-5, 5)) + self.opt.add_variables([m.y]) + + # Add objective + m.obj = pyo.Objective( + expr=(1.0 - m.x) + 100.0 * (m.y - m.x), sense=pyo.minimize + ) + self.opt.set_objective(m.obj) + res = self.opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -1004) + self.assertAlmostEqual(m.x.value, 5) + self.assertAlmostEqual(m.y.value, -5) + + def test_incremental_add_constraints(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(initialize=1.5, bounds=(-5, 5)) + m.y = pyo.Var(initialize=1.5, bounds=(-5, 5)) + m.obj = pyo.Objective(expr=(m.y - m.x) ** 2, sense=pyo.minimize) + + self.opt.set_instance(m) + + # Add constraint incrementally + m.c1 = pyo.Constraint(expr=m.x**2 + m.y**2 <= 4) + self.opt.add_constraints([m.c1]) + + results = self.opt.solve(m) + self.assertAlmostEqual(results.incumbent_objective, 0.0) + # Check feasibility + self.assertTrue(pyo.value(m.x) ** 2 + pyo.value(m.y) ** 2 <= 4.0001) + + def test_incremental_add_block(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(initialize=0, bounds=(-5, 5)) + m.obj = pyo.Objective(expr=m.x, sense=pyo.minimize) + self.opt.set_instance(m) + + m.b = pyo.Block() + m.b.y = pyo.Var(initialize=0, bounds=(-5, 5)) + m.b.c = pyo.Constraint(expr=m.b.y >= m.x) + + self.opt.add_block(m.b) + + # Update objective to include y + m.obj.expr += m.b.y + self.opt.set_objective(m.obj) + + self.opt.solve(m) + # min x + y s.t. y >= x, -5<=x<=5, -5<=y<=5 + # x=-5, y=-5 => obj = -10 + self.assertAlmostEqual(m.x.value, -5) + self.assertAlmostEqual(m.b.y.value, -5) + + def test_incremental_set_objective(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(initialize=1.5, bounds=(-5, 5)) + m.y = pyo.Var(initialize=1.5, bounds=(-5, 5)) + m.obj = pyo.Objective(expr=(m.x - m.y) ** 2, sense=pyo.minimize) + + self.opt.set_objective(m.obj) + + # Add constraint incrementally + m.c1 = pyo.Constraint(expr=m.x**2 + m.y**2 <= 4) + self.opt.add_constraints([m.c1]) + + results = self.opt.solve(m) + self.assertAlmostEqual(results.incumbent_objective, 0) + # Check feasibility + self.assertTrue(pyo.value(m.x) ** 2 + pyo.value(m.y) ** 2 <= 4.0001) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 8aa452444ec..d4f3db5718b 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -37,6 +37,7 @@ from pyomo.contrib.solver.solvers.highs import Highs from pyomo.contrib.solver.solvers.ipopt import Ipopt from pyomo.contrib.solver.solvers.knitro.direct import KnitroDirectSolver +from pyomo.contrib.solver.solvers.knitro.persistent import KnitroPersistentSolver from pyomo.contrib.solver.tests.solvers import instances from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.core.expr.numeric_expr import LinearExpression @@ -56,6 +57,7 @@ ('ipopt', Ipopt), ('highs', Highs), ('knitro_direct', KnitroDirectSolver), + ('knitro_persistent', KnitroPersistentSolver), ] mip_solvers = [ ('gurobi_persistent', GurobiPersistent), @@ -63,31 +65,37 @@ ('gurobi_direct_minlp', GurobiDirectMINLP), ('highs', Highs), ('knitro_direct', KnitroDirectSolver), + ('knitro_persistent', KnitroPersistentSolver), ] nlp_solvers = [ ('gurobi_direct_minlp', GurobiDirectMINLP), ('ipopt', Ipopt), ('knitro_direct', KnitroDirectSolver), + ('knitro_persistent', KnitroPersistentSolver), ] qcp_solvers = [ ('gurobi_persistent', GurobiPersistent), ('gurobi_direct_minlp', GurobiDirectMINLP), ('ipopt', Ipopt), ('knitro_direct', KnitroDirectSolver), + ('knitro_persistent', KnitroPersistentSolver), ] qp_solvers = qcp_solvers + [("highs", Highs)] miqcqp_solvers = [ ('gurobi_direct_minlp', GurobiDirectMINLP), ('gurobi_persistent', GurobiPersistent), ('knitro_direct', KnitroDirectSolver), + ('knitro_persistent', KnitroPersistentSolver), ] nl_solvers = [('ipopt', Ipopt)] nl_solvers_set = {i[0] for i in nl_solvers} -def _load_tests(solver_list): +def _load_tests(solver_list, skip=None): res = list() for solver_name, solver in solver_list: + if skip and solver_name in skip: + continue if solver_name in nl_solvers_set: test_name = f"{solver_name}_presolve" res.append((test_name, solver, True)) @@ -628,7 +636,7 @@ def test_results_object_populated( self.assertIsNotNone(res.solver_log) self.assertIsInstance(res.solver_log, str) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=["knitro_persistent"])) def test_remove_variable_and_objective( self, name: str, opt_class: Type[SolverBase], use_presolve ): @@ -704,7 +712,7 @@ def test_stale_vars( res.solution_loader.load_vars([m.y]) self.assertFalse(m.y.stale) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=["knitro_persistent"])) def test_range_constraint( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -732,7 +740,7 @@ def test_range_constraint( duals = res.solution_loader.get_duals() self.assertAlmostEqual(duals[m.c], 1) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=["knitro_persistent"])) def test_reduced_costs( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -761,7 +769,7 @@ def test_reduced_costs( self.assertAlmostEqual(rc[m.x], -3) self.assertAlmostEqual(rc[m.y], -4) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=["knitro_persistent"])) def test_reduced_costs2( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -788,7 +796,7 @@ def test_reduced_costs2( rc = res.solution_loader.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_param_changes( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -831,7 +839,7 @@ def test_param_changes( self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_immutable_param( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -878,7 +886,7 @@ def test_immutable_param( self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): @@ -922,7 +930,7 @@ def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bo self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_linear_expression( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -967,7 +975,7 @@ def test_linear_expression( bound = res.objective_bound self.assertTrue(bound <= m.y.value) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_no_objective( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1008,7 +1016,7 @@ def test_no_objective( self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=["knitro_persistent"])) def test_add_remove_cons( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1153,7 +1161,7 @@ def test_duals(self, name: str, opt_class: Type[SolverBase], use_presolve: bool) self.assertAlmostEqual(duals[m.c1], 0.5) self.assertNotIn(m.c2, duals) - @parameterized.expand(input=_load_tests(qcp_solvers)) + @parameterized.expand(input=_load_tests(qcp_solvers, skip=['knitro_persistent'])) def test_mutable_quadratic_coefficient( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1182,7 +1190,7 @@ def test_mutable_quadratic_coefficient( self.assertAlmostEqual(m.x.value, 0.10256137418973625, 4) self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) - @parameterized.expand(input=_load_tests(qcp_solvers)) + @parameterized.expand(input=_load_tests(qcp_solvers, skip=['knitro_persistent'])) def test_mutable_quadratic_objective_qcp( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1214,7 +1222,7 @@ def test_mutable_quadratic_objective_qcp( self.assertAlmostEqual(m.x.value, 0.6962249634573562, 4) self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) - @parameterized.expand(input=_load_tests(qp_solvers)) + @parameterized.expand(input=_load_tests(qp_solvers, skip=['knitro_persistent'])) def test_mutable_quadratic_objective_qp( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1289,7 +1297,7 @@ def test_mutable_quadratic_objective_qp( if opt_class is Highs: self.assertIn(opt._pyomo_var_to_solver_var_map[id(m.x3)], {0, 1}) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_fixed_vars( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1335,7 +1343,7 @@ def test_fixed_vars( self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_fixed_vars_2( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1380,7 +1388,7 @@ def test_fixed_vars_2( self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_fixed_vars_3( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1404,7 +1412,7 @@ def test_fixed_vars_3( self.assertAlmostEqual(res.incumbent_objective, 3) self.assertAlmostEqual(m.x.value, 2) - @parameterized.expand(input=_load_tests(nlp_solvers)) + @parameterized.expand(input=_load_tests(nlp_solvers, skip=['knitro_persistent'])) def test_fixed_vars_4( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1431,7 +1439,7 @@ def test_fixed_vars_4( self.assertAlmostEqual(m.x.value, 2**0.5, delta=1e-3) self.assertAlmostEqual(m.y.value, 2**0.5, delta=1e-3) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_mutable_param_with_range( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1528,7 +1536,7 @@ def test_mutable_param_with_range( self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_add_and_remove_vars( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1650,7 +1658,7 @@ def test_with_numpy( self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_bounds_with_params( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1809,7 +1817,7 @@ def test_time_limit( {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit}, ) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_objective_changes( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1880,7 +1888,7 @@ def test_objective_changes( res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, 4) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_domain(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): @@ -1909,7 +1917,7 @@ def test_domain(self, name: str, opt_class: Type[SolverBase], use_presolve: bool res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, 0) - @parameterized.expand(input=_load_tests(mip_solvers)) + @parameterized.expand(input=_load_tests(mip_solvers, skip=['knitro_persistent'])) def test_domain_with_integers( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -1940,7 +1948,7 @@ def test_domain_with_integers( res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, 1) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_fixed_binaries( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -2011,7 +2019,7 @@ def test_with_gdp(self, name: str, opt_class: Type[SolverBase], use_presolve: bo self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_variables_elsewhere( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -2045,7 +2053,7 @@ def test_variables_elsewhere( self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_variables_elsewhere2( self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): @@ -2087,7 +2095,7 @@ def test_variables_elsewhere2( self.assertIn(m.y, sol) self.assertNotIn(m.z, sol) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_bug_1(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): @@ -2115,7 +2123,7 @@ def test_bug_1(self, name: str, opt_class: Type[SolverBase], use_presolve: bool) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(res.incumbent_objective, 3) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): """ This test is for a bug where an objective containing a fixed variable does @@ -2162,7 +2170,7 @@ def test_presolve_with_zero_coef( opt.config.writer_config.linear_presolve = False """ - when c2 gets presolved out, c1 becomes + when c2 gets presolved out, c1 becomes x - y + y = 0 which becomes x - 0*y == 0 which is the zero we are testing for """ @@ -2214,7 +2222,7 @@ def test_presolve_with_zero_coef( ) self.assertEqual(res.termination_condition, exp) - @parameterized.expand(input=_load_tests(all_solvers)) + @parameterized.expand(input=_load_tests(all_solvers, skip=['knitro_persistent'])) def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available():