mopt 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mopt/__init__.py +12 -0
- mopt/autodiff/__init__.py +5 -0
- mopt/autodiff/torch_adapter.py +75 -0
- mopt/base_classes.py +66 -0
- mopt/linear/__init__.py +6 -0
- mopt/linear/problem.py +59 -0
- mopt/linear/simplex.py +161 -0
- mopt/nonlinear/__init__.py +6 -0
- mopt/nonlinear/finite_diff.py +53 -0
- mopt/nonlinear/problem.py +58 -0
- mopt-0.0.1.dist-info/METADATA +48 -0
- mopt-0.0.1.dist-info/RECORD +14 -0
- mopt-0.0.1.dist-info/WHEEL +4 -0
- mopt-0.0.1.dist-info/licenses/LICENSE +21 -0
mopt/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""MOpt: mathematical optimization solvers.
|
|
2
|
+
|
|
3
|
+
Implements solvers for linear programming (Simplex, ...), nonlinear
|
|
4
|
+
programming (line search, Newton's method, ...), and other classes of
|
|
5
|
+
optimization problems.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from mopt.base_classes import BaseOptimizer, OptimizeResult
|
|
9
|
+
|
|
10
|
+
__version__ = "0.0.1"
|
|
11
|
+
|
|
12
|
+
__all__ = ["BaseOptimizer", "OptimizeResult"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Exact gradients via PyTorch autodiff, behind a NumPy-facing interface.
|
|
2
|
+
|
|
3
|
+
PyTorch is an optional dependency (``pip install mopt[torch]``); it is
|
|
4
|
+
imported lazily so the rest of mopt works without it. Functions passed to
|
|
5
|
+
this module must be written with torch operations — autodiff cannot see
|
|
6
|
+
through plain-NumPy code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from mopt.nonlinear import NLPProblem
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _torch():
|
|
19
|
+
try:
|
|
20
|
+
import torch
|
|
21
|
+
except ImportError as exc: # pragma: no cover
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"PyTorch is required for mopt.autodiff.torch_adapter; "
|
|
24
|
+
"install it with: pip install mopt[torch]"
|
|
25
|
+
) from exc
|
|
26
|
+
return torch
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def torch_function(f) -> Callable[[np.ndarray], float]:
|
|
30
|
+
"""Wrap a torch-written scalar function as a NumPy-facing objective."""
|
|
31
|
+
|
|
32
|
+
def f_np(x: np.ndarray) -> float:
|
|
33
|
+
torch = _torch()
|
|
34
|
+
with torch.no_grad():
|
|
35
|
+
return float(f(torch.as_tensor(np.asarray(x, dtype=float))))
|
|
36
|
+
|
|
37
|
+
return f_np
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def torch_gradient(f) -> Callable[[np.ndarray], np.ndarray]:
|
|
41
|
+
"""Wrap a torch-written scalar function as a NumPy-facing gradient.
|
|
42
|
+
|
|
43
|
+
The returned callable evaluates the exact gradient of ``f`` by reverse-
|
|
44
|
+
mode autodiff: one forward pass builds the computation graph, one
|
|
45
|
+
backward pass applies the chain rule — the full gradient costs roughly
|
|
46
|
+
one extra evaluation of ``f`` regardless of the dimension.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
f : callable
|
|
51
|
+
Maps a 1-D float64 torch tensor to a scalar torch tensor, using
|
|
52
|
+
torch operations throughout.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
callable
|
|
57
|
+
``grad(x: np.ndarray) -> np.ndarray`` with float64 precision.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def grad(x: np.ndarray) -> np.ndarray:
|
|
61
|
+
torch = _torch()
|
|
62
|
+
xt = torch.tensor(np.asarray(x, dtype=float), requires_grad=True)
|
|
63
|
+
(g,) = torch.autograd.grad(f(xt), xt)
|
|
64
|
+
return g.numpy()
|
|
65
|
+
|
|
66
|
+
return grad
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def from_torch(f, x0) -> NLPProblem:
|
|
70
|
+
"""Build an :class:`~mopt.nonlinear.NLPProblem` from a torch-written ``f``.
|
|
71
|
+
|
|
72
|
+
The objective and its exact autodiff gradient are both wired in, so the
|
|
73
|
+
resulting problem never falls back to finite differences.
|
|
74
|
+
"""
|
|
75
|
+
return NLPProblem(f=torch_function(f), x0=x0, grad=torch_gradient(f))
|
mopt/base_classes.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Base classes shared by every MOpt optimizer.
|
|
2
|
+
|
|
3
|
+
All solvers follow one contract: a Problem object describes *what* to
|
|
4
|
+
optimize, and each optimizer implements ``solve(problem) -> OptimizeResult``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class OptimizeResult:
|
|
18
|
+
"""Outcome of an optimization run, returned by every solver.
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
x : np.ndarray or None
|
|
23
|
+
The solution found, or None when the run failed (e.g. the problem
|
|
24
|
+
is infeasible or unbounded).
|
|
25
|
+
fun : float or None
|
|
26
|
+
Objective value at ``x``, or None when there is no solution.
|
|
27
|
+
success : bool
|
|
28
|
+
Whether the solver terminated with a valid solution.
|
|
29
|
+
message : str
|
|
30
|
+
Human-readable description of the termination cause.
|
|
31
|
+
n_iter : int
|
|
32
|
+
Number of iterations performed.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
x: np.ndarray | None
|
|
36
|
+
fun: float | None
|
|
37
|
+
success: bool
|
|
38
|
+
message: str = ""
|
|
39
|
+
n_iter: int = 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BaseOptimizer(ABC):
|
|
43
|
+
"""Abstract base class that all MOpt optimizers must inherit.
|
|
44
|
+
|
|
45
|
+
Subclasses implement :meth:`solve`; they must not mutate the problem
|
|
46
|
+
they are given, and must report failures (infeasible, unbounded,
|
|
47
|
+
iteration limit, ...) via ``OptimizeResult.success`` and
|
|
48
|
+
``OptimizeResult.message`` rather than by raising.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def solve(self, problem: Any) -> OptimizeResult:
|
|
53
|
+
"""Solve the given problem.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
problem : Any
|
|
58
|
+
A problem description understood by this solver. Concrete
|
|
59
|
+
Problem classes are introduced alongside the first solver for
|
|
60
|
+
each problem class.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
OptimizeResult
|
|
65
|
+
The solution and termination details.
|
|
66
|
+
"""
|
mopt/linear/__init__.py
ADDED
mopt/linear/problem.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Linear programming problem description."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class LPProblem:
|
|
12
|
+
"""A linear program.
|
|
13
|
+
|
|
14
|
+
Describes the problem
|
|
15
|
+
|
|
16
|
+
.. math::
|
|
17
|
+
|
|
18
|
+
\\min_x \\; c^T x
|
|
19
|
+
\\quad \\text{s.t.} \\quad
|
|
20
|
+
A_{ub} x \\le b_{ub}, \\;
|
|
21
|
+
A_{eq} x = b_{eq}, \\;
|
|
22
|
+
x \\ge 0
|
|
23
|
+
|
|
24
|
+
Attributes
|
|
25
|
+
----------
|
|
26
|
+
c : np.ndarray, shape (n,)
|
|
27
|
+
Objective coefficients (minimized).
|
|
28
|
+
A_ub, b_ub : np.ndarray or None
|
|
29
|
+
Inequality constraints ``A_ub @ x <= b_ub``, shapes (m_ub, n) and (m_ub,).
|
|
30
|
+
A_eq, b_eq : np.ndarray or None
|
|
31
|
+
Equality constraints ``A_eq @ x == b_eq``, shapes (m_eq, n) and (m_eq,).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
c: np.ndarray
|
|
35
|
+
A_ub: np.ndarray | None = None
|
|
36
|
+
b_ub: np.ndarray | None = None
|
|
37
|
+
A_eq: np.ndarray | None = None
|
|
38
|
+
b_eq: np.ndarray | None = None
|
|
39
|
+
|
|
40
|
+
def __post_init__(self):
|
|
41
|
+
self.c = np.atleast_1d(np.asarray(self.c, dtype=float))
|
|
42
|
+
if self.c.ndim != 1:
|
|
43
|
+
raise ValueError("c must be a 1-D array.")
|
|
44
|
+
n = self.c.size
|
|
45
|
+
for name in ("ub", "eq"):
|
|
46
|
+
A = getattr(self, f"A_{name}")
|
|
47
|
+
b = getattr(self, f"b_{name}")
|
|
48
|
+
if (A is None) != (b is None):
|
|
49
|
+
raise ValueError(f"A_{name} and b_{name} must be given together.")
|
|
50
|
+
if A is None:
|
|
51
|
+
continue
|
|
52
|
+
A = np.atleast_2d(np.asarray(A, dtype=float))
|
|
53
|
+
b = np.atleast_1d(np.asarray(b, dtype=float))
|
|
54
|
+
if A.shape != (b.size, n):
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"A_{name} has shape {A.shape}, expected ({b.size}, {n})."
|
|
57
|
+
)
|
|
58
|
+
setattr(self, f"A_{name}", A)
|
|
59
|
+
setattr(self, f"b_{name}", b)
|
mopt/linear/simplex.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Two-phase primal simplex method on a dense tableau.
|
|
2
|
+
|
|
3
|
+
The problem is first brought to standard form ``A x = b, x >= 0, b >= 0`` by
|
|
4
|
+
adding one slack variable per inequality row and negating rows with negative
|
|
5
|
+
right-hand sides. Phase 1 then minimizes the sum of artificial variables to
|
|
6
|
+
find a basic feasible solution (a positive optimum proves infeasibility);
|
|
7
|
+
phase 2 minimizes the original objective from that starting basis. Pivots
|
|
8
|
+
follow Bland's rule (smallest-index entering and leaving variable), which
|
|
9
|
+
guarantees termination even on degenerate problems.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from mopt.base_classes import BaseOptimizer, OptimizeResult
|
|
17
|
+
from mopt.linear.problem import LPProblem
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _pivot(T: np.ndarray, basis: list[int], row: int, col: int) -> None:
|
|
21
|
+
"""Pivot the tableau so that ``col`` becomes basic in ``row``."""
|
|
22
|
+
T[row] /= T[row, col]
|
|
23
|
+
factors = T[:, col].copy()
|
|
24
|
+
factors[row] = 0.0
|
|
25
|
+
T -= np.outer(factors, T[row])
|
|
26
|
+
basis[row] = col
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Simplex(BaseOptimizer):
|
|
30
|
+
"""Two-phase tableau simplex solver for :class:`LPProblem`.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
max_iter : int
|
|
35
|
+
Maximum number of pivots across both phases.
|
|
36
|
+
tol : float
|
|
37
|
+
Numerical tolerance for optimality, ratio, and feasibility tests.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, max_iter: int = 10_000, tol: float = 1e-9):
|
|
41
|
+
self.max_iter = max_iter
|
|
42
|
+
self.tol = tol
|
|
43
|
+
|
|
44
|
+
def solve(self, problem: LPProblem) -> OptimizeResult:
|
|
45
|
+
c, A, b = self._standard_form(problem)
|
|
46
|
+
m, n_total = A.shape
|
|
47
|
+
|
|
48
|
+
# Phase 1: minimize the sum of one artificial variable per row,
|
|
49
|
+
# starting from the all-artificial basis. Tableau layout: one row per
|
|
50
|
+
# constraint plus a reduced-cost row; last column is the RHS, and
|
|
51
|
+
# T[-1, -1] holds minus the current objective value.
|
|
52
|
+
T = np.zeros((m + 1, n_total + m + 1))
|
|
53
|
+
T[:m, :n_total] = A
|
|
54
|
+
T[:m, n_total:-1] = np.eye(m)
|
|
55
|
+
T[:m, -1] = b
|
|
56
|
+
T[-1, :n_total] = -A.sum(axis=0)
|
|
57
|
+
T[-1, -1] = -b.sum()
|
|
58
|
+
basis = list(range(n_total, n_total + m))
|
|
59
|
+
|
|
60
|
+
status, it1 = self._iterate(T, basis, self.max_iter)
|
|
61
|
+
if status == "maxiter":
|
|
62
|
+
return OptimizeResult(
|
|
63
|
+
x=None, fun=None, success=False,
|
|
64
|
+
message="Iteration limit reached in phase 1.", n_iter=it1,
|
|
65
|
+
)
|
|
66
|
+
if -T[-1, -1] > self.tol * (1.0 + np.abs(b).sum()):
|
|
67
|
+
return OptimizeResult(
|
|
68
|
+
x=None, fun=None, success=False,
|
|
69
|
+
message="Problem is infeasible.", n_iter=it1,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Drive leftover artificials out of the basis (they sit at value 0
|
|
73
|
+
# after a feasible phase 1); a row with no usable pivot is redundant.
|
|
74
|
+
drop = []
|
|
75
|
+
for i in range(m):
|
|
76
|
+
if basis[i] >= n_total:
|
|
77
|
+
j = int(np.argmax(np.abs(T[i, :n_total])))
|
|
78
|
+
if abs(T[i, j]) > self.tol:
|
|
79
|
+
_pivot(T, basis, i, j)
|
|
80
|
+
else:
|
|
81
|
+
drop.append(i)
|
|
82
|
+
if drop:
|
|
83
|
+
T = np.delete(T, drop, axis=0)
|
|
84
|
+
basis = [bj for i, bj in enumerate(basis) if i not in drop]
|
|
85
|
+
T = np.hstack([T[:, :n_total], T[:, -1:]])
|
|
86
|
+
|
|
87
|
+
# Phase 2: restore the original objective, priced out over the basis.
|
|
88
|
+
T[-1, :-1] = c
|
|
89
|
+
T[-1, -1] = 0.0
|
|
90
|
+
for i, j in enumerate(basis):
|
|
91
|
+
if c[j] != 0.0:
|
|
92
|
+
T[-1] -= c[j] * T[i]
|
|
93
|
+
|
|
94
|
+
status, it2 = self._iterate(T, basis, self.max_iter - it1)
|
|
95
|
+
n_iter = it1 + it2
|
|
96
|
+
if status == "unbounded":
|
|
97
|
+
return OptimizeResult(
|
|
98
|
+
x=None, fun=None, success=False,
|
|
99
|
+
message="Problem is unbounded.", n_iter=n_iter,
|
|
100
|
+
)
|
|
101
|
+
if status == "maxiter":
|
|
102
|
+
return OptimizeResult(
|
|
103
|
+
x=None, fun=None, success=False,
|
|
104
|
+
message="Iteration limit reached in phase 2.", n_iter=n_iter,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
x_full = np.zeros(n_total)
|
|
108
|
+
x_full[basis] = T[:-1, -1]
|
|
109
|
+
x = x_full[: problem.c.size]
|
|
110
|
+
return OptimizeResult(
|
|
111
|
+
x=x, fun=float(problem.c @ x), success=True,
|
|
112
|
+
message="Optimal solution found.", n_iter=n_iter,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _iterate(self, T: np.ndarray, basis: list[int], max_iter: int):
|
|
116
|
+
"""Run simplex pivots until optimal, unbounded, or out of budget."""
|
|
117
|
+
m = len(basis)
|
|
118
|
+
for it in range(max_iter):
|
|
119
|
+
negative = np.where(T[-1, :-1] < -self.tol)[0]
|
|
120
|
+
if negative.size == 0:
|
|
121
|
+
return "optimal", it
|
|
122
|
+
col = int(negative[0]) # Bland's rule: smallest index enters
|
|
123
|
+
column = T[:m, col]
|
|
124
|
+
eligible = column > self.tol
|
|
125
|
+
if not eligible.any():
|
|
126
|
+
return "unbounded", it
|
|
127
|
+
ratios = np.full(m, np.inf)
|
|
128
|
+
ratios[eligible] = T[:m, -1][eligible] / column[eligible]
|
|
129
|
+
ties = np.where(ratios <= ratios.min() + self.tol)[0]
|
|
130
|
+
row = int(min(ties, key=lambda i: basis[i])) # smallest leaves
|
|
131
|
+
_pivot(T, basis, row, col)
|
|
132
|
+
return "maxiter", max_iter
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _standard_form(problem: LPProblem):
|
|
136
|
+
"""Return ``(c, A, b)`` for the equivalent ``A x = b, x >= 0, b >= 0``.
|
|
137
|
+
|
|
138
|
+
One slack variable is appended per inequality row; rows with a
|
|
139
|
+
negative right-hand side are negated.
|
|
140
|
+
"""
|
|
141
|
+
n = problem.c.size
|
|
142
|
+
m_ub = 0 if problem.A_ub is None else problem.A_ub.shape[0]
|
|
143
|
+
rows, rhs = [], []
|
|
144
|
+
if m_ub:
|
|
145
|
+
rows.append(np.hstack([problem.A_ub, np.eye(m_ub)]))
|
|
146
|
+
rhs.append(problem.b_ub)
|
|
147
|
+
if problem.A_eq is not None:
|
|
148
|
+
m_eq = problem.A_eq.shape[0]
|
|
149
|
+
rows.append(np.hstack([problem.A_eq, np.zeros((m_eq, m_ub))]))
|
|
150
|
+
rhs.append(problem.b_eq)
|
|
151
|
+
if rows:
|
|
152
|
+
A = np.vstack(rows)
|
|
153
|
+
b = np.concatenate(rhs).astype(float)
|
|
154
|
+
else:
|
|
155
|
+
A = np.empty((0, n + m_ub))
|
|
156
|
+
b = np.empty(0)
|
|
157
|
+
neg = b < 0
|
|
158
|
+
A[neg] *= -1
|
|
159
|
+
b[neg] *= -1
|
|
160
|
+
c = np.concatenate([problem.c, np.zeros(m_ub)])
|
|
161
|
+
return c, A, b
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Numerical differentiation by central finite differences."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def finite_difference_gradient(
|
|
11
|
+
f: Callable[[np.ndarray], float],
|
|
12
|
+
x: np.ndarray,
|
|
13
|
+
h: float | None = None,
|
|
14
|
+
) -> np.ndarray:
|
|
15
|
+
"""Approximate the gradient of ``f`` at ``x`` by central differences.
|
|
16
|
+
|
|
17
|
+
Each component is estimated as
|
|
18
|
+
|
|
19
|
+
.. math::
|
|
20
|
+
|
|
21
|
+
\\partial_i f(x) \\approx
|
|
22
|
+
\\frac{f(x + h_i e_i) - f(x - h_i e_i)}{2 h_i},
|
|
23
|
+
|
|
24
|
+
with a per-coordinate step ``h_i = h * (1 + |x_i|)`` so the step scales
|
|
25
|
+
with the magnitude of ``x``. The truncation error is O(h^2), at the cost
|
|
26
|
+
of ``2 n`` evaluations of ``f``.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
f : callable
|
|
31
|
+
Scalar-valued function of a 1-D array.
|
|
32
|
+
x : np.ndarray, shape (n,)
|
|
33
|
+
Point at which to differentiate.
|
|
34
|
+
h : float, optional
|
|
35
|
+
Base step size. Defaults to ``eps ** (1/3)`` (about 6e-6), the
|
|
36
|
+
rule-of-thumb optimum balancing truncation and rounding error for
|
|
37
|
+
central differences.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
np.ndarray, shape (n,)
|
|
42
|
+
Gradient estimate.
|
|
43
|
+
"""
|
|
44
|
+
x = np.asarray(x, dtype=float)
|
|
45
|
+
if h is None:
|
|
46
|
+
h = float(np.finfo(float).eps) ** (1 / 3)
|
|
47
|
+
steps = h * (1.0 + np.abs(x))
|
|
48
|
+
g = np.empty(x.size)
|
|
49
|
+
for i in range(x.size):
|
|
50
|
+
e = np.zeros_like(x)
|
|
51
|
+
e[i] = steps[i]
|
|
52
|
+
g[i] = (f(x + e) - f(x - e)) / (2.0 * steps[i])
|
|
53
|
+
return g
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Nonlinear programming problem description."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from mopt.nonlinear.finite_diff import finite_difference_gradient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class NLPProblem:
|
|
15
|
+
"""An unconstrained nonlinear program.
|
|
16
|
+
|
|
17
|
+
Describes the problem
|
|
18
|
+
|
|
19
|
+
.. math::
|
|
20
|
+
|
|
21
|
+
\\min_{x \\in \\mathbb{R}^n} \\; f(x)
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
f : callable
|
|
26
|
+
Objective ``f(x) -> float`` for a 1-D array ``x``.
|
|
27
|
+
x0 : np.ndarray, shape (n,)
|
|
28
|
+
Starting point; also fixes the problem dimension.
|
|
29
|
+
grad : callable or None
|
|
30
|
+
Gradient ``grad(x) -> np.ndarray`` of shape (n,). When None,
|
|
31
|
+
:meth:`gradient` falls back to central finite differences of ``f``.
|
|
32
|
+
hess : callable or None
|
|
33
|
+
Hessian ``hess(x) -> np.ndarray`` of shape (n, n). Optional; needed
|
|
34
|
+
only by second-order solvers such as Newton's method.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
f: Callable[[np.ndarray], float]
|
|
38
|
+
x0: np.ndarray
|
|
39
|
+
grad: Callable[[np.ndarray], np.ndarray] | None = None
|
|
40
|
+
hess: Callable[[np.ndarray], np.ndarray] | None = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self):
|
|
43
|
+
if not callable(self.f):
|
|
44
|
+
raise TypeError("f must be callable.")
|
|
45
|
+
self.x0 = np.atleast_1d(np.asarray(self.x0, dtype=float))
|
|
46
|
+
if self.x0.ndim != 1:
|
|
47
|
+
raise ValueError("x0 must be a 1-D array.")
|
|
48
|
+
|
|
49
|
+
def gradient(self, x: np.ndarray) -> np.ndarray:
|
|
50
|
+
"""Gradient of ``f`` at ``x``.
|
|
51
|
+
|
|
52
|
+
Uses the user-supplied ``grad`` when given, otherwise central
|
|
53
|
+
finite differences of ``f``. Solvers should call this rather than
|
|
54
|
+
``grad`` directly so the fallback applies uniformly.
|
|
55
|
+
"""
|
|
56
|
+
if self.grad is not None:
|
|
57
|
+
return np.asarray(self.grad(x), dtype=float)
|
|
58
|
+
return finite_difference_gradient(self.f, x)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mopt
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Mathematical optimization solvers: linear programming, nonlinear programming, and more
|
|
5
|
+
Project-URL: Homepage, https://github.com/Trminh06-work/MOpt
|
|
6
|
+
Project-URL: Issues, https://github.com/Trminh06-work/MOpt/issues
|
|
7
|
+
Author-email: Trần Bảo Minh <trminh06.work@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: numpy>=1.24
|
|
16
|
+
Provides-Extra: torch
|
|
17
|
+
Requires-Dist: torch; extra == 'torch'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# MOpt
|
|
21
|
+
|
|
22
|
+
Mathematical optimization solvers in Python, implemented from scratch.
|
|
23
|
+
|
|
24
|
+
MOpt aims to cover the major classes of optimization problems behind a single,
|
|
25
|
+
consistent solver interface:
|
|
26
|
+
|
|
27
|
+
- **Linear programming** — Simplex method, and more
|
|
28
|
+
- **Nonlinear programming** — line search, gradient descent, Newton's method, and more
|
|
29
|
+
- **Planned** — convex optimization, metaheuristics, matheuristics
|
|
30
|
+
|
|
31
|
+
> ⚠️ Early development. The API is not yet stable and solvers are being added
|
|
32
|
+
> incrementally.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install mopt
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or install the latest development version from GitHub:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install git+https://github.com/Trminh06-work/MOpt.git
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
mopt/__init__.py,sha256=c3DcEd7efBYI5KbUz4QCLd6XjyKl6bws8hq9uHp49as,341
|
|
2
|
+
mopt/base_classes.py,sha256=-MjilgUoCRfVic1xHg4X8Fp5Xf-qGUuaquV3KVUPHrQ,1871
|
|
3
|
+
mopt/autodiff/__init__.py,sha256=J7S3MxhTNjhvHHMuVptjNEdyvpGuoNEXeBxqHhayM24,207
|
|
4
|
+
mopt/autodiff/torch_adapter.py,sha256=S0SxHR2wD7D_rUo1M7U44dZ9CqHLMy0CGKq7tj1GeU0,2315
|
|
5
|
+
mopt/linear/__init__.py,sha256=Rj3yfcXO59_KJaBuJlUCXQW83RV-l5auNyhYxlWzRyo,166
|
|
6
|
+
mopt/linear/problem.py,sha256=TiIaYBS3YaxraueTC4F1RY6dm-4Cmrc0lmqP6vwuCe4,1756
|
|
7
|
+
mopt/linear/simplex.py,sha256=I_8gcIz7I9cRGFJ9k1U6QqLItBSAtFuJfRkz0kRUzY8,6118
|
|
8
|
+
mopt/nonlinear/__init__.py,sha256=s0qiVw9a65iokxLF66xkz49GqfZKQKHN1loIAkCWWng,219
|
|
9
|
+
mopt/nonlinear/finite_diff.py,sha256=Xoo_-nBO7EiXP8gZCoN2LOKNS6sQ67j4iXv35JXSUak,1455
|
|
10
|
+
mopt/nonlinear/problem.py,sha256=h_pTKs95F741Tx9h8x37MUwIWAf13IT6YMkhU9t820E,1838
|
|
11
|
+
mopt-0.0.1.dist-info/METADATA,sha256=O8Q-7_OQRR46Ln-9uznXvkT-WkarpPpE95FUxmlC0O0,1452
|
|
12
|
+
mopt-0.0.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
13
|
+
mopt-0.0.1.dist-info/licenses/LICENSE,sha256=koiPNKlqANQS_0wef1_Koe6Yf5XchsqGw0bsVqZ95KY,1074
|
|
14
|
+
mopt-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trần Bảo Minh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|