|
96 | 96 |
|
97 | 97 | available_solvers.append("xpress") |
98 | 98 |
|
| 99 | +with contextlib.suppress(ModuleNotFoundError, ImportError): |
| 100 | + import knitro |
| 101 | + |
| 102 | + available_solvers.append("knitro") |
| 103 | + |
99 | 104 | # xpress.Namespaces was added in xpress 9.6 |
100 | 105 | try: |
101 | 106 | from xpress import Namespaces as xpress_Namespaces |
@@ -160,6 +165,7 @@ class SolverName(enum.Enum): |
160 | 165 | Gurobi = "gurobi" |
161 | 166 | SCIP = "scip" |
162 | 167 | Xpress = "xpress" |
| 168 | + Knitro = "knitro" |
163 | 169 | Mosek = "mosek" |
164 | 170 | COPT = "copt" |
165 | 171 | MindOpt = "mindopt" |
@@ -1625,6 +1631,220 @@ def get_solver_solution() -> Solution: |
1625 | 1631 | return Result(status, solution, m) |
1626 | 1632 |
|
1627 | 1633 |
|
| 1634 | +class Knitro(Solver[None]): |
| 1635 | + """ |
| 1636 | + Solver subclass for the Knitro solver. |
| 1637 | +
|
| 1638 | + Knitro is a powerful nonlinear optimization solver that also handles |
| 1639 | + linear and quadratic problems efficiently. |
| 1640 | +
|
| 1641 | + For more information on solver options, see |
| 1642 | + https://www.artelys.com/app/docs/knitro/3_referenceManual/knitroPythonReference.html |
| 1643 | +
|
| 1644 | + Attributes |
| 1645 | + ---------- |
| 1646 | + **solver_options |
| 1647 | + options for the given solver |
| 1648 | + """ |
| 1649 | + |
| 1650 | + def __init__( |
| 1651 | + self, |
| 1652 | + **solver_options: Any, |
| 1653 | + ) -> None: |
| 1654 | + super().__init__(**solver_options) |
| 1655 | + |
| 1656 | + def solve_problem_from_model( |
| 1657 | + self, |
| 1658 | + model: Model, |
| 1659 | + solution_fn: Path | None = None, |
| 1660 | + log_fn: Path | None = None, |
| 1661 | + warmstart_fn: Path | None = None, |
| 1662 | + basis_fn: Path | None = None, |
| 1663 | + env: None = None, |
| 1664 | + explicit_coordinate_names: bool = False, |
| 1665 | + ) -> Result: |
| 1666 | + msg = "Direct API not implemented for Knitro" |
| 1667 | + raise NotImplementedError(msg) |
| 1668 | + |
| 1669 | + def solve_problem_from_file( |
| 1670 | + self, |
| 1671 | + problem_fn: Path, |
| 1672 | + solution_fn: Path | None = None, |
| 1673 | + log_fn: Path | None = None, |
| 1674 | + warmstart_fn: Path | None = None, |
| 1675 | + basis_fn: Path | None = None, |
| 1676 | + env: None = None, |
| 1677 | + ) -> Result: |
| 1678 | + """ |
| 1679 | + Solve a linear problem from a problem file using the Knitro solver. |
| 1680 | +
|
| 1681 | + This function reads the linear problem file and passes it to the Knitro |
| 1682 | + solver. If the solution is successful it returns variable solutions and |
| 1683 | + constraint dual values. |
| 1684 | +
|
| 1685 | + Parameters |
| 1686 | + ---------- |
| 1687 | + problem_fn : Path |
| 1688 | + Path to the problem file. |
| 1689 | + solution_fn : Path, optional |
| 1690 | + Path to the solution file. |
| 1691 | + log_fn : Path, optional |
| 1692 | + Path to the log file. |
| 1693 | + warmstart_fn : Path, optional |
| 1694 | + Path to the warmstart file. |
| 1695 | + basis_fn : Path, optional |
| 1696 | + Path to the basis file. |
| 1697 | + env : None, optional |
| 1698 | + Environment for the solver |
| 1699 | +
|
| 1700 | + Returns |
| 1701 | + ------- |
| 1702 | + Result |
| 1703 | + """ |
| 1704 | + # Knitro status codes: https://www.artelys.com/app/docs/knitro/3_referenceManual/knitroReturnCodes.html |
| 1705 | + CONDITION_MAP = { |
| 1706 | + 0: "optimal", |
| 1707 | + -100: "feasible_point", |
| 1708 | + -101: "infeasible", |
| 1709 | + -102: "feasible_point", |
| 1710 | + -200: "unbounded", |
| 1711 | + -201: "infeasible_or_unbounded", |
| 1712 | + -202: "iteration_limit", |
| 1713 | + -203: "time_limit", |
| 1714 | + -204: "function_evaluation_limit", |
| 1715 | + -300: "unbounded", |
| 1716 | + -400: "iteration_limit", |
| 1717 | + -401: "time_limit", |
| 1718 | + -410: "mip_node_limit", |
| 1719 | + -411: "mip_solution_limit", |
| 1720 | + } |
| 1721 | + |
| 1722 | + io_api = read_io_api_from_problem_file(problem_fn) |
| 1723 | + sense = read_sense_from_problem_file(problem_fn) |
| 1724 | + |
| 1725 | + try: |
| 1726 | + kc = knitro.KN_new() |
| 1727 | + except Exception as e: |
| 1728 | + msg = f"Failed to create Knitro solver instance: {e}" |
| 1729 | + raise RuntimeError(msg) |
| 1730 | + |
| 1731 | + try: |
| 1732 | + # Read the problem file |
| 1733 | + ret = knitro.KN_load_mps_file(kc, path_to_string(problem_fn)) |
| 1734 | + if ret != 0: |
| 1735 | + msg = f"Failed to load problem file: Knitro error code {ret}" |
| 1736 | + raise RuntimeError(msg) |
| 1737 | + |
| 1738 | + # Set log file if specified |
| 1739 | + if log_fn is not None: |
| 1740 | + knitro.KN_set_param_by_name(kc, "outlev", 6) # Enable detailed output |
| 1741 | + knitro.KN_set_param_by_name(kc, "outmode", path_to_string(log_fn)) |
| 1742 | + |
| 1743 | + # Set solver options |
| 1744 | + for k, v in self.solver_options.items(): |
| 1745 | + if isinstance(v, int | float): |
| 1746 | + knitro.KN_set_param_by_name(kc, k, v) |
| 1747 | + elif isinstance(v, str): |
| 1748 | + knitro.KN_set_char_param_by_name(kc, k, v) |
| 1749 | + |
| 1750 | + # Load warmstart if provided |
| 1751 | + if warmstart_fn is not None: |
| 1752 | + try: |
| 1753 | + # Knitro doesn't have direct basis loading, but we can set initial values |
| 1754 | + logger.info( |
| 1755 | + "Warmstart not directly supported by Knitro LP interface" |
| 1756 | + ) |
| 1757 | + except Exception as err: |
| 1758 | + logger.info("Warmstart could not be loaded. Error: %s", err) |
| 1759 | + |
| 1760 | + # Solve the problem |
| 1761 | + ret = knitro.KN_solve(kc) |
| 1762 | + |
| 1763 | + # Get termination condition |
| 1764 | + termination_condition = CONDITION_MAP.get(ret, "unknown") |
| 1765 | + status = Status.from_termination_condition(termination_condition) |
| 1766 | + status.legacy_status = ret |
| 1767 | + |
| 1768 | + def get_solver_solution() -> Solution: |
| 1769 | + # Get objective value |
| 1770 | + try: |
| 1771 | + obj_ptr = knitro.KN_get_obj_value(kc) |
| 1772 | + objective = obj_ptr[0] if obj_ptr[1] == 0 else np.nan |
| 1773 | + except Exception: |
| 1774 | + objective = np.nan |
| 1775 | + |
| 1776 | + # Get variable values |
| 1777 | + try: |
| 1778 | + n_vars = knitro.KN_get_number_vars(kc) |
| 1779 | + x_ptr = knitro.KN_get_var_primal_values(kc, n_vars) |
| 1780 | + if x_ptr[1] == 0: |
| 1781 | + # Get variable names |
| 1782 | + var_names = [] |
| 1783 | + for i in range(n_vars): |
| 1784 | + name_ptr = knitro.KN_get_var_name(kc, i) |
| 1785 | + if name_ptr[1] == 0: |
| 1786 | + var_names.append(name_ptr[0]) |
| 1787 | + else: |
| 1788 | + var_names.append(f"x{i}") |
| 1789 | + sol = pd.Series(x_ptr[0], index=var_names, dtype=float) |
| 1790 | + else: |
| 1791 | + sol = pd.Series(dtype=float) |
| 1792 | + except Exception as e: |
| 1793 | + logger.warning(f"Could not extract primal solution: {e}") |
| 1794 | + sol = pd.Series(dtype=float) |
| 1795 | + |
| 1796 | + # Get dual values (constraint multipliers) |
| 1797 | + try: |
| 1798 | + n_cons = knitro.KN_get_number_cons(kc) |
| 1799 | + if n_cons > 0: |
| 1800 | + dual_ptr = knitro.KN_get_con_dual_values(kc, n_cons) |
| 1801 | + if dual_ptr[1] == 0: |
| 1802 | + # Get constraint names |
| 1803 | + con_names = [] |
| 1804 | + for i in range(n_cons): |
| 1805 | + name_ptr = knitro.KN_get_con_name(kc, i) |
| 1806 | + if name_ptr[1] == 0: |
| 1807 | + con_names.append(name_ptr[0]) |
| 1808 | + else: |
| 1809 | + con_names.append(f"c{i}") |
| 1810 | + dual = pd.Series(dual_ptr[0], index=con_names, dtype=float) |
| 1811 | + else: |
| 1812 | + dual = pd.Series(dtype=float) |
| 1813 | + else: |
| 1814 | + dual = pd.Series(dtype=float) |
| 1815 | + except Exception as e: |
| 1816 | + logger.warning(f"Could not extract dual solution: {e}") |
| 1817 | + dual = pd.Series(dtype=float) |
| 1818 | + |
| 1819 | + return Solution(sol, dual, objective) |
| 1820 | + |
| 1821 | + solution = self.safe_get_solution(status=status, func=get_solver_solution) |
| 1822 | + solution = maybe_adjust_objective_sign(solution, io_api, sense) |
| 1823 | + |
| 1824 | + # Save basis if requested |
| 1825 | + if basis_fn is not None: |
| 1826 | + try: |
| 1827 | + # Knitro doesn't have direct basis export for LP files |
| 1828 | + logger.info( |
| 1829 | + "Basis export not directly supported by Knitro LP interface" |
| 1830 | + ) |
| 1831 | + except Exception as err: |
| 1832 | + logger.info("No basis stored. Error: %s", err) |
| 1833 | + |
| 1834 | + # Save solution if requested |
| 1835 | + if solution_fn is not None: |
| 1836 | + try: |
| 1837 | + knitro.KN_write_mps_file(kc, path_to_string(solution_fn)) |
| 1838 | + except Exception as err: |
| 1839 | + logger.info("Could not write solution file. Error: %s", err) |
| 1840 | + |
| 1841 | + return Result(status, solution, kc) |
| 1842 | + |
| 1843 | + finally: |
| 1844 | + # Clean up Knitro context |
| 1845 | + knitro.KN_free(kc) |
| 1846 | + |
| 1847 | + |
1628 | 1848 | mosek_bas_re = re.compile(r" (XL|XU)\s+([^ \t]+)\s+([^ \t]+)| (LL|UL|BS)\s+([^ \t]+)") |
1629 | 1849 |
|
1630 | 1850 |
|
|
0 commit comments