polyblocks 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {polyblocks-0.1.0 → polyblocks-0.2.0}/PKG-INFO +14 -12
- {polyblocks-0.1.0 → polyblocks-0.2.0}/README.md +11 -10
- {polyblocks-0.1.0 → polyblocks-0.2.0}/pyproject.toml +3 -2
- {polyblocks-0.1.0 → polyblocks-0.2.0}/pyproject.toml.orig +3 -2
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/__init__.py +2 -2
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/abstract.py +35 -9
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/utils.py +4 -3
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/containers.py +0 -0
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/jit_funcs.py +0 -0
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/naive.py +0 -0
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/py.typed +0 -0
- {polyblocks-0.1.0 → polyblocks-0.2.0}/src/polyblocks/tree.py +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polyblocks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Solvers for global monotonic optimisation problems
|
|
5
5
|
Author: Ahmed Rashwan
|
|
6
6
|
Author-email: Ahmed Rashwan <ar3009@bath.ac.uk>
|
|
7
7
|
License-Expression: MIT
|
|
8
8
|
Requires-Dist: numba>=0.65.1
|
|
9
|
+
Requires-Dist: numerize>=0.12
|
|
9
10
|
Requires-Dist: numpy>=2.2.6
|
|
10
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.11
|
|
11
12
|
Project-URL: Homepage, https://github.com/RashwanA/polyblocks
|
|
12
13
|
Description-Content-Type: text/markdown
|
|
13
14
|
|
|
@@ -19,9 +20,9 @@ This package provides a set of solvers for monotonic optimisation problems, whic
|
|
|
19
20
|
$$
|
|
20
21
|
\begin{aligned}
|
|
21
22
|
\max_{x \in \mathbb{R}^n} \quad & f(x) \\
|
|
22
|
-
\text{s.t.} \ \quad & x_l \le x \le x_u \\
|
|
23
|
-
& g(x) \le 0 \\
|
|
24
|
-
& h(x) \ge 0
|
|
23
|
+
\text{s.t.} \ \quad & x_l \le x \le x_u, \\
|
|
24
|
+
& g(x) \le 0, \\
|
|
25
|
+
& h(x) \ge 0,
|
|
25
26
|
\end{aligned}
|
|
26
27
|
$$
|
|
27
28
|
|
|
@@ -29,7 +30,7 @@ where $f$, $g$, and $h$ are non-decreasing in each coordinate.
|
|
|
29
30
|
This covers a broad class of non-convex problems, including polynomial programming along with many radio resource allocation problems in communications.
|
|
30
31
|
|
|
31
32
|
Solvers are built around the **Polyblock Outer-approximation (POA)** algorithm: a branch-and-bound algorithm which iteratively refines a rectangular outer-approximation of the solution space.
|
|
32
|
-
Full details are available in the accompanying paper
|
|
33
|
+
Full details are available in the [accompanying paper](https://arxiv.org/pdf/2608.13694).
|
|
33
34
|
|
|
34
35
|
## Installation
|
|
35
36
|
|
|
@@ -49,7 +50,7 @@ pip install .
|
|
|
49
50
|
|
|
50
51
|
The package includes three built-in solvers: `BasePOA`, `BalancedPOA`, and `TreePOA`.
|
|
51
52
|
Of these `TreePOA` typically has the best empirical performance as it uses an efficient tree-based representation of the solution space.
|
|
52
|
-
All solvers implement the common `ABPolyblock` interface, which exposes a set of subroutines used by the POA algorithm.
|
|
53
|
+
All solvers implement the common `ABPolyblock` interface, which exposes a set of subroutines used by the POA algorithm. See the [interface documentation](https://rashwana.github.io/polyblocks/interface/) for details.
|
|
53
54
|
Users may also define custom solvers by implementing this interface.
|
|
54
55
|
|
|
55
56
|
## Usage
|
|
@@ -77,6 +78,7 @@ sol = TreePOA.solve(
|
|
|
77
78
|
x_u=(1., 1.),
|
|
78
79
|
)
|
|
79
80
|
|
|
81
|
+
# solution: x=0.6, y=0.8
|
|
80
82
|
print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
|
|
81
83
|
```
|
|
82
84
|
|
|
@@ -85,9 +87,9 @@ Geometric programming example:
|
|
|
85
87
|
```python
|
|
86
88
|
from polyblocks import TreePOA
|
|
87
89
|
|
|
88
|
-
# maximise x
|
|
89
|
-
# subject to x
|
|
90
|
-
# y
|
|
90
|
+
# maximise x*y*z
|
|
91
|
+
# subject to x*(y + z) <= 4,
|
|
92
|
+
# y*z <= 4,
|
|
91
93
|
# x, y, z ∈ [0, 4]
|
|
92
94
|
|
|
93
95
|
def obj(x):
|
|
@@ -98,14 +100,14 @@ def ub_oracle(x):
|
|
|
98
100
|
cons2 = x[:,1:].prod(1) <= 4.
|
|
99
101
|
return cons1 * cons2
|
|
100
102
|
|
|
103
|
+
# solution: x=1, y=2, z=2
|
|
101
104
|
sol = TreePOA.solve(
|
|
102
105
|
obj=obj,
|
|
103
106
|
ub_oracle=ub_oracle,
|
|
104
107
|
x_l=(0., 0., 0.),
|
|
105
108
|
x_u=4., # bounds are broadcast
|
|
106
|
-
verbose_gap=
|
|
109
|
+
verbose_gap=50, # print progress every 50 iterations
|
|
107
110
|
)
|
|
108
|
-
print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
|
|
109
111
|
```
|
|
110
112
|
|
|
111
113
|
Objectives and oracles are expected to accept batched inputs of shape `(num_points, dim)`.
|
|
@@ -6,9 +6,9 @@ This package provides a set of solvers for monotonic optimisation problems, whic
|
|
|
6
6
|
$$
|
|
7
7
|
\begin{aligned}
|
|
8
8
|
\max_{x \in \mathbb{R}^n} \quad & f(x) \\
|
|
9
|
-
\text{s.t.} \ \quad & x_l \le x \le x_u \\
|
|
10
|
-
& g(x) \le 0 \\
|
|
11
|
-
& h(x) \ge 0
|
|
9
|
+
\text{s.t.} \ \quad & x_l \le x \le x_u, \\
|
|
10
|
+
& g(x) \le 0, \\
|
|
11
|
+
& h(x) \ge 0,
|
|
12
12
|
\end{aligned}
|
|
13
13
|
$$
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ where $f$, $g$, and $h$ are non-decreasing in each coordinate.
|
|
|
16
16
|
This covers a broad class of non-convex problems, including polynomial programming along with many radio resource allocation problems in communications.
|
|
17
17
|
|
|
18
18
|
Solvers are built around the **Polyblock Outer-approximation (POA)** algorithm: a branch-and-bound algorithm which iteratively refines a rectangular outer-approximation of the solution space.
|
|
19
|
-
Full details are available in the accompanying paper
|
|
19
|
+
Full details are available in the [accompanying paper](https://arxiv.org/pdf/2608.13694).
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
@@ -36,7 +36,7 @@ pip install .
|
|
|
36
36
|
|
|
37
37
|
The package includes three built-in solvers: `BasePOA`, `BalancedPOA`, and `TreePOA`.
|
|
38
38
|
Of these `TreePOA` typically has the best empirical performance as it uses an efficient tree-based representation of the solution space.
|
|
39
|
-
All solvers implement the common `ABPolyblock` interface, which exposes a set of subroutines used by the POA algorithm.
|
|
39
|
+
All solvers implement the common `ABPolyblock` interface, which exposes a set of subroutines used by the POA algorithm. See the [interface documentation](https://rashwana.github.io/polyblocks/interface/) for details.
|
|
40
40
|
Users may also define custom solvers by implementing this interface.
|
|
41
41
|
|
|
42
42
|
## Usage
|
|
@@ -64,6 +64,7 @@ sol = TreePOA.solve(
|
|
|
64
64
|
x_u=(1., 1.),
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
+
# solution: x=0.6, y=0.8
|
|
67
68
|
print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -72,9 +73,9 @@ Geometric programming example:
|
|
|
72
73
|
```python
|
|
73
74
|
from polyblocks import TreePOA
|
|
74
75
|
|
|
75
|
-
# maximise x
|
|
76
|
-
# subject to x
|
|
77
|
-
# y
|
|
76
|
+
# maximise x*y*z
|
|
77
|
+
# subject to x*(y + z) <= 4,
|
|
78
|
+
# y*z <= 4,
|
|
78
79
|
# x, y, z ∈ [0, 4]
|
|
79
80
|
|
|
80
81
|
def obj(x):
|
|
@@ -85,14 +86,14 @@ def ub_oracle(x):
|
|
|
85
86
|
cons2 = x[:,1:].prod(1) <= 4.
|
|
86
87
|
return cons1 * cons2
|
|
87
88
|
|
|
89
|
+
# solution: x=1, y=2, z=2
|
|
88
90
|
sol = TreePOA.solve(
|
|
89
91
|
obj=obj,
|
|
90
92
|
ub_oracle=ub_oracle,
|
|
91
93
|
x_l=(0., 0., 0.),
|
|
92
94
|
x_u=4., # bounds are broadcast
|
|
93
|
-
verbose_gap=
|
|
95
|
+
verbose_gap=50, # print progress every 50 iterations
|
|
94
96
|
)
|
|
95
|
-
print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
Objectives and oracles are expected to accept batched inputs of shape `(num_points, dim)`.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "polyblocks"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Solvers for global monotonic optimisation problems"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"numba>=0.65.1",
|
|
9
|
+
"numerize>=0.12",
|
|
9
10
|
"numpy>=2.2.6",
|
|
10
11
|
]
|
|
11
12
|
license = "MIT"
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "polyblocks"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Solvers for global monotonic optimisation problems"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "Ahmed Rashwan", email = "ar3009@bath.ac.uk" }
|
|
8
8
|
]
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"numba>=0.65.1",
|
|
12
|
+
"numerize>=0.12",
|
|
12
13
|
"numpy>=2.2.6",
|
|
13
14
|
]
|
|
14
15
|
license = "MIT"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from .abstract import ABPolyblock, Solution
|
|
1
|
+
from .abstract import ABPolyblock, Solution, Status
|
|
2
2
|
from .naive import BalancedPOA, BasePOA
|
|
3
3
|
from .tree import TreePOA
|
|
4
4
|
|
|
5
|
-
__all__ = ["ABPolyblock", "BalancedPOA", "BasePOA", "Solution", "TreePOA"]
|
|
5
|
+
__all__ = ["ABPolyblock", "BalancedPOA", "BasePOA", "Solution", "Status", "TreePOA"]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import Callable
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
4
5
|
from functools import partial
|
|
5
6
|
from time import perf_counter
|
|
6
7
|
|
|
@@ -10,6 +11,27 @@ from numpy.typing import ArrayLike, NDArray
|
|
|
10
11
|
from .utils import monotone_proj, print_row, tighten
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
class Status(StrEnum):
|
|
15
|
+
"""
|
|
16
|
+
Termination status of `ABPolyblock.solve`.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
MAX_ITER: Iteration limit reached before convergence.
|
|
20
|
+
INFEASIBLE_X_L: Problem infeasible since `x_l` not in normal set.
|
|
21
|
+
OPTIMAL: Solver converged and certified the incumbent as optimal.
|
|
22
|
+
INFEASIBLE_RELAXATION: No feasible point found under the current relaxation.
|
|
23
|
+
MAX_POLYBLOCK_SIZE: Polyblock size exceeded `POLYBLOCK_LIMIT`.
|
|
24
|
+
TIME_LIMIT: Time limit exceeded before convergence.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
MAX_ITER = "Maximum iterations reached."
|
|
28
|
+
INFEASIBLE_X_L = "Problem Infeasible since `x_l` not in normal set."
|
|
29
|
+
OPTIMAL = "Optimal!"
|
|
30
|
+
INFEASIBLE_RELAXATION = "Infeasible under current relaxation."
|
|
31
|
+
MAX_POLYBLOCK_SIZE = "Maximum polyblock size exceeded."
|
|
32
|
+
TIME_LIMIT = "Time limit exceeded."
|
|
33
|
+
|
|
34
|
+
|
|
13
35
|
@dataclass
|
|
14
36
|
class Solution:
|
|
15
37
|
"""
|
|
@@ -20,16 +42,18 @@ class Solution:
|
|
|
20
42
|
obj: Optimal (or best attained) objective value.
|
|
21
43
|
best_bound: Best upper-bound on the optimal objective.
|
|
22
44
|
success: Whether the solver returned an optimal solution.
|
|
23
|
-
status:
|
|
45
|
+
status: Termination status.
|
|
24
46
|
n_iter: Number of iterations executed.
|
|
47
|
+
runtime: Total runtime of the solver.
|
|
25
48
|
"""
|
|
26
49
|
|
|
27
50
|
x: NDArray | None = None
|
|
28
51
|
obj: float = -np.inf
|
|
29
52
|
best_bound: float = np.inf
|
|
30
53
|
success: bool = False
|
|
31
|
-
status:
|
|
54
|
+
status: Status = Status.MAX_ITER
|
|
32
55
|
n_iter: int = 0
|
|
56
|
+
runtime: float = 0.0
|
|
33
57
|
|
|
34
58
|
|
|
35
59
|
class ABPolyblock(ABC):
|
|
@@ -214,7 +238,7 @@ class ABPolyblock(ABC):
|
|
|
214
238
|
raise ValueError("`x_u` must be no smaller than `x_l` element-wise.")
|
|
215
239
|
elif not ub_oracle(x_l[None]):
|
|
216
240
|
sol.success = True
|
|
217
|
-
sol.status =
|
|
241
|
+
sol.status = Status.INFEASIBLE_X_L
|
|
218
242
|
if verbose:
|
|
219
243
|
print(sol.status)
|
|
220
244
|
return sol
|
|
@@ -266,15 +290,15 @@ class ABPolyblock(ABC):
|
|
|
266
290
|
if empty:
|
|
267
291
|
sol.success = True
|
|
268
292
|
if sol.x is not None:
|
|
269
|
-
sol.status =
|
|
293
|
+
sol.status = Status.OPTIMAL
|
|
270
294
|
else:
|
|
271
|
-
sol.status =
|
|
295
|
+
sol.status = Status.INFEASIBLE_RELAXATION
|
|
272
296
|
break
|
|
273
297
|
elif polyblock.size > cls.POLYBLOCK_LIMIT:
|
|
274
|
-
sol.status =
|
|
298
|
+
sol.status = Status.MAX_POLYBLOCK_SIZE
|
|
275
299
|
break
|
|
276
300
|
elif perf_counter() - start_time > time_limit:
|
|
277
|
-
sol.status =
|
|
301
|
+
sol.status = Status.TIME_LIMIT
|
|
278
302
|
break
|
|
279
303
|
|
|
280
304
|
if verbose and i % verbose_gap == 0:
|
|
@@ -290,11 +314,13 @@ class ABPolyblock(ABC):
|
|
|
290
314
|
|
|
291
315
|
sol.n_iter = i + 1
|
|
292
316
|
sol.best_bound = polyblock.best_bound
|
|
317
|
+
sol.runtime = perf_counter() - start_time
|
|
293
318
|
if verbose:
|
|
294
319
|
print(
|
|
295
320
|
f"{sol.status}, "
|
|
296
|
-
f"
|
|
297
|
-
f"
|
|
321
|
+
f"Iter: {sol.n_iter}, "
|
|
322
|
+
f"Runtime: {sol.runtime:.2f} s, "
|
|
323
|
+
f"Best solution: {sol.x.round(2)}, "
|
|
298
324
|
f"Obj: {sol.obj:.3f}"
|
|
299
325
|
)
|
|
300
326
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
|
+
from numerize.numerize import numerize
|
|
4
5
|
from numpy.linalg import norm
|
|
5
6
|
from numpy.typing import NDArray
|
|
6
7
|
|
|
@@ -11,7 +12,7 @@ def monotone_proj(
|
|
|
11
12
|
oracle: Callable,
|
|
12
13
|
eps=1e-4,
|
|
13
14
|
delta=0.0,
|
|
14
|
-
):
|
|
15
|
+
) -> NDArray:
|
|
15
16
|
"""
|
|
16
17
|
Perform a vectorised bisection search to compute monotone projections onto the `delta`-eroded normal set corresponding to the given oracle.
|
|
17
18
|
|
|
@@ -48,7 +49,7 @@ def monotone_proj(
|
|
|
48
49
|
return lb * diff + vertices
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def tighten(root: NDArray, reduced: NDArray, oracle: Callable, eps=1e-4):
|
|
52
|
+
def tighten(root: NDArray, reduced: NDArray, oracle: Callable, eps=1e-4) -> NDArray:
|
|
52
53
|
"""Tighten the box anchored at `root` using a monotone oracle."""
|
|
53
54
|
|
|
54
55
|
reduced = reduced.copy()
|
|
@@ -90,7 +91,7 @@ def print_row(*, header: bool = True, **columns):
|
|
|
90
91
|
cells = []
|
|
91
92
|
for name, value in columns.items():
|
|
92
93
|
if isinstance(value, int):
|
|
93
|
-
value = f"{value
|
|
94
|
+
value = f"{numerize(value, decimals=2)}"
|
|
94
95
|
elif isinstance(value, float):
|
|
95
96
|
value = f"{value:#.4g}"
|
|
96
97
|
else:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|