Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions pyomo/contrib/solver/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,24 @@
from .solvers.gurobi_persistent import GurobiPersistent
from .solvers.gurobi_direct import GurobiDirect
from .solvers.highs import Highs
from .solvers.knitro import load as load_knitro


def load():
SolverFactory.register(
name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver'
name="ipopt", legacy_name="ipopt_v2", doc="The IPOPT NLP solver"
)(Ipopt, LegacyIpoptSolver)
SolverFactory.register(
name='gurobi_persistent',
legacy_name='gurobi_persistent_v2',
doc='Persistent interface to Gurobi',
name="gurobi_persistent",
legacy_name="gurobi_persistent_v2",
doc="Persistent interface to Gurobi",
)(GurobiPersistent)
SolverFactory.register(
name='gurobi_direct',
legacy_name='gurobi_direct_v2',
doc='Direct (scipy-based) interface to Gurobi',
name="gurobi_direct",
legacy_name="gurobi_direct_v2",
doc="Direct (scipy-based) interface to Gurobi",
)(GurobiDirect)
SolverFactory.register(
name='highs', legacy_name='highs', doc='Persistent interface to HiGHS'
name="highs", legacy_name="highs", doc="Persistent interface to HiGHS"
)(Highs)
load_knitro()
26 changes: 26 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ___________________________________________________________________________
#
# 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.contrib.solver.common.factory import SolverFactory

from .config import Config as KnitroConfig
from .direct import Solver as KnitroDirectSolver

__all__ = ["KnitroConfig", "KnitroDirectSolver"]


# This function needs to be called from the plugins load function
def load():
SolverFactory.register(
name="knitro_direct",
legacy_name="knitro_direct",
doc="Direct interface to KNITRO solver",
)(KnitroDirectSolver)
18 changes: 18 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# ___________________________________________________________________________
#
# 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.dependencies import attempt_import

# import knitro

knitro, KNITRO_AVAILABLE = attempt_import("knitro")
KNITRO_VERSION = knitro.__version__ if KNITRO_AVAILABLE else "0.0.0"
235 changes: 235 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# ___________________________________________________________________________
#
# 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 abc import abstractmethod
from collections.abc import Iterable, Mapping, Sequence
from datetime import datetime, timezone
from io import StringIO
from typing import Optional

from pyomo.common.collections import ComponentMap
from pyomo.common.errors import ApplicationError
from pyomo.common.numeric_types import value
from pyomo.common.tee import TeeStream, capture_output
from pyomo.common.timing import HierarchicalTimer
from pyomo.contrib.solver.common import base
from pyomo.contrib.solver.common.results import (
Results,
SolutionStatus,
TerminationCondition,
)
from pyomo.contrib.solver.common.util import (
IncompatibleModelError,
NoDualsError,
NoOptimalSolutionError,
NoSolutionError,
)
from pyomo.core.base.block import BlockData
from pyomo.core.base.constraint import ConstraintData
from pyomo.core.base.var import VarData
from pyomo.core.staleflag import StaleFlagManager

from .api import knitro
from .config import Config
from .engine import Engine
from .package import PackageChecker
from .utils import Problem
from .solution import SolutionLoader, SolutionProvider


class SolverBase(SolutionProvider, PackageChecker, base.SolverBase):
CONFIG = Config()
config: Config

_engine: Engine
_problem: Problem
_stream: StringIO
_saved_var_values: Mapping[int, float]

def __init__(self, **kwds) -> None:
PackageChecker.__init__(self)
base.SolverBase.__init__(self, **kwds)
self._engine = Engine()
self._problem = Problem()
self._stream = StringIO()

def solve(self, model: BlockData, **kwds) -> Results:
tick = datetime.now(timezone.utc)
self._check_available()

config = self._build_config(**kwds)
timer = config.timer or HierarchicalTimer()

StaleFlagManager.mark_all_as_stale()

self._presolve(model, config, timer)
self._validate_problem()

self._stream = StringIO()
if config.restore_variable_values_after_solve:
self._save_var_values()

with capture_output(TeeStream(self._stream, *config.tee), capture_fd=False):
self._solve(config, timer)

if config.restore_variable_values_after_solve:
self._restore_var_values()

results = self._postsolve(config, timer)

tock = datetime.now(timezone.utc)

results.timing_info.start_timestamp = tick
results.timing_info.wall_time = (tock - tick).total_seconds()
return results

def _build_config(self, **kwds) -> Config:
return self.config(value=kwds, preserve_implicit=True)

def _validate_problem(self) -> None:
if len(self._problem.objs) > 1:
msg = f"{self.name} does not support multiple objectives."
raise IncompatibleModelError(msg)

def _check_available(self) -> None:
avail = self.available()
if not avail:
msg = f"Solver {self.name} is not available: {avail}."
raise ApplicationError(msg)

def _save_var_values(self) -> None:
self._saved_var_values = {id(var): value(var.value) for var in self.get_vars()}

def _restore_var_values(self) -> None:
for var in self.get_vars():
if id(var) in self._saved_var_values:
var.set_value(self._saved_var_values[id(var)])

@abstractmethod
def _presolve(
self, model: BlockData, config: Config, timer: HierarchicalTimer
) -> None:
raise NotImplementedError

@abstractmethod
def _solve(self, config: Config, timer: HierarchicalTimer) -> None:
raise NotImplementedError

def _postsolve(self, config: Config, timer: HierarchicalTimer) -> Results:
status = self._engine.get_status()
results = Results()
results.solver_name = self.name
results.solver_version = self.version()
results.solver_log = self._stream.getvalue()
results.solver_config = config
results.solution_status = self.get_solution_status(status)
results.termination_condition = self.get_termination_condition(status)
results.incumbent_objective = self._engine.get_obj_value()
results.iteration_count = self._engine.get_num_iters()
results.timing_info.solve_time = self._engine.get_solve_time()
results.timing_info.timer = timer

if (
config.raise_exception_on_nonoptimal_result
and results.termination_condition
!= TerminationCondition.convergenceCriteriaSatisfied
):
raise NoOptimalSolutionError()

results.solution_loader = SolutionLoader(self)
if config.load_solutions:
timer.start("load_solutions")
results.solution_loader.load_vars()
timer.stop("load_solutions")

return results

def get_vars(self):
return self._problem.variables

def get_objectives(self):
return self._problem.objs

def get_cons(self):
return self._problem.cons

def get_primals(self, vars_to_load: Optional[Sequence[VarData]] = None):
if vars_to_load is None:
vars_to_load = self.get_vars()

x = self._engine.get_primals(vars_to_load)
if x is None:
return NoSolutionError()
return ComponentMap([(var, x[i]) for i, var in enumerate(vars_to_load)])

def get_duals(self, cons_to_load: Optional[Sequence[ConstraintData]] = None):
if cons_to_load is None:
cons_to_load = self.get_cons()
y = self._engine.get_duals(cons_to_load)
if y is None:
return NoDualsError()
return ComponentMap([(con, y[i]) for i, con in enumerate(cons_to_load)])

def get_num_solutions(self):
return self._engine.get_num_solutions()

@staticmethod
def get_solution_status(status: int) -> SolutionStatus:
if (
status == knitro.KN_RC_OPTIMAL
or status == knitro.KN_RC_OPTIMAL_OR_SATISFACTORY
or status == knitro.KN_RC_NEAR_OPT
):
return SolutionStatus.optimal
elif status == knitro.KN_RC_FEAS_NO_IMPROVE:
return SolutionStatus.feasible
elif (
status == knitro.KN_RC_INFEASIBLE
or status == knitro.KN_RC_INFEAS_CON_BOUNDS
or status == knitro.KN_RC_INFEAS_VAR_BOUNDS
or status == knitro.KN_RC_INFEAS_NO_IMPROVE
):
return SolutionStatus.infeasible
else:
return SolutionStatus.noSolution

@staticmethod
def get_termination_condition(status: int) -> TerminationCondition:
if (
status == knitro.KN_RC_OPTIMAL
or status == knitro.KN_RC_OPTIMAL_OR_SATISFACTORY
or status == knitro.KN_RC_NEAR_OPT
):
return TerminationCondition.convergenceCriteriaSatisfied
elif status == knitro.KN_RC_INFEAS_NO_IMPROVE:
return TerminationCondition.locallyInfeasible
elif status == knitro.KN_RC_INFEASIBLE:
return TerminationCondition.provenInfeasible
elif (
status == knitro.KN_RC_UNBOUNDED_OR_INFEAS
or status == knitro.KN_RC_UNBOUNDED
):
return TerminationCondition.infeasibleOrUnbounded
elif (
status == knitro.KN_RC_ITER_LIMIT_FEAS
or status == knitro.KN_RC_ITER_LIMIT_INFEAS
):
return TerminationCondition.iterationLimit
elif (
status == knitro.KN_RC_TIME_LIMIT_FEAS
or status == knitro.KN_RC_TIME_LIMIT_INFEAS
):
return TerminationCondition.maxTimeLimit
elif status == knitro.KN_RC_USER_TERMINATION:
return TerminationCondition.interrupted
else:
return TerminationCondition.unknown
49 changes: 49 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# ___________________________________________________________________________
#
# 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.config import Bool, ConfigValue
from pyomo.contrib.solver.common.config import SolverConfig


class Config(SolverConfig):
def __init__(
self,
description=None,
doc=None,
implicit=False,
implicit_domain=None,
visibility=0,
):
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="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.",
),
)

self.restore_variable_values_after_solve: bool = self.declare(
"restore_variable_values_after_solve",
ConfigValue(
domain=Bool,
default=True,
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.",
),
)
Loading
Loading