-
Notifications
You must be signed in to change notification settings - Fork 557
New KNITRO direct solver interface #3707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
eminyouskn
wants to merge
65
commits into
Pyomo:main
Choose a base branch
from
eminyouskn:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
65 commits
Select commit
Hold shift + click to select a range
49d3bcb
Add KNITRO direct solver implementation and tests
eminyouskn c5fe4ea
Refactor Knitro API imports and update solver name in tests
eminyouskn e462a36
Add objective value retrieval and quadratic programming test for Knit…
eminyouskn 7a6f16b
Add QCP test to Knitro direct solver
eminyouskn fa8e00a
Add non linear tests.
eminyouskn d43859b
run black.
eminyouskn 97d6f7b
Sort imports.
eminyouskn aa3cc19
Refactor code.
eminyouskn a5408c8
Remove obsolete LP test files for duality tests
eminyouskn b337dee
Refactor code.
eminyouskn 0da5857
Refactor code.
eminyouskn 2bfe480
Refactor code.
eminyouskn 90305df
Add gradient for nonlinear.
eminyouskn 6c2ee44
Defer knitro import.
eminyouskn 8dc0b0d
Improve get_status.
eminyouskn c6e2c14
Refactor imports
eminyouskn 0448318
Use component_set instead of dict.
eminyouskn 8d79af1
Fix level default arg.
eminyouskn 3de9459
Skip test if knitro is not available.
eminyouskn 9d0aa7b
Add KnitroDirectSolver to solver lists
eminyouskn 0573bbd
Add methods for retrieving reduced costs and update duals retrieval i…
eminyouskn bc295f1
black format
eminyouskn f1c8f08
Defer import knitro
eminyouskn f976908
Handle no objective case.
eminyouskn 196b21f
Fix status mapping.
eminyouskn a1e6eb6
black format
eminyouskn b68b2a8
Refactor and fix stale managment flag.
eminyouskn 080d22b
Fix tolerance test
eminyouskn 7faa5d9
Add hessian computation.
eminyouskn d75e0ca
black format
eminyouskn 27a8da3
Refactor
eminyouskn 8097cc7
Refactor
eminyouskn ea592cb
Refactor.
eminyouskn eeb6232
Refactor.
eminyouskn 586d392
Refactor
eminyouskn bf72313
Refactor
eminyouskn 457b199
Refactor
eminyouskn 8aabcdf
Black format
eminyouskn 36a1d2e
Refactor type annotations.
eminyouskn ced727d
Black format
eminyouskn 65d319f
Clean and improve typehint
eminyouskn 485001e
Refactor.
eminyouskn 5d88477
Improve solution retreival
eminyouskn 8022e70
Refactor
eminyouskn 5f49789
Refactor
eminyouskn 1c56aa3
Refactor.
eminyouskn 3f5ab75
Final Refactor.
eminyouskn 5041ab6
black format
eminyouskn b55e8ab
handle mrmundt comments.
eminyouskn df9e1ea
Refactor
eminyouskn 31634cd
Black format
eminyouskn c88b0f7
Fix add_callback call.
eminyouskn 12e910e
Merge pyomo:main
eminyouskn 0ee97e8
Fix and sort imports
eminyouskn 46edd34
Resolve mrmundt comments
eminyouskn a92848a
fix knitro import
eminyouskn cc153d3
Fix knitro module usage.
eminyouskn ca3264f
Update pyomo/contrib/solver/solvers/knitro/engine.py
eminyouskn 5f49a67
make Engine as context manager.
eminyouskn 67a9a92
Use DeveloperError
eminyouskn ea35120
fix test_environ
eminyouskn b0a5a22
fix typing and get_version
eminyouskn daaf888
Add KNITRO to docs
eminyouskn 618ad16
Update doc/OnlineDocs/explanation/experimental/solvers.rst
eminyouskn d1133a0
refactor version
eminyouskn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# ___________________________________________________________________________ | ||
# | ||
# 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. | ||
# ___________________________________________________________________________ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# ___________________________________________________________________________ | ||
# | ||
# 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 typing import Optional | ||
|
||
from pyomo.common.dependencies import attempt_import | ||
|
||
knitro, KNITRO_AVAILABLE = attempt_import("knitro") | ||
|
||
|
||
def get_version() -> Optional[str]: | ||
if not bool(KNITRO_AVAILABLE): | ||
return None | ||
return knitro.__version__ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,271 @@ | ||
# ___________________________________________________________________________ | ||
# | ||
# 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 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, DeveloperError, PyomoException | ||
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.base import SolverBase | ||
from pyomo.contrib.solver.common.results import ( | ||
Results, | ||
SolutionStatus, | ||
TerminationCondition, | ||
) | ||
from pyomo.contrib.solver.common.util import ( | ||
IncompatibleModelError, | ||
NoDualsError, | ||
NoOptimalSolutionError, | ||
NoReducedCostsError, | ||
NoSolutionError, | ||
) | ||
from pyomo.contrib.solver.solvers.knitro.api import knitro | ||
from pyomo.contrib.solver.solvers.knitro.config import KnitroConfig | ||
from pyomo.contrib.solver.solvers.knitro.engine import Engine | ||
from pyomo.contrib.solver.solvers.knitro.package import PackageChecker | ||
from pyomo.contrib.solver.solvers.knitro.solution import ( | ||
SolutionLoader, | ||
SolutionProvider, | ||
) | ||
from pyomo.contrib.solver.solvers.knitro.typing import ItemData, ItemType, ValueType | ||
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.var import VarData | ||
from pyomo.core.staleflag import StaleFlagManager | ||
|
||
|
||
class KnitroSolverBase(SolutionProvider, PackageChecker, SolverBase): | ||
CONFIG = KnitroConfig() | ||
config: KnitroConfig | ||
|
||
_engine: Engine | ||
_model_data: KnitroModelData | ||
_stream: StringIO | ||
_saved_var_values: dict[int, Optional[float]] | ||
|
||
def __init__(self, **kwds) -> None: | ||
PackageChecker.__init__(self) | ||
SolverBase.__init__(self, **kwds) | ||
self._engine = Engine() | ||
self._model_data = KnitroModelData() | ||
self._stream = StringIO() | ||
self._saved_var_values = {} | ||
|
||
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) -> KnitroConfig: | ||
return self.config(value=kwds, preserve_implicit=True) # type: ignore | ||
|
||
def _validate_problem(self) -> None: | ||
if len(self._model_data.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.clear() | ||
for var in self._get_vars(): | ||
self._saved_var_values[id(var)] = value(var.value) | ||
|
||
def _restore_var_values(self) -> None: | ||
StaleFlagManager.mark_all_as_stale(delayed=True) | ||
for var in self._get_vars(): | ||
var.set_value(self._saved_var_values[id(var)]) | ||
StaleFlagManager.mark_all_as_stale() | ||
|
||
@abstractmethod | ||
def _presolve( | ||
self, model: BlockData, config: KnitroConfig, timer: HierarchicalTimer | ||
) -> None: | ||
raise NotImplementedError | ||
|
||
@abstractmethod | ||
def _solve(self, config: KnitroConfig, timer: HierarchicalTimer) -> None: | ||
raise NotImplementedError | ||
|
||
def _postsolve(self, config: KnitroConfig, 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, | ||
has_primals=results.solution_status | ||
not in {SolutionStatus.infeasible, SolutionStatus.noSolution}, | ||
has_reduced_costs=results.solution_status == SolutionStatus.optimal, | ||
has_duals=results.solution_status | ||
not in {SolutionStatus.infeasible, SolutionStatus.noSolution}, | ||
) | ||
if config.load_solutions: | ||
timer.start("load_solutions") | ||
results.solution_loader.load_vars() | ||
timer.stop("load_solutions") | ||
|
||
return results | ||
|
||
def get_values( | ||
self, | ||
item_type: type[ItemType], | ||
value_type: ValueType, | ||
items: Optional[Sequence[ItemType]] = None, | ||
*, | ||
exists: bool, | ||
solution_id: Optional[int] = None, | ||
) -> Mapping[ItemType, float]: | ||
error_type = self._get_error_type(item_type, value_type) | ||
if not exists: | ||
raise error_type() | ||
# KNITRO only supports a single solution | ||
assert solution_id is None | ||
if items is None: | ||
items = self._get_items(item_type) | ||
x = self._engine.get_values(item_type, value_type, items) | ||
if x is None: | ||
raise error_type() | ||
sign = value_type.sign | ||
return ComponentMap([(k, sign * xk) for k, xk in zip(items, x)]) | ||
|
||
def get_num_solutions(self) -> int: | ||
return self._engine.get_num_solutions() | ||
|
||
def _get_vars(self) -> list[VarData]: | ||
return self._model_data.variables | ||
|
||
def _get_items(self, item_type: type[ItemType]) -> Sequence[ItemType]: | ||
maps = { | ||
VarData: self._model_data.variables, | ||
ConstraintData: self._model_data.cons, | ||
} | ||
return maps[item_type] | ||
|
||
@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 | ||
or status == knitro.KN_RC_INFEAS_CON_BOUNDS | ||
or status == knitro.KN_RC_INFEAS_VAR_BOUNDS | ||
): | ||
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 | ||
|
||
@staticmethod | ||
def _get_error_type( | ||
item_type: type[ItemData], value_type: ValueType | ||
) -> type[PyomoException]: | ||
if item_type is VarData and value_type == ValueType.PRIMAL: | ||
return NoSolutionError | ||
elif item_type is VarData and value_type == ValueType.DUAL: | ||
return NoReducedCostsError | ||
elif item_type is ConstraintData and value_type == ValueType.DUAL: | ||
return NoDualsError | ||
raise DeveloperError() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, explicitly casting the flag to bool is unnecessary. You don't need to change it, but the following works:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will change it then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by the way my linter is not happy about removing the bool. So I think I will keep it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the linter complaining about?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it says the following:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh... does the following patch resolve the linter's complaint:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that this patch fixes the issue, but the linter will still complain because the return type of that function doesn’t match the returned value. In #3746, I addressed this by wrapping available with bool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The linter is unfortunately not correct. The return type is always a
bool
(it has to be: returning anything other than abool
from__bool__()
will generate aTypeError
). In this case, a side effect ofresolve()
is to resolve the_indicator
to its bool value.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally agree. I think since the type inferring is static, the linter is not able to detect that the
_available
will be always abool
after theresolve()
call.