From 96cb9d98bd12937a8a128a097cd40ab5a922bee0 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 11 Dec 2025 13:57:37 +0100 Subject: [PATCH 01/25] Add Knitro solver support - Add Knitro detection to available_solvers list - Implement Knitro solver class with MPS/LP file support - Add solver capabilities for Knitro (quadratic, LP names, no solution file) - Add tests for Knitro solver functionality - Map Knitro status codes to linopy Status system --- linopy/solver_capabilities.py | 11 ++ linopy/solvers.py | 220 ++++++++++++++++++++++++++++++++++ test/test_solvers.py | 38 ++++++ 3 files changed, 269 insertions(+) diff --git a/linopy/solver_capabilities.py b/linopy/solver_capabilities.py index 721cc34d..7a9910a4 100644 --- a/linopy/solver_capabilities.py +++ b/linopy/solver_capabilities.py @@ -111,6 +111,17 @@ def supports(self, feature: SolverFeature) -> bool: } ), ), + "knitro": SolverInfo( + name="knitro", + display_name="Artelys Knitro", + features=frozenset( + { + SolverFeature.QUADRATIC_OBJECTIVE, + SolverFeature.LP_FILE_NAMES, + SolverFeature.SOLUTION_FILE_NOT_NEEDED, + } + ), + ), "scip": SolverInfo( name="scip", display_name="SCIP", diff --git a/linopy/solvers.py b/linopy/solvers.py index f0f732fe..23345b6d 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -96,6 +96,11 @@ available_solvers.append("xpress") +with contextlib.suppress(ModuleNotFoundError, ImportError): + import knitro + + available_solvers.append("knitro") + # xpress.Namespaces was added in xpress 9.6 try: from xpress import Namespaces as xpress_Namespaces @@ -160,6 +165,7 @@ class SolverName(enum.Enum): Gurobi = "gurobi" SCIP = "scip" Xpress = "xpress" + Knitro = "knitro" Mosek = "mosek" COPT = "copt" MindOpt = "mindopt" @@ -1625,6 +1631,220 @@ def get_solver_solution() -> Solution: return Result(status, solution, m) +class Knitro(Solver[None]): + """ + Solver subclass for the Knitro solver. + + Knitro is a powerful nonlinear optimization solver that also handles + linear and quadratic problems efficiently. + + For more information on solver options, see + https://www.artelys.com/app/docs/knitro/3_referenceManual/knitroPythonReference.html + + Attributes + ---------- + **solver_options + options for the given solver + """ + + def __init__( + self, + **solver_options: Any, + ) -> None: + super().__init__(**solver_options) + + def solve_problem_from_model( + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, + ) -> Result: + msg = "Direct API not implemented for Knitro" + raise NotImplementedError(msg) + + def solve_problem_from_file( + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + ) -> Result: + """ + Solve a linear problem from a problem file using the Knitro solver. + + This function reads the linear problem file and passes it to the Knitro + solver. If the solution is successful it returns variable solutions and + constraint dual values. + + Parameters + ---------- + problem_fn : Path + Path to the problem file. + solution_fn : Path, optional + Path to the solution file. + log_fn : Path, optional + Path to the log file. + warmstart_fn : Path, optional + Path to the warmstart file. + basis_fn : Path, optional + Path to the basis file. + env : None, optional + Environment for the solver + + Returns + ------- + Result + """ + # Knitro status codes: https://www.artelys.com/app/docs/knitro/3_referenceManual/knitroReturnCodes.html + CONDITION_MAP = { + 0: "optimal", + -100: "feasible_point", + -101: "infeasible", + -102: "feasible_point", + -200: "unbounded", + -201: "infeasible_or_unbounded", + -202: "iteration_limit", + -203: "time_limit", + -204: "function_evaluation_limit", + -300: "unbounded", + -400: "iteration_limit", + -401: "time_limit", + -410: "mip_node_limit", + -411: "mip_solution_limit", + } + + io_api = read_io_api_from_problem_file(problem_fn) + sense = read_sense_from_problem_file(problem_fn) + + try: + kc = knitro.KN_new() + except Exception as e: + msg = f"Failed to create Knitro solver instance: {e}" + raise RuntimeError(msg) + + try: + # Read the problem file + ret = knitro.KN_load_mps_file(kc, path_to_string(problem_fn)) + if ret != 0: + msg = f"Failed to load problem file: Knitro error code {ret}" + raise RuntimeError(msg) + + # Set log file if specified + if log_fn is not None: + knitro.KN_set_param_by_name(kc, "outlev", 6) # Enable detailed output + knitro.KN_set_param_by_name(kc, "outmode", path_to_string(log_fn)) + + # Set solver options + for k, v in self.solver_options.items(): + if isinstance(v, int | float): + knitro.KN_set_param_by_name(kc, k, v) + elif isinstance(v, str): + knitro.KN_set_char_param_by_name(kc, k, v) + + # Load warmstart if provided + if warmstart_fn is not None: + try: + # Knitro doesn't have direct basis loading, but we can set initial values + logger.info( + "Warmstart not directly supported by Knitro LP interface" + ) + except Exception as err: + logger.info("Warmstart could not be loaded. Error: %s", err) + + # Solve the problem + ret = knitro.KN_solve(kc) + + # Get termination condition + termination_condition = CONDITION_MAP.get(ret, "unknown") + status = Status.from_termination_condition(termination_condition) + status.legacy_status = ret + + def get_solver_solution() -> Solution: + # Get objective value + try: + obj_ptr = knitro.KN_get_obj_value(kc) + objective = obj_ptr[0] if obj_ptr[1] == 0 else np.nan + except Exception: + objective = np.nan + + # Get variable values + try: + n_vars = knitro.KN_get_number_vars(kc) + x_ptr = knitro.KN_get_var_primal_values(kc, n_vars) + if x_ptr[1] == 0: + # Get variable names + var_names = [] + for i in range(n_vars): + name_ptr = knitro.KN_get_var_name(kc, i) + if name_ptr[1] == 0: + var_names.append(name_ptr[0]) + else: + var_names.append(f"x{i}") + sol = pd.Series(x_ptr[0], index=var_names, dtype=float) + else: + sol = pd.Series(dtype=float) + except Exception as e: + logger.warning(f"Could not extract primal solution: {e}") + sol = pd.Series(dtype=float) + + # Get dual values (constraint multipliers) + try: + n_cons = knitro.KN_get_number_cons(kc) + if n_cons > 0: + dual_ptr = knitro.KN_get_con_dual_values(kc, n_cons) + if dual_ptr[1] == 0: + # Get constraint names + con_names = [] + for i in range(n_cons): + name_ptr = knitro.KN_get_con_name(kc, i) + if name_ptr[1] == 0: + con_names.append(name_ptr[0]) + else: + con_names.append(f"c{i}") + dual = pd.Series(dual_ptr[0], index=con_names, dtype=float) + else: + dual = pd.Series(dtype=float) + else: + dual = pd.Series(dtype=float) + except Exception as e: + logger.warning(f"Could not extract dual solution: {e}") + dual = pd.Series(dtype=float) + + return Solution(sol, dual, objective) + + solution = self.safe_get_solution(status=status, func=get_solver_solution) + solution = maybe_adjust_objective_sign(solution, io_api, sense) + + # Save basis if requested + if basis_fn is not None: + try: + # Knitro doesn't have direct basis export for LP files + logger.info( + "Basis export not directly supported by Knitro LP interface" + ) + except Exception as err: + logger.info("No basis stored. Error: %s", err) + + # Save solution if requested + if solution_fn is not None: + try: + knitro.KN_write_mps_file(kc, path_to_string(solution_fn)) + except Exception as err: + logger.info("Could not write solution file. Error: %s", err) + + return Result(status, solution, kc) + + finally: + # Clean up Knitro context + knitro.KN_free(kc) + + mosek_bas_re = re.compile(r" (XL|XU)\s+([^ \t]+)\s+([^ \t]+)| (LL|UL|BS)\s+([^ \t]+)") diff --git a/test/test_solvers.py b/test/test_solvers.py index 129c1e0b..561eb69f 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -67,6 +67,44 @@ def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None: assert result.solution.objective == 30.0 +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) +def test_knitro_solver(tmp_path: Path) -> None: + """Test Knitro solver with a simple MPS problem.""" + knitro = solvers.Knitro() + + mps_file = tmp_path / "problem.mps" + mps_file.write_text(free_mps_problem) + sol_file = tmp_path / "solution.sol" + + result = knitro.solve_problem(problem_fn=mps_file, solution_fn=sol_file) + + assert result.status.is_ok + assert result.solution.objective == 30.0 + + +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) +def test_knitro_solver_with_options(tmp_path: Path) -> None: + """Test Knitro solver with custom options.""" + # Set some common Knitro options + knitro = solvers.Knitro(maxit=100, feastol=1e-6) + + mps_file = tmp_path / "problem.mps" + mps_file.write_text(free_mps_problem) + sol_file = tmp_path / "solution.sol" + log_file = tmp_path / "knitro.log" + + result = knitro.solve_problem( + problem_fn=mps_file, solution_fn=sol_file, log_fn=log_file + ) + + assert result.status.is_ok + assert log_file.exists() + + @pytest.mark.skipif( "gurobi" not in set(solvers.available_solvers), reason="Gurobi is not installed" ) From 2337f1604b2cc15a4cbb30195eb35932658d9daa Mon Sep 17 00:00:00 2001 From: Fabian Date: Sun, 14 Dec 2025 12:42:44 +0100 Subject: [PATCH 02/25] Fix Knitro solver integration --- linopy/solvers.py | 195 +++++++++++++++++++++++++++++++++------------- 1 file changed, 141 insertions(+), 54 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index 23345b6d..be39f115 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -96,11 +96,6 @@ available_solvers.append("xpress") -with contextlib.suppress(ModuleNotFoundError, ImportError): - import knitro - - available_solvers.append("knitro") - # xpress.Namespaces was added in xpress 9.6 try: from xpress import Namespaces as xpress_Namespaces @@ -112,6 +107,14 @@ class xpress_Namespaces: # type: ignore[no-redef] SET = 3 +with contextlib.suppress(ModuleNotFoundError, ImportError): + import knitro + + with contextlib.suppress(Exception): + kc = knitro.KN_new() + knitro.KN_free(kc) + available_solvers.append("knitro") + with contextlib.suppress(ModuleNotFoundError): import mosek @@ -1702,26 +1705,67 @@ def solve_problem_from_file( Result """ # Knitro status codes: https://www.artelys.com/app/docs/knitro/3_referenceManual/knitroReturnCodes.html - CONDITION_MAP = { - 0: "optimal", - -100: "feasible_point", - -101: "infeasible", - -102: "feasible_point", - -200: "unbounded", - -201: "infeasible_or_unbounded", - -202: "iteration_limit", - -203: "time_limit", - -204: "function_evaluation_limit", - -300: "unbounded", - -400: "iteration_limit", - -401: "time_limit", - -410: "mip_node_limit", - -411: "mip_solution_limit", + CONDITION_MAP: dict[int, TerminationCondition] = { + 0: TerminationCondition.optimal, + -100: TerminationCondition.suboptimal, + -101: TerminationCondition.infeasible, + -102: TerminationCondition.suboptimal, + -200: TerminationCondition.unbounded, + -201: TerminationCondition.infeasible_or_unbounded, + -202: TerminationCondition.iteration_limit, + -203: TerminationCondition.time_limit, + -204: TerminationCondition.terminated_by_limit, + -300: TerminationCondition.unbounded, + -400: TerminationCondition.iteration_limit, + -401: TerminationCondition.time_limit, + -410: TerminationCondition.terminated_by_limit, + -411: TerminationCondition.terminated_by_limit, } io_api = read_io_api_from_problem_file(problem_fn) sense = read_sense_from_problem_file(problem_fn) + def unpack_value_and_rc(obj: Any) -> tuple[Any, int]: + if isinstance(obj, tuple) and len(obj) == 2: + return obj[0], int(obj[1]) + return obj, 0 + + def set_param_by_name(kc: Any, name: str, value: Any) -> None: + if isinstance(value, bool): + value = int(value) + + if isinstance(value, int): + setter = getattr(knitro, "KN_set_int_param_by_name", None) or getattr( + knitro, "KN_set_param_by_name", None + ) + elif isinstance(value, float): + setter = getattr( + knitro, "KN_set_double_param_by_name", None + ) or getattr(knitro, "KN_set_param_by_name", None) + elif isinstance(value, str): + setter = getattr(knitro, "KN_set_char_param_by_name", None) + else: + logger.warning( + "Ignoring unsupported Knitro option %r=%r (type %s)", + name, + value, + type(value).__name__, + ) + return + + if setter is None: + logger.warning( + "Could not set Knitro option %r; required setter not available in knitro Python API.", + name, + ) + return + + try: + setter(kc, name, value) + except Exception as e: + logger.warning("Could not set Knitro option %r=%r: %s", name, value, e) + + kc = None try: kc = knitro.KN_new() except Exception as e: @@ -1730,22 +1774,36 @@ def solve_problem_from_file( try: # Read the problem file - ret = knitro.KN_load_mps_file(kc, path_to_string(problem_fn)) + problem_path = path_to_string(problem_fn) + if io_api is not None and io_api.startswith("lp"): + load_fn = getattr(knitro, "KN_load_lp_file", None) + if load_fn is None: + msg = "Knitro Python API does not support loading LP files (missing KN_load_lp_file)." + raise RuntimeError(msg) + else: + load_fn = getattr(knitro, "KN_load_mps_file", None) + if load_fn is None: + msg = "Knitro Python API does not support loading MPS files (missing KN_load_mps_file)." + raise RuntimeError(msg) + + ret = int(load_fn(kc, problem_path)) if ret != 0: msg = f"Failed to load problem file: Knitro error code {ret}" raise RuntimeError(msg) # Set log file if specified if log_fn is not None: - knitro.KN_set_param_by_name(kc, "outlev", 6) # Enable detailed output - knitro.KN_set_param_by_name(kc, "outmode", path_to_string(log_fn)) + log_fn.parent.mkdir(parents=True, exist_ok=True) + with open(path_to_string(log_fn), "w", encoding="utf-8") as f: + f.write("linopy: knitro log\n") + + set_param_by_name(kc, "outlev", 6) + set_param_by_name(kc, "outmode", 1) + set_param_by_name(kc, "outname", path_to_string(log_fn)) # Set solver options for k, v in self.solver_options.items(): - if isinstance(v, int | float): - knitro.KN_set_param_by_name(kc, k, v) - elif isinstance(v, str): - knitro.KN_set_char_param_by_name(kc, k, v) + set_param_by_name(kc, k, v) # Load warmstart if provided if warmstart_fn is not None: @@ -1758,35 +1816,47 @@ def solve_problem_from_file( logger.info("Warmstart could not be loaded. Error: %s", err) # Solve the problem - ret = knitro.KN_solve(kc) + ret = int(knitro.KN_solve(kc)) # Get termination condition - termination_condition = CONDITION_MAP.get(ret, "unknown") + if ret in CONDITION_MAP: + termination_condition = CONDITION_MAP[ret] + elif ret > 0: + termination_condition = TerminationCondition.internal_solver_error + else: + termination_condition = TerminationCondition.unknown + status = Status.from_termination_condition(termination_condition) - status.legacy_status = ret + status.legacy_status = str(ret) def get_solver_solution() -> Solution: # Get objective value try: - obj_ptr = knitro.KN_get_obj_value(kc) - objective = obj_ptr[0] if obj_ptr[1] == 0 else np.nan + obj_val, obj_rc = unpack_value_and_rc(knitro.KN_get_obj_value(kc)) + objective = float(obj_val) if obj_rc == 0 else np.nan except Exception: objective = np.nan # Get variable values try: - n_vars = knitro.KN_get_number_vars(kc) - x_ptr = knitro.KN_get_var_primal_values(kc, n_vars) - if x_ptr[1] == 0: + n_vars_val, n_vars_rc = unpack_value_and_rc( + knitro.KN_get_number_vars(kc) + ) + n_vars = int(n_vars_val) if n_vars_rc == 0 else 0 + + x_val, x_rc = unpack_value_and_rc( + knitro.KN_get_var_primal_values(kc, n_vars) + ) + if x_rc == 0 and n_vars > 0: # Get variable names var_names = [] for i in range(n_vars): - name_ptr = knitro.KN_get_var_name(kc, i) - if name_ptr[1] == 0: - var_names.append(name_ptr[0]) - else: - var_names.append(f"x{i}") - sol = pd.Series(x_ptr[0], index=var_names, dtype=float) + name_val, name_rc = unpack_value_and_rc( + knitro.KN_get_var_name(kc, i) + ) + var_names.append(str(name_val) if name_rc == 0 else f"x{i}") + + sol = pd.Series(x_val, index=var_names, dtype=float) else: sol = pd.Series(dtype=float) except Exception as e: @@ -1795,19 +1865,27 @@ def get_solver_solution() -> Solution: # Get dual values (constraint multipliers) try: - n_cons = knitro.KN_get_number_cons(kc) + n_cons_val, n_cons_rc = unpack_value_and_rc( + knitro.KN_get_number_cons(kc) + ) + n_cons = int(n_cons_val) if n_cons_rc == 0 else 0 + if n_cons > 0: - dual_ptr = knitro.KN_get_con_dual_values(kc, n_cons) - if dual_ptr[1] == 0: + dual_val, dual_rc = unpack_value_and_rc( + knitro.KN_get_con_dual_values(kc, n_cons) + ) + if dual_rc == 0: # Get constraint names con_names = [] for i in range(n_cons): - name_ptr = knitro.KN_get_con_name(kc, i) - if name_ptr[1] == 0: - con_names.append(name_ptr[0]) - else: - con_names.append(f"c{i}") - dual = pd.Series(dual_ptr[0], index=con_names, dtype=float) + name_val, name_rc = unpack_value_and_rc( + knitro.KN_get_con_name(kc, i) + ) + con_names.append( + str(name_val) if name_rc == 0 else f"c{i}" + ) + + dual = pd.Series(dual_val, index=con_names, dtype=float) else: dual = pd.Series(dtype=float) else: @@ -1834,15 +1912,24 @@ def get_solver_solution() -> Solution: # Save solution if requested if solution_fn is not None: try: - knitro.KN_write_mps_file(kc, path_to_string(solution_fn)) + write_sol = getattr(knitro, "KN_write_sol_file", None) + if write_sol is None: + logger.info( + "Solution export not supported by Knitro interface; ignoring solution_fn=%s", + solution_fn, + ) + else: + solution_fn.parent.mkdir(parents=True, exist_ok=True) + write_sol(kc, path_to_string(solution_fn)) except Exception as err: logger.info("Could not write solution file. Error: %s", err) - return Result(status, solution, kc) + return Result(status, solution) finally: - # Clean up Knitro context - knitro.KN_free(kc) + if kc is not None: + with contextlib.suppress(Exception): + knitro.KN_free(kc) mosek_bas_re = re.compile(r" (XL|XU)\s+([^ \t]+)\s+([^ \t]+)| (LL|UL|BS)\s+([^ \t]+)") From 241c6e9341c2b412829d6171633e3df138eac65b Mon Sep 17 00:00:00 2001 From: Fabian Date: Sun, 14 Dec 2025 12:51:45 +0100 Subject: [PATCH 03/25] Document Knitro and improve file loading --- doc/release_notes.rst | 1 + linopy/solvers.py | 43 ++++++++++++++++++++++++++++++++++--------- pyproject.toml | 1 + 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 5ca5ecc7..c1b5a0e2 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -4,6 +4,7 @@ Release Notes .. Upcoming Version * Fix compatibility for xpress versions below 9.6 (regression) +* Add support for the Artelys Knitro solver (via the Knitro Python API) * Performance: Up to 50x faster ``repr()`` for variables/constraints via O(log n) label lookup and direct numpy indexing * Performance: Up to 46x faster ``ncons`` property by replacing ``.flat.labels.unique()`` with direct counting diff --git a/linopy/solvers.py b/linopy/solvers.py index be39f115..c32ff90d 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1775,18 +1775,43 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: try: # Read the problem file problem_path = path_to_string(problem_fn) - if io_api is not None and io_api.startswith("lp"): - load_fn = getattr(knitro, "KN_load_lp_file", None) - if load_fn is None: - msg = "Knitro Python API does not support loading LP files (missing KN_load_lp_file)." - raise RuntimeError(msg) + suffix = problem_fn.suffix.lower() + if suffix == ".lp": + candidate_loaders = [ + "KN_load_lp_file", + "KN_load_file", + "KN_load_mps_file", + ] + elif suffix == ".mps": + candidate_loaders = [ + "KN_load_mps_file", + "KN_load_file", + "KN_load_lp_file", + ] else: - load_fn = getattr(knitro, "KN_load_mps_file", None) + candidate_loaders = [ + "KN_load_file", + "KN_load_mps_file", + "KN_load_lp_file", + ] + + last_ret: int | None = None + for candidate in candidate_loaders: + load_fn = getattr(knitro, candidate, None) if load_fn is None: - msg = "Knitro Python API does not support loading MPS files (missing KN_load_mps_file)." - raise RuntimeError(msg) + continue + ret_val, _ret_rc = unpack_value_and_rc(load_fn(kc, problem_path)) + last_ret = int(ret_val) + if last_ret == 0: + break + else: + msg = ( + "Knitro Python API does not expose a suitable file loader for " + f"{suffix or 'unknown'} problems (tried: {', '.join(candidate_loaders)})." + ) + raise RuntimeError(msg) - ret = int(load_fn(kc, problem_path)) + ret = 0 if last_ret is None else last_ret if ret != 0: msg = f"Failed to load problem file: Knitro error code {ret}" raise RuntimeError(msg) diff --git a/pyproject.toml b/pyproject.toml index b5105230..18c03f66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ solvers = [ "coptpy!=7.2.1", "xpress; platform_system != 'Darwin' and python_version < '3.11'", "pyscipopt; platform_system != 'Darwin'", + "knitro" ] [tool.setuptools.packages.find] From 2b4229165b8ba884efd1ef85f2a3119635f4bb0d Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Fri, 30 Jan 2026 18:26:21 +0100 Subject: [PATCH 04/25] code: add check to solve mypy issue --- test/test_solvers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_solvers.py b/test/test_solvers.py index 9a08a4d1..283610e7 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -85,6 +85,7 @@ def test_knitro_solver(tmp_path: Path) -> None: result = knitro.solve_problem(problem_fn=mps_file, solution_fn=sol_file) assert result.status.is_ok + assert result.solution is not None assert result.solution.objective == 30.0 From 77b9c8119215c1997eee55004b0a0a793748ee6c Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Mon, 2 Feb 2026 12:20:59 +0100 Subject: [PATCH 05/25] code: remove unnecessary candidate loaders --- linopy/solvers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index e9e62787..fad52a47 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1891,13 +1891,11 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: candidate_loaders = [ "KN_load_lp_file", "KN_load_file", - "KN_load_mps_file", ] elif suffix == ".mps": candidate_loaders = [ "KN_load_mps_file", "KN_load_file", - "KN_load_lp_file", ] else: candidate_loaders = [ From 8dbfdbe58a402cfacc63f1cfe855fbfb1c11cde0 Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Mon, 2 Feb 2026 12:26:47 +0100 Subject: [PATCH 06/25] code: remove unnecessary candidate loaders --- linopy/solvers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index fad52a47..c6cbae6e 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1889,19 +1889,19 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: suffix = problem_fn.suffix.lower() if suffix == ".lp": candidate_loaders = [ - "KN_load_lp_file", - "KN_load_file", + "KN_load_lp", + "KN_read_problem" ] elif suffix == ".mps": candidate_loaders = [ "KN_load_mps_file", - "KN_load_file", + "KN_read_problem" ] else: candidate_loaders = [ - "KN_load_file", + "KN_read_problem", "KN_load_mps_file", - "KN_load_lp_file", + "KN_load_lp", ] last_ret: int | None = None From bfd89c8ebafafb4f495eab00d47ac28110fb41c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:27:51 +0000 Subject: [PATCH 07/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linopy/solvers.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index c6cbae6e..57afdf9a 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1888,15 +1888,9 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: problem_path = path_to_string(problem_fn) suffix = problem_fn.suffix.lower() if suffix == ".lp": - candidate_loaders = [ - "KN_load_lp", - "KN_read_problem" - ] + candidate_loaders = ["KN_load_lp", "KN_read_problem"] elif suffix == ".mps": - candidate_loaders = [ - "KN_load_mps_file", - "KN_read_problem" - ] + candidate_loaders = ["KN_load_mps_file", "KN_read_problem"] else: candidate_loaders = [ "KN_read_problem", From 634f1868c8a7b67cee54c53457bd591163a4a45f Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Mon, 2 Feb 2026 12:28:34 +0100 Subject: [PATCH 08/25] code: use just KN_read_problem for lp --- linopy/solvers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index c6cbae6e..cbf00fe1 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1889,7 +1889,6 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: suffix = problem_fn.suffix.lower() if suffix == ".lp": candidate_loaders = [ - "KN_load_lp", "KN_read_problem" ] elif suffix == ".mps": From 3f291a66a2dfdf88565b434101d210cb058ba443 Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Mon, 2 Feb 2026 12:33:20 +0100 Subject: [PATCH 09/25] add read_options --- linopy/solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index 523fc1d9..af1634a2 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1903,7 +1903,7 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: load_fn = getattr(knitro, candidate, None) if load_fn is None: continue - ret_val, _ret_rc = unpack_value_and_rc(load_fn(kc, problem_path)) + ret_val, _ret_rc = unpack_value_and_rc(load_fn(kc, problem_path, read_options=[])) last_ret = int(ret_val) if last_ret == 0: break From 3b7f85badd55ad5cd54daa3cd93ddb3f3ba780fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:35:56 +0000 Subject: [PATCH 10/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linopy/solvers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index af1634a2..42a67e10 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1903,7 +1903,9 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: load_fn = getattr(knitro, candidate, None) if load_fn is None: continue - ret_val, _ret_rc = unpack_value_and_rc(load_fn(kc, problem_path, read_options=[])) + ret_val, _ret_rc = unpack_value_and_rc( + load_fn(kc, problem_path, read_options=[]) + ) last_ret = int(ret_val) if last_ret == 0: break From 37ecec706f1831494c86561e888725bad608587d Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Mon, 2 Feb 2026 16:35:15 +0100 Subject: [PATCH 11/25] code: update KN_read_problem calling --- linopy/solvers.py | 623 +++++++++++++++++++++++----------------------- 1 file changed, 315 insertions(+), 308 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index af1634a2..c6d58b9d 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -146,7 +146,6 @@ def _target() -> None: if sub.run([which, "glpsol"], stdout=sub.DEVNULL, stderr=sub.STDOUT).returncode == 0: available_solvers.append("glpk") - if sub.run([which, "cbc"], stdout=sub.DEVNULL, stderr=sub.STDOUT).returncode == 0: available_solvers.append("cbc") @@ -175,7 +174,6 @@ class xpress_Namespaces: # type: ignore[no-redef] COLUMN = 2 SET = 3 - with contextlib.suppress(ModuleNotFoundError, ImportError): import knitro @@ -219,11 +217,9 @@ class xpress_Namespaces: # type: ignore[no-redef] except ImportError: pass - quadratic_solvers = [s for s in QUADRATIC_SOLVERS if s in available_solvers] logger = logging.getLogger(__name__) - io_structure = dict( lp_file={ "gurobi", @@ -283,7 +279,7 @@ def read_io_api_from_problem_file(problem_fn: Path | str) -> str: def maybe_adjust_objective_sign( - solution: Solution, io_api: str | None, sense: str | None + solution: Solution, io_api: str | None, sense: str | None ) -> Solution: if sense == "min": return solution @@ -307,8 +303,8 @@ class Solver(ABC, Generic[EnvType]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: self.solver_options = solver_options @@ -318,7 +314,7 @@ def __init__( raise ImportError(msg) def safe_get_solution( - self, status: Status, func: Callable[[], Solution] + self, status: Status, func: Callable[[], Solution] ) -> Solution: """ Get solution from function call, if status is unknown still try to run it. @@ -338,14 +334,14 @@ def safe_get_solution( @abstractmethod def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: EnvType | None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: EnvType | None = None, + explicit_coordinate_names: bool = False, ) -> Result: """ Abstract method to solve a linear problem from a model. @@ -358,13 +354,13 @@ def solve_problem_from_model( @abstractmethod def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: EnvType | None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: EnvType | None = None, ) -> Result: """ Abstract method to solve a linear problem from a problem file. @@ -376,15 +372,15 @@ def solve_problem_from_file( pass def solve_problem( - self, - model: Model | None = None, - problem_fn: Path | None = None, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: EnvType | None = None, - explicit_coordinate_names: bool = False, + self, + model: Model | None = None, + problem_fn: Path | None = None, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: EnvType | None = None, + explicit_coordinate_names: bool = False, ) -> Result: """ Solve a linear problem either from a model or a problem file. @@ -435,32 +431,32 @@ class CBC(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for CBC" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the CBC solver. @@ -510,10 +506,10 @@ def solve_problem_from_file( if self.solver_options: command += ( - " ".join( - "-" + " ".join([k, str(v)]) for k, v in self.solver_options.items() - ) - + " " + " ".join( + "-" + " ".join([k, str(v)]) for k, v in self.solver_options.items() + ) + + " " ) command += f"-solve -solu {solution_fn} " @@ -621,32 +617,32 @@ class GLPK(Solver[None]): """ def __init( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for GLPK" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the glpk solver. @@ -709,10 +705,10 @@ def solve_problem_from_file( command += f"-w {basis_fn} " if self.solver_options: command += ( - " ".join( - "--" + " ".join([k, str(v)]) for k, v in self.solver_options.items() - ) - + " " + " ".join( + "--" + " ".join([k, str(v)]) for k, v in self.solver_options.items() + ) + + " " ) command = command.strip() @@ -801,20 +797,20 @@ class Highs(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: """ Solve a linear problem directly from a linopy model using the Highs solver. @@ -871,13 +867,13 @@ def solve_problem_from_model( ) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the Highs solver. @@ -921,9 +917,9 @@ def solve_problem_from_file( ) def _set_solver_params( - self, - highs_solver: highspy.Highs, - log_fn: Path | None = None, + self, + highs_solver: highspy.Highs, + log_fn: Path | None = None, ) -> None: if log_fn is not None: self.solver_options["log_file"] = path_to_string(log_fn) @@ -933,14 +929,14 @@ def _set_solver_params( highs_solver.setOptionValue(k, v) def _solve( - self, - h: highspy.Highs, - solution_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - model: Model | None = None, - io_api: str | None = None, - sense: str | None = None, + self, + h: highspy.Highs, + solution_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + model: Model | None = None, + io_api: str | None = None, + sense: str | None = None, ) -> Result: """ Solve a linear problem from a Highs object. @@ -1042,20 +1038,20 @@ class Gurobi(Solver["gurobipy.Env | dict[str, Any] | None"]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: gurobipy.Env | dict[str, Any] | None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: gurobipy.Env | dict[str, Any] | None = None, + explicit_coordinate_names: bool = False, ) -> Result: """ Solve a linear problem directly from a linopy model using the Gurobi solver. @@ -1106,13 +1102,13 @@ def solve_problem_from_model( ) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: gurobipy.Env | dict[str, Any] | None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: gurobipy.Env | dict[str, Any] | None = None, ) -> Result: """ Solve a linear problem from a problem file using the Gurobi solver. @@ -1163,14 +1159,14 @@ def solve_problem_from_file( ) def _solve( - self, - m: gurobipy.Model, - solution_fn: Path | None, - log_fn: Path | None, - warmstart_fn: Path | None, - basis_fn: Path | None, - io_api: str | None, - sense: str | None, + self, + m: gurobipy.Model, + solution_fn: Path | None, + log_fn: Path | None, + warmstart_fn: Path | None, + basis_fn: Path | None, + io_api: str | None, + sense: str | None, ) -> Result: """ Solve a linear problem from a Gurobi object. @@ -1281,32 +1277,32 @@ class Cplex(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for Cplex" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the cplex solver. @@ -1433,32 +1429,32 @@ class SCIP(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for SCIP" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the scip solver. @@ -1589,32 +1585,32 @@ class Xpress(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for Xpress" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the Xpress solver. @@ -1762,32 +1758,32 @@ class Knitro(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for Knitro" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the Knitro solver. @@ -1888,36 +1884,47 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: problem_path = path_to_string(problem_fn) suffix = problem_fn.suffix.lower() if suffix == ".lp": - candidate_loaders = ["KN_read_problem"] + read_options = "l" elif suffix == ".mps": - candidate_loaders = ["KN_load_mps_file", "KN_read_problem"] - else: - candidate_loaders = [ - "KN_read_problem", - "KN_load_mps_file", - "KN_load_lp", - ] - - last_ret: int | None = None - for candidate in candidate_loaders: - load_fn = getattr(knitro, candidate, None) - if load_fn is None: - continue - ret_val, _ret_rc = unpack_value_and_rc(load_fn(kc, problem_path, read_options=[])) - last_ret = int(ret_val) - if last_ret == 0: - break + read_options = "m" else: + read_options = "" + + try: + knitro.KN_read_problem(kc, problem_path, read_options=read_options) + except AttributeError as attr_err: msg = ( - "Knitro Python API does not expose a suitable file loader for " - f"{suffix or 'unknown'} problems (tried: {', '.join(candidate_loaders)})." + "Knitro Python API method not found. " + f"Possible API changes or incorrect Knitro installation. " + f"Original error: {attr_err}" ) - raise RuntimeError(msg) - - ret = 0 if last_ret is None else last_ret - if ret != 0: - msg = f"Failed to load problem file: Knitro error code {ret}" - raise RuntimeError(msg) + raise RuntimeError(msg) from attr_err + except TypeError as type_err: + msg = ( + "Invalid argument types passed to KN_read_problem. " + f"Check problem_path and read_options types. " + f"Original error: {type_err}" + ) + raise RuntimeError(msg) from type_err + except FileNotFoundError as file_err: + msg = ( + f"Optimization problem file not found: {problem_path}. " + "Please verify the file path and existence." + ) + raise RuntimeError(msg) from file_err + except PermissionError as perm_err: + msg = ( + f"Permission denied when accessing: {problem_path}. " + "Check file permissions and access rights." + ) + raise RuntimeError(msg) from perm_err + except Exception as unexpected_err: + msg = ( + "Unexpected error occurred while loading Knitro problem. " + f"File: {problem_path}, " + f"Error details: {unexpected_err}" + ) + raise RuntimeError(msg) from unexpected_err # Set log file if specified if log_fn is not None: @@ -2084,20 +2091,20 @@ class Mosek(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: """ Solve a linear problem directly from a linopy model using the MOSEK solver. @@ -2147,13 +2154,13 @@ def solve_problem_from_model( ) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the MOSEK solver. Both mps and @@ -2206,14 +2213,14 @@ def solve_problem_from_file( ) def _solve( - self, - m: mosek.Task, - solution_fn: Path | None, - log_fn: Path | None, - warmstart_fn: Path | None, - basis_fn: Path | None, - io_api: str | None, - sense: str | None, + self, + m: mosek.Task, + solution_fn: Path | None, + log_fn: Path | None, + warmstart_fn: Path | None, + basis_fn: Path | None, + io_api: str | None, + sense: str | None, ) -> Result: """ Solve a linear problem from a Mosek task object. @@ -2418,32 +2425,32 @@ class COPT(Solver[None]): """ def __init( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for COPT" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the COPT solver. @@ -2559,32 +2566,32 @@ class MindOpt(Solver[None]): """ def __init( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, + explicit_coordinate_names: bool = False, ) -> Result: msg = "Direct API not implemented for MindOpt" raise NotImplementedError(msg) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: None = None, ) -> Result: """ Solve a linear problem from a problem file using the MindOpt solver. @@ -2693,8 +2700,8 @@ class PIPS(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) msg = "The PIPS solver interface is not yet implemented." @@ -2722,19 +2729,19 @@ class cuPDLPx(Solver[None]): """ def __init__( - self, - **solver_options: Any, + self, + **solver_options: Any, ) -> None: super().__init__(**solver_options) def solve_problem_from_file( - self, - problem_fn: Path, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: EnvType | None = None, + self, + problem_fn: Path, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: EnvType | None = None, ) -> Result: """ Solve a linear problem from a problem file using the solver cuPDLPx. @@ -2784,14 +2791,14 @@ def solve_problem_from_file( ) def solve_problem_from_model( - self, - model: Model, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - env: EnvType | None = None, - explicit_coordinate_names: bool = False, + self, + model: Model, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + env: EnvType | None = None, + explicit_coordinate_names: bool = False, ) -> Result: """ Solve a linear problem directly from a linopy model using the solver cuPDLPx. @@ -2838,15 +2845,15 @@ def solve_problem_from_model( ) def _solve( - self, - cu_model: cupdlpx.Model, - l_model: Model | None = None, - solution_fn: Path | None = None, - log_fn: Path | None = None, - warmstart_fn: Path | None = None, - basis_fn: Path | None = None, - io_api: str | None = None, - sense: str | None = None, + self, + cu_model: cupdlpx.Model, + l_model: Model | None = None, + solution_fn: Path | None = None, + log_fn: Path | None = None, + warmstart_fn: Path | None = None, + basis_fn: Path | None = None, + io_api: str | None = None, + sense: str | None = None, ) -> Result: """ Solve a linear problem from a cupdlpx.Model object. From a3a5015fe76b04e29c75e328e9e8ef78a4f2e0bb Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Tue, 3 Feb 2026 11:24:49 +0100 Subject: [PATCH 12/25] code: new changes from Daniele Lerede --- linopy/solvers.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index 79a64251..da56524e 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1936,10 +1936,6 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: with open(path_to_string(log_fn), "w", encoding="utf-8") as f: f.write("linopy: knitro log\n") - set_param_by_name(kc, "outlev", 6) - set_param_by_name(kc, "outmode", 1) - set_param_by_name(kc, "outname", path_to_string(log_fn)) - # Set solver options for k, v in self.solver_options.items(): set_param_by_name(kc, k, v) @@ -1984,7 +1980,7 @@ def get_solver_solution() -> Solution: n_vars = int(n_vars_val) if n_vars_rc == 0 else 0 x_val, x_rc = unpack_value_and_rc( - knitro.KN_get_var_primal_values(kc, n_vars) + knitro.KN_get_var_primal_values(kc, n_vars-1) ) if x_rc == 0 and n_vars > 0: # Get variable names @@ -2011,7 +2007,7 @@ def get_solver_solution() -> Solution: if n_cons > 0: dual_val, dual_rc = unpack_value_and_rc( - knitro.KN_get_con_dual_values(kc, n_cons) + knitro.KN_get_con_dual_values(kc, n_cons-1) ) if dual_rc == 0: # Get constraint names From 65799de7a696bfdec054d4a3223c995ad6bbf2fb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:25:03 +0000 Subject: [PATCH 13/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linopy/solvers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index da56524e..c417f37d 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1980,7 +1980,7 @@ def get_solver_solution() -> Solution: n_vars = int(n_vars_val) if n_vars_rc == 0 else 0 x_val, x_rc = unpack_value_and_rc( - knitro.KN_get_var_primal_values(kc, n_vars-1) + knitro.KN_get_var_primal_values(kc, n_vars - 1) ) if x_rc == 0 and n_vars > 0: # Get variable names @@ -2007,7 +2007,7 @@ def get_solver_solution() -> Solution: if n_cons > 0: dual_val, dual_rc = unpack_value_and_rc( - knitro.KN_get_con_dual_values(kc, n_cons-1) + knitro.KN_get_con_dual_values(kc, n_cons - 1) ) if dual_rc == 0: # Get constraint names From 32617a4685d17bfc6da10bacfcfe2971ad40f2b6 Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Tue, 3 Feb 2026 16:41:41 +0100 Subject: [PATCH 14/25] code: add reported runtime --- linopy/solvers.py | 92 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index da56524e..1cac71e7 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1953,6 +1953,14 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: # Solve the problem ret = int(knitro.KN_solve(kc)) + reported_runtime = None + try: + val, rc = knitro.KN_get_solve_time_real(kc) + if rc == 0: + reported_runtime = float(val) + except Exception: + pass + # Get termination condition if ret in CONDITION_MAP: termination_condition = CONDITION_MAP[ret] @@ -1967,7 +1975,9 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: def get_solver_solution() -> Solution: # Get objective value try: - obj_val, obj_rc = unpack_value_and_rc(knitro.KN_get_obj_value(kc)) + obj_val, obj_rc = unpack_value_and_rc( + knitro.KN_get_obj_value(kc) + ) objective = float(obj_val) if obj_rc == 0 else np.nan except Exception: objective = np.nan @@ -1979,19 +1989,26 @@ def get_solver_solution() -> Solution: ) n_vars = int(n_vars_val) if n_vars_rc == 0 else 0 - x_val, x_rc = unpack_value_and_rc( - knitro.KN_get_var_primal_values(kc, n_vars-1) - ) - if x_rc == 0 and n_vars > 0: - # Get variable names - var_names = [] - for i in range(n_vars): - name_val, name_rc = unpack_value_and_rc( - knitro.KN_get_var_name(kc, i) + if n_vars > 0: + x_val, x_rc = unpack_value_and_rc( + knitro.KN_get_var_primal_values(kc, n_vars - 1) + ) + + if x_rc == 0: + names_val, names_rc = unpack_value_and_rc( + knitro.KN_get_var_names(kc) ) - var_names.append(str(name_val) if name_rc == 0 else f"x{i}") - sol = pd.Series(x_val, index=var_names, dtype=float) + if names_rc == 0 and names_val is not None: + var_names = list(names_val) + else: + var_names = [f"x{i}" for i in range(n_vars)] + + sol = pd.Series( + x_val, index=var_names, dtype=float + ) + else: + sol = pd.Series(dtype=float) else: sol = pd.Series(dtype=float) except Exception as e: @@ -2007,18 +2024,17 @@ def get_solver_solution() -> Solution: if n_cons > 0: dual_val, dual_rc = unpack_value_and_rc( - knitro.KN_get_con_dual_values(kc, n_cons-1) + knitro.KN_get_con_dual_values(kc, n_cons - 1) ) if dual_rc == 0: - # Get constraint names - con_names = [] - for i in range(n_cons): - name_val, name_rc = unpack_value_and_rc( - knitro.KN_get_con_name(kc, i) - ) - con_names.append( - str(name_val) if name_rc == 0 else f"c{i}" - ) + names_val, names_rc = unpack_value_and_rc( + knitro.KN_get_con_names(kc) + ) + + if names_rc == 0 and names_val is not None: + con_names = list(names_val) + else: + con_names = [f"c{i}" for i in range(n_cons)] dual = pd.Series(dual_val, index=con_names, dtype=float) else: @@ -2059,7 +2075,37 @@ def get_solver_solution() -> Solution: except Exception as err: logger.info("Could not write solution file. Error: %s", err) - return Result(status, solution) + return Result(status, solution, reported_runtime) + + solution = self.safe_get_solution(status=status, func=get_solver_solution) + solution = maybe_adjust_objective_sign(solution, io_api, sense) + + # Save basis if requested + if basis_fn is not None: + try: + # Knitro doesn't have direct basis export for LP files + logger.info( + "Basis export not directly supported by Knitro LP interface" + ) + except Exception as err: + logger.info("No basis stored. Error: %s", err) + + # Save solution if requested + if solution_fn is not None: + try: + write_sol = getattr(knitro, "KN_write_sol_file", None) + if write_sol is None: + logger.info( + "Solution export not supported by Knitro interface; ignoring solution_fn=%s", + solution_fn, + ) + else: + solution_fn.parent.mkdir(parents=True, exist_ok=True) + write_sol(kc, path_to_string(solution_fn)) + except Exception as err: + logger.info("Could not write solution file. Error: %s", err) + + return Result(status, solution, kc) finally: if kc is not None: From ef7e04e3b9aedafad2270b5c3714dcf93df3ea7e Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Tue, 3 Feb 2026 17:17:11 +0100 Subject: [PATCH 15/25] code: remove unnecessary code --- linopy/solvers.py | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index fb6a85d4..bc00eeec 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1955,8 +1955,8 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: reported_runtime = None try: - val, rc = knitro.KN_get_solve_time_real(kc) - if rc == 0: + val = knitro.KN_get_solve_time_real(kc) + if val: reported_runtime = float(val) except Exception: pass @@ -2073,36 +2073,6 @@ def get_solver_solution() -> Solution: return Result(status, solution, reported_runtime) - solution = self.safe_get_solution(status=status, func=get_solver_solution) - solution = maybe_adjust_objective_sign(solution, io_api, sense) - - # Save basis if requested - if basis_fn is not None: - try: - # Knitro doesn't have direct basis export for LP files - logger.info( - "Basis export not directly supported by Knitro LP interface" - ) - except Exception as err: - logger.info("No basis stored. Error: %s", err) - - # Save solution if requested - if solution_fn is not None: - try: - write_sol = getattr(knitro, "KN_write_sol_file", None) - if write_sol is None: - logger.info( - "Solution export not supported by Knitro interface; ignoring solution_fn=%s", - solution_fn, - ) - else: - solution_fn.parent.mkdir(parents=True, exist_ok=True) - write_sol(kc, path_to_string(solution_fn)) - except Exception as err: - logger.info("Could not write solution file. Error: %s", err) - - return Result(status, solution, kc) - finally: if kc is not None: with contextlib.suppress(Exception): From ee1cd9f61a007f241150d8e5e0d528dc337e1dd4 Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Wed, 4 Feb 2026 10:41:34 +0100 Subject: [PATCH 16/25] doc: update README.md and realease_notes --- README.md | 1 + doc/release_notes.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 644b556c..9037c40d 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ Fri 0 4 * [MOSEK](https://www.mosek.com/) * [COPT](https://www.shanshu.ai/copt) * [cuPDLPx](https://github.com/MIT-Lu-Lab/cuPDLPx) +* [Knitro](https://www.artelys.com/solvers/knitro/) Note that these do have to be installed by the user separately. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index d2ed2e97..514448df 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -6,6 +6,7 @@ Upcoming Version * Fix docs (pick highs solver) * Add the `sphinx-copybutton` to the documentation +* Add support for the `knitro` solver via the knitro python API Version 0.6.1 -------------- From 49cb2aa1bd2f87759e6645ec25327275d15dbb9e Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Wed, 4 Feb 2026 11:37:36 +0100 Subject: [PATCH 17/25] code: add new unit tests for Knitro --- test/test_solvers.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/test_solvers.py b/test/test_solvers.py index 283610e7..8536de9a 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -110,6 +110,42 @@ def test_knitro_solver_with_options(tmp_path: Path) -> None: assert log_file.exists() +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) +def test_knitro_solver_with_model(model: Model, tmp_path: Path) -> None: # noqa: F811 + """Test Knitro solver with a linopy Model instance.""" + knitro = solvers.Knitro() + sol_file = tmp_path / "solution.sol" + result = knitro.solve_problem(model=model, solution_fn=sol_file) + + assert result.status.is_ok + assert result.solution is not None + # The objective for the 'model' fixture is different from the MPS file + assert abs(result.solution.objective - 10.0) < 1e-6 + + +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) +def test_knitro_solver_no_log(tmp_path: Path) -> None: + """Test Knitro solver with logging disabled via options.""" + # outlev=0 should suppress log output + knitro = solvers.Knitro(outlev=0) + + mps_file = tmp_path / "problem.mps" + mps_file.write_text(free_mps_problem) + sol_file = tmp_path / "solution.sol" + log_file = tmp_path / "knitro.log" + + result = knitro.solve_problem( + problem_fn=mps_file, solution_fn=sol_file, log_fn=log_file + ) + + assert result.status.is_ok + assert not log_file.exists() + + @pytest.mark.skipif( "gurobi" not in set(solvers.available_solvers), reason="Gurobi is not installed" ) From 2bb8c5d29e7a2ff7fc9a83275d4dec3aa2f43dee Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Wed, 4 Feb 2026 11:39:38 +0100 Subject: [PATCH 18/25] code: add new unit tests for Knitro --- test/test_solvers.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/test_solvers.py b/test/test_solvers.py index 8536de9a..cf0fe430 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -113,16 +113,13 @@ def test_knitro_solver_with_options(tmp_path: Path) -> None: @pytest.mark.skipif( "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" ) -def test_knitro_solver_with_model(model: Model, tmp_path: Path) -> None: # noqa: F811 - """Test Knitro solver with a linopy Model instance.""" +def test_knitro_solver_with_model_raises_error(model: Model) -> None: # noqa: F811 + """Test Knitro solver raises NotImplementedError for model-based solving.""" knitro = solvers.Knitro() - sol_file = tmp_path / "solution.sol" - result = knitro.solve_problem(model=model, solution_fn=sol_file) - - assert result.status.is_ok - assert result.solution is not None - # The objective for the 'model' fixture is different from the MPS file - assert abs(result.solution.objective - 10.0) < 1e-6 + with pytest.raises( + NotImplementedError, match="Direct API not implemented for Knitro" + ): + knitro.solve_problem(model=model) @pytest.mark.skipif( From 9aab4db7cc5257c69ab9580dee6a586413973db9 Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Wed, 4 Feb 2026 11:46:00 +0100 Subject: [PATCH 19/25] code: add test for lp for knitro --- test/test_solvers.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/test_solvers.py b/test/test_solvers.py index cf0fe430..5d7ab9ae 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -45,6 +45,18 @@ ENDATA """ +free_lp_problem = """ +Maximize + z: 3 x + 4 y +Subject To + c1: 2 x + y <= 10 + c2: x + 2 y <= 12 +Bounds + 0 <= x + 0 <= y +End +""" + @pytest.mark.parametrize("solver", set(solvers.available_solvers)) def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None: @@ -74,7 +86,7 @@ def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None: @pytest.mark.skipif( "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" ) -def test_knitro_solver(tmp_path: Path) -> None: +def test_knitro_solver_for_mps(tmp_path: Path) -> None: """Test Knitro solver with a simple MPS problem.""" knitro = solvers.Knitro() @@ -89,6 +101,24 @@ def test_knitro_solver(tmp_path: Path) -> None: assert result.solution.objective == 30.0 +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) +def test_knitro_solver_for_lp(tmp_path: Path) -> None: + """Test Knitro solver with a simple MPS problem.""" + knitro = solvers.Knitro() + + mps_file = tmp_path / "problem.lp" + mps_file.write_text(free_lp_problem) + sol_file = tmp_path / "solution.sol" + + result = knitro.solve_problem(problem_fn=mps_file, solution_fn=sol_file) + + assert result.status.is_ok + assert result.solution is not None + assert result.solution.objective == 28.0 + + @pytest.mark.skipif( "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" ) From aa81351110d094ef424663527358a5e4e7e0c3db Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Wed, 4 Feb 2026 11:51:36 +0100 Subject: [PATCH 20/25] code: add test for lp for knitro --- test/test_solvers.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/test_solvers.py b/test/test_solvers.py index 5d7ab9ae..a1f95dce 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -83,9 +83,9 @@ def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None: assert result.solution.objective == 30.0 -@pytest.mark.skipif( - "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -) +#@pytest.mark.skipif( +# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +#) def test_knitro_solver_for_mps(tmp_path: Path) -> None: """Test Knitro solver with a simple MPS problem.""" knitro = solvers.Knitro() @@ -101,9 +101,9 @@ def test_knitro_solver_for_mps(tmp_path: Path) -> None: assert result.solution.objective == 30.0 -@pytest.mark.skipif( - "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -) +#@pytest.mark.skipif( +# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +#) def test_knitro_solver_for_lp(tmp_path: Path) -> None: """Test Knitro solver with a simple MPS problem.""" knitro = solvers.Knitro() @@ -119,9 +119,9 @@ def test_knitro_solver_for_lp(tmp_path: Path) -> None: assert result.solution.objective == 28.0 -@pytest.mark.skipif( - "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -) +#@pytest.mark.skipif( +# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +#) def test_knitro_solver_with_options(tmp_path: Path) -> None: """Test Knitro solver with custom options.""" # Set some common Knitro options @@ -140,9 +140,9 @@ def test_knitro_solver_with_options(tmp_path: Path) -> None: assert log_file.exists() -@pytest.mark.skipif( - "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -) +#@pytest.mark.skipif( +# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +#) def test_knitro_solver_with_model_raises_error(model: Model) -> None: # noqa: F811 """Test Knitro solver raises NotImplementedError for model-based solving.""" knitro = solvers.Knitro() @@ -152,9 +152,9 @@ def test_knitro_solver_with_model_raises_error(model: Model) -> None: # noqa: F knitro.solve_problem(model=model) -@pytest.mark.skipif( - "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -) +#@pytest.mark.skipif( +# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +#) def test_knitro_solver_no_log(tmp_path: Path) -> None: """Test Knitro solver with logging disabled via options.""" # outlev=0 should suppress log output From ca172f6053e182c37e6f558d0ff186f12c1f5628 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:51:49 +0000 Subject: [PATCH 21/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/test_solvers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/test_solvers.py b/test/test_solvers.py index a1f95dce..045ec237 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -83,9 +83,9 @@ def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None: assert result.solution.objective == 30.0 -#@pytest.mark.skipif( +# @pytest.mark.skipif( # "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -#) +# ) def test_knitro_solver_for_mps(tmp_path: Path) -> None: """Test Knitro solver with a simple MPS problem.""" knitro = solvers.Knitro() @@ -101,9 +101,9 @@ def test_knitro_solver_for_mps(tmp_path: Path) -> None: assert result.solution.objective == 30.0 -#@pytest.mark.skipif( +# @pytest.mark.skipif( # "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -#) +# ) def test_knitro_solver_for_lp(tmp_path: Path) -> None: """Test Knitro solver with a simple MPS problem.""" knitro = solvers.Knitro() @@ -119,9 +119,9 @@ def test_knitro_solver_for_lp(tmp_path: Path) -> None: assert result.solution.objective == 28.0 -#@pytest.mark.skipif( +# @pytest.mark.skipif( # "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -#) +# ) def test_knitro_solver_with_options(tmp_path: Path) -> None: """Test Knitro solver with custom options.""" # Set some common Knitro options @@ -140,9 +140,9 @@ def test_knitro_solver_with_options(tmp_path: Path) -> None: assert log_file.exists() -#@pytest.mark.skipif( +# @pytest.mark.skipif( # "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -#) +# ) def test_knitro_solver_with_model_raises_error(model: Model) -> None: # noqa: F811 """Test Knitro solver raises NotImplementedError for model-based solving.""" knitro = solvers.Knitro() @@ -152,9 +152,9 @@ def test_knitro_solver_with_model_raises_error(model: Model) -> None: # noqa: F knitro.solve_problem(model=model) -#@pytest.mark.skipif( +# @pytest.mark.skipif( # "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -#) +# ) def test_knitro_solver_no_log(tmp_path: Path) -> None: """Test Knitro solver with logging disabled via options.""" # outlev=0 should suppress log output From 3e9a7bb88fcf0f4601559565b6c8dbf9046be8ba Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Wed, 4 Feb 2026 12:01:46 +0100 Subject: [PATCH 22/25] code: add-back again skip --- test/test_solvers.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/test_solvers.py b/test/test_solvers.py index 045ec237..ddc2e1b6 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -83,10 +83,10 @@ def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None: assert result.solution.objective == 30.0 -# @pytest.mark.skipif( -# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -# ) -def test_knitro_solver_for_mps(tmp_path: Path) -> None: +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) +def test_knitro_solver_mps(tmp_path: Path) -> None: """Test Knitro solver with a simple MPS problem.""" knitro = solvers.Knitro() @@ -101,9 +101,9 @@ def test_knitro_solver_for_mps(tmp_path: Path) -> None: assert result.solution.objective == 30.0 -# @pytest.mark.skipif( -# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -# ) +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) def test_knitro_solver_for_lp(tmp_path: Path) -> None: """Test Knitro solver with a simple MPS problem.""" knitro = solvers.Knitro() @@ -140,9 +140,9 @@ def test_knitro_solver_with_options(tmp_path: Path) -> None: assert log_file.exists() -# @pytest.mark.skipif( -# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -# ) +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) def test_knitro_solver_with_model_raises_error(model: Model) -> None: # noqa: F811 """Test Knitro solver raises NotImplementedError for model-based solving.""" knitro = solvers.Knitro() @@ -152,9 +152,9 @@ def test_knitro_solver_with_model_raises_error(model: Model) -> None: # noqa: F knitro.solve_problem(model=model) -# @pytest.mark.skipif( -# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -# ) +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) def test_knitro_solver_no_log(tmp_path: Path) -> None: """Test Knitro solver with logging disabled via options.""" # outlev=0 should suppress log output From 164a1e00587cb03fb235c51ecbe2495dfc8bce9a Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Wed, 4 Feb 2026 12:44:32 +0100 Subject: [PATCH 23/25] code: remove uncomment to skipif --- test/test_solvers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_solvers.py b/test/test_solvers.py index ddc2e1b6..26c14596 100644 --- a/test/test_solvers.py +++ b/test/test_solvers.py @@ -119,9 +119,9 @@ def test_knitro_solver_for_lp(tmp_path: Path) -> None: assert result.solution.objective == 28.0 -# @pytest.mark.skipif( -# "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" -# ) +@pytest.mark.skipif( + "knitro" not in set(solvers.available_solvers), reason="Knitro is not installed" +) def test_knitro_solver_with_options(tmp_path: Path) -> None: """Test Knitro solver with custom options.""" # Set some common Knitro options From 7b03e48b28cace1f396a1469ab0df04716f6ebb6 Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Tue, 10 Feb 2026 12:33:59 +0100 Subject: [PATCH 24/25] add namedtuple --- linopy/solvers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index bc00eeec..46cecc99 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1261,7 +1261,7 @@ def get_solver_solution() -> Solution: return Solution(sol, dual, objective) solution = self.safe_get_solution(status=status, func=get_solver_solution) - solution = solution = maybe_adjust_objective_sign(solution, io_api, sense) + solution = maybe_adjust_objective_sign(solution, io_api, sense) return Result(status, solution, m) @@ -1961,6 +1961,8 @@ def set_param_by_name(kc: Any, name: str, value: Any) -> None: except Exception: pass + knitro_model = namedtuple("KnitroModel", "reported_runtime") + # Get termination condition if ret in CONDITION_MAP: termination_condition = CONDITION_MAP[ret] @@ -2071,7 +2073,7 @@ def get_solver_solution() -> Solution: except Exception as err: logger.info("Could not write solution file. Error: %s", err) - return Result(status, solution, reported_runtime) + return Result(status, solution, knitro_model(reported_runtime=reported_runtime)) finally: if kc is not None: From e2f256402705719b972134c6b6b81e60360d1e17 Mon Sep 17 00:00:00 2001 From: Fabrizio Finozzi Date: Tue, 10 Feb 2026 12:34:20 +0100 Subject: [PATCH 25/25] include pre-commit checks --- linopy/solvers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/linopy/solvers.py b/linopy/solvers.py index 46cecc99..92b32c81 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -2073,7 +2073,9 @@ def get_solver_solution() -> Solution: except Exception as err: logger.info("Could not write solution file. Error: %s", err) - return Result(status, solution, knitro_model(reported_runtime=reported_runtime)) + return Result( + status, solution, knitro_model(reported_runtime=reported_runtime) + ) finally: if kc is not None: