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.
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyblocks
3
- Version: 0.1.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.10
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: **LINK**
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 y z
89
- # subject to x (y + z) <= 4,
90
- # y z <= 4,
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=10, # print progress every 10 iterations
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: **LINK**
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 y z
76
- # subject to x (y + z) <= 4,
77
- # y z <= 4,
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=10, # print progress every 10 iterations
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.1.0"
3
+ version = "0.2.0"
4
4
  description = "Solvers for global monotonic optimisation problems"
5
5
  readme = "README.md"
6
- requires-python = ">=3.10"
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.1.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.10"
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: Description of the termination 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: str = "Maximum iterations reached"
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 = "Problem Infeasible since `x_l` not in normal set."
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 = "Optimal!"
293
+ sol.status = Status.OPTIMAL
270
294
  else:
271
- sol.status = "Infeasible under current relaxation."
295
+ sol.status = Status.INFEASIBLE_RELAXATION
272
296
  break
273
297
  elif polyblock.size > cls.POLYBLOCK_LIMIT:
274
- sol.status = "Maximum polyblock size exceeded."
298
+ sol.status = Status.MAX_POLYBLOCK_SIZE
275
299
  break
276
300
  elif perf_counter() - start_time > time_limit:
277
- sol.status = "Time limit exceeded."
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"itr: {sol.n_iter}, "
297
- f"Best solution: {sol.x}, "
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:#.2g}"
94
+ value = f"{numerize(value, decimals=2)}"
94
95
  elif isinstance(value, float):
95
96
  value = f"{value:#.4g}"
96
97
  else: