Skip to content

Commit

Permalink
Merge 0d28844 into d450456
Browse files Browse the repository at this point in the history
  • Loading branch information
Stéphane Caron committed Jul 18, 2023
2 parents d450456 + 0d28844 commit c8715aa
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
MANIFEST
build/
dist/
examples/lpsolvers
lpsolvers.egg-info
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to this project will be documented in this file.

## Unreleased

### Added

- Continuous integration: ruff
- ProxQP (LP variant) solver interface

### Changed

- The ``solver`` keyword argument is now mandatory

## [1.1.0] - 2022/03/17

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# LP Solvers for Python

[![Build](https://img.shields.io/github/actions/workflow/status/stephane-caron/lpsolvers/build.yml?branch=master)](https://github.com/stephane-caron/lpsolvers/actions)
[![Build](https://img.shields.io/github/actions/workflow/status/stephane-caron/lpsolvers/test.yml?branch=master)](https://github.com/stephane-caron/lpsolvers/actions)
[![Coverage](https://coveralls.io/repos/github/stephane-caron/lpsolvers/badge.svg?branch=master)](https://coveralls.io/github/stephane-caron/lpsolvers?branch=master)
[![Documentation](https://img.shields.io/badge/docs-online-brightgreen?logo=read-the-docs&style=flat)](https://scaron.info/doc/lpsolvers/)
[![PyPI version](https://img.shields.io/pypi/v/lpsolvers)](https://pypi.org/project/lpsolvers/)
Expand Down
83 changes: 59 additions & 24 deletions lpsolvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with lpsolvers. If not, see <http://www.gnu.org/licenses/>.

"""Linear programming solvers in Python with a unified API"""
"""Linear programming solvers in Python with a unified API."""

from typing import Optional

import numpy as np

from .exceptions import SolverNotFound
from .exceptions import NoSolverSelected, SolverNotFound

__version__ = "1.1.0"

Expand All @@ -47,9 +47,7 @@ def cdd_solve_lp(
A: Optional[np.ndarray] = None,
b: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Error function defined when cdd is not available.
"""
"""Error function defined when cdd is not available."""
raise ImportError("cdd not found")


Expand All @@ -69,10 +67,9 @@ def cvxopt_solve_lp(
A: Optional[np.ndarray] = None,
b: Optional[np.ndarray] = None,
solver: Optional[str] = None,
**kwargs,
) -> np.ndarray:
"""
Error function defined when CVXOPT is not available.
"""
"""Error function defined when CVXOPT is not available."""
raise ImportError("CVXOPT not found")


Expand All @@ -93,35 +90,58 @@ def cvxpy_solve_lp(
b: Optional[np.ndarray] = None,
solver: Optional[str] = None,
verbose: bool = False,
**kwargs,
) -> np.ndarray:
"""
Error function defined when CVXPY is not available.
"""
"""Error function defined when CVXPY is not available."""
raise ImportError("CVXPY not found")


# ProxQP
# ======

try:
from .proxqp_ import proxqp_solve_lp

available_solvers.append("proxqp")
except ImportError:

def proxqp_solve_lp(
c: np.ndarray,
G: np.ndarray,
h: np.ndarray,
A: Optional[np.ndarray] = None,
b: Optional[np.ndarray] = None,
initvals: Optional[np.ndarray] = None,
verbose: bool = False,
backend: Optional[str] = None,
**kwargs,
) -> np.ndarray:
"""Error function defined when ProxQP is not available."""
raise ImportError("ProxQP not found")


def solve_lp(
c: np.ndarray,
G: np.ndarray,
h: np.ndarray,
A: Optional[np.ndarray] = None,
b: Optional[np.ndarray] = None,
solver="cvxopt",
solver: Optional[str] = None,
**kwargs,
) -> np.ndarray:
"""
Solve a Linear Program defined as:
r"""Solve a linear program using one of the available LP solvers.
The linear program is defined as:
.. math::
\\begin{split}\\begin{array}{ll}
\\mbox{minimize} &
c^T x \\\\
\\mbox{subject to}
& G x \\leq h \\\\
\begin{split}\begin{array}{ll}
\mbox{minimize} &
c^T x \\
\mbox{subject to}
& G x \leq h \\
& A x = b
\\end{array}\\end{split}
using one of the available LP solvers.
\end{array}\end{split}
Parameters
----------
Expand Down Expand Up @@ -149,15 +169,29 @@ def solve_lp(
If the LP is not feasible.
SolverNotFound
If the requested LP solver is not found.
Notes
-----
Extra keyword arguments given to this function are forwarded to the
underlying solver. For example, we can call ProxQP with a custom absolute
feasibility tolerance by ``solve_lp(c, G, h, solver='proxqp',
eps_abs=1e-8)``.
"""
if solver is None:
raise NoSolverSelected(
"Set the `solver` keyword argument to one of the "
f"available solvers in {available_solvers}"
)
if isinstance(G, np.ndarray) and G.ndim == 1:
G = G.reshape((1, G.shape[0]))
if solver == "cdd":
return cdd_solve_lp(c, G, h, A, b)
if solver == "cvxopt":
return cvxopt_solve_lp(c, G, h, A, b)
return cvxopt_solve_lp(c, G, h, A, b, **kwargs)
if solver == "cvxpy":
return cvxpy_solve_lp(c, G, h, A, b)
return cvxpy_solve_lp(c, G, h, A, b, **kwargs)
if solver == "proxqp":
return proxqp_solve_lp(c, G, h, A, b, **kwargs)
raise SolverNotFound(f"solver '{solver}' is not available")


Expand All @@ -167,5 +201,6 @@ def solve_lp(
"cdd_solve_lp",
"cvxopt_solve_lp",
"cvxpy_solve_lp",
"proxqp_solve_lp",
"solve_lp",
]
7 changes: 4 additions & 3 deletions lpsolvers/cdd_.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ def cdd_solve_lp(
A: Optional[np.ndarray] = None,
b: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Solve a linear program defined by:
r"""Solve a linear program using the LP solver from cdd.
The linear program is defined by:
.. math::
Expand All @@ -46,7 +47,7 @@ def cdd_solve_lp(
& A x = b
\\end{array}\\end{split}
using the LP solver from `cdd <https://github.com/mcmtroffaes/pycddlib>`_.
It is solved using `cdd <https://github.com/mcmtroffaes/pycddlib>`_.
Parameters
----------
Expand Down
25 changes: 12 additions & 13 deletions lpsolvers/cvxopt_.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@
"""Solver interface for CVXOPT."""

import logging

from typing import Optional

import cvxopt
import cvxopt.solvers
import numpy as np

from cvxopt.solvers import lp


cvxopt.solvers.options["show_progress"] = False # disable cvxopt output

GLPK_IF_AVAILABLE: Optional[str] = None
Expand Down Expand Up @@ -76,21 +73,23 @@ def cvxopt_solve_lp(
A: Optional[np.ndarray] = None,
b: Optional[np.ndarray] = None,
solver: Optional[str] = GLPK_IF_AVAILABLE,
**kwargs,
) -> np.ndarray:
"""
Solve a linear program defined by:
r"""Solve a linear program using CVXOPT.
The linear program is defined by:
.. math::
\\begin{split}\\begin{array}{ll}
\\mbox{minimize} &
c^T x \\\\
\\mbox{subject to}
& G x \\leq h \\\\
\begin{split}\begin{array}{ll}
\mbox{minimize} &
c^T x \\
\mbox{subject to}
& G x \leq h \\
& A x = b
\\end{array}\\end{split}
\end{array}\end{split}
using the LP solver from `CVXOPT <http://cvxopt.org/>`_.
It is solved using the LP solver from `CVXOPT <http://cvxopt.org/>`_.
Parameters
----------
Expand Down Expand Up @@ -120,7 +119,7 @@ def cvxopt_solve_lp(
args = [cvxopt_matrix(c), cvxopt_matrix(G), cvxopt_matrix(h)]
if A is not None and b is not None:
args.extend([cvxopt_matrix(A), cvxopt_matrix(b)])
sol = lp(*args, solver=solver)
sol = lp(*args, solver=solver, **kwargs)
if "optimal" not in sol["status"]:
raise ValueError(f"LP optimum not found: {sol['status']}")
n = c.shape[0]
Expand Down
25 changes: 13 additions & 12 deletions lpsolvers/cvxpy_.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from typing import Optional

import numpy as np

from cvxpy import Minimize, Problem, Variable
from numpy import array

Expand All @@ -36,22 +35,24 @@ def cvxpy_solve_lp(
b: Optional[np.ndarray] = None,
solver: Optional[str] = None,
verbose: bool = False,
**kwargs,
) -> np.ndarray:
"""
Solve a linear program defined as:
r"""Solve a linear program using CVXPY.
The linear program is defined by:
.. math::
\\begin{split}\\begin{array}{ll}
\\mbox{minimize}
& c^T x \\\\
\\mbox{subject to}
& G x \\leq h \\\\
\begin{split}\begin{array}{ll}
\mbox{minimize}
& c^T x \\
\mbox{subject to}
& G x \leq h \\
& A x = b
\\end{array}\\end{split}
\end{array}\end{split}
calling a given solver using the `CVXPY <http://www.cvxpy.org/>`_ modelling
language.
It is solved using a solver wrapped by `CVXPY <http://www.cvxpy.org/>`_.
The underlying solver is selected via the corresponding keyword argument.
Parameters
----------
Expand Down Expand Up @@ -89,7 +90,7 @@ def cvxpy_solve_lp(
if A is not None:
constraints.append(A @ x == b)
prob = Problem(objective, constraints)
prob.solve(solver=solver, verbose=verbose)
prob.solve(solver=solver, verbose=verbose, **kwargs)
if x.value is None:
raise ValueError("Linear program is not feasible")
return array(x.value).reshape((n,))
9 changes: 8 additions & 1 deletion lpsolvers/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"""Exceptions."""


class SolverNotFound(Exception):
class LPSolverException(Exception):
"""Base class for lpsolvers exception."""


class SolverNotFound(LPSolverException):
"""Exception raised when a requested solver is not found."""


class NoSolverSelected(LPSolverException):
"""Exception raised when the `solver` keyword argument is not set."""
Loading

0 comments on commit c8715aa

Please sign in to comment.