polyblocks 0.2.0__tar.gz → 0.3.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.2.0 → polyblocks-0.3.0}/PKG-INFO +1 -1
- {polyblocks-0.2.0 → polyblocks-0.3.0}/pyproject.toml +1 -1
- {polyblocks-0.2.0 → polyblocks-0.3.0}/pyproject.toml.orig +1 -1
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/abstract.py +7 -6
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/containers.py +10 -6
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/naive.py +5 -4
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/tree.py +2 -2
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/utils.py +14 -17
- {polyblocks-0.2.0 → polyblocks-0.3.0}/README.md +0 -0
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/__init__.py +0 -0
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/jit_funcs.py +0 -0
- {polyblocks-0.2.0 → polyblocks-0.3.0}/src/polyblocks/py.typed +0 -0
|
@@ -60,7 +60,7 @@ class ABPolyblock(ABC):
|
|
|
60
60
|
"""
|
|
61
61
|
An abstract class for custom implementations of the Polyblock Outer-approximation (POA) algorithm.
|
|
62
62
|
|
|
63
|
-
POA maximises an increasing objective over `G ∩ H ∩ [x_l, x_u]`, where `G` is a normal set given by `ub_oracle
|
|
63
|
+
POA maximises an increasing objective over `G ∩ H ∩ [x_l, x_u]`, where `G` is a normal set given by `ub_oracle`, `H` a co-normal set given by `lb_oracle`, and `x_l < x_u`.
|
|
64
64
|
It does so by maintaining a *polyblock*: a finite vertex set `V` whose union of boxes `(-inf, v]` contains every feasible point which could still improve the incumbent.
|
|
65
65
|
Each iteration projects vertices of `V` onto the boundary of `G` and cuts the
|
|
66
66
|
infeasible cone strictly above each projection out of the polyblock, so the outer-approximation tightens until `V` empties and the incumbent is certified optimal.
|
|
@@ -83,7 +83,7 @@ class ABPolyblock(ABC):
|
|
|
83
83
|
POLYBLOCK_LIMIT = 2 * int(1e8)
|
|
84
84
|
|
|
85
85
|
@abstractmethod
|
|
86
|
-
def __init__(self, lower: NDArray, upper: NDArray) -> None:
|
|
86
|
+
def __init__(self, lower: NDArray, upper: NDArray, upper_obj: np.floating) -> None:
|
|
87
87
|
"""
|
|
88
88
|
Initialises polyblock representation using lower and upper points which define the feasible rectangle.
|
|
89
89
|
|
|
@@ -92,6 +92,7 @@ class ABPolyblock(ABC):
|
|
|
92
92
|
Args:
|
|
93
93
|
lower: Lower point of the feasible rectangle, of shape `(dim,)`.
|
|
94
94
|
upper: Upper point of the feasible rectangle, of shape `(dim,)`.
|
|
95
|
+
upper_obj: Objective value of the upper point.
|
|
95
96
|
"""
|
|
96
97
|
|
|
97
98
|
@abstractmethod
|
|
@@ -223,7 +224,7 @@ class ABPolyblock(ABC):
|
|
|
223
224
|
The best solution found, see `Solution`. Note that `success` reports whether the solver terminated on its own certificate rather than a limit, so it is also set when the problem is proven infeasible.
|
|
224
225
|
|
|
225
226
|
Raises:
|
|
226
|
-
ValueError: If `x_u` is
|
|
227
|
+
ValueError: If `x_u` is not greater than `x_l` in each coordinate.
|
|
227
228
|
"""
|
|
228
229
|
|
|
229
230
|
lb_exists = lb_oracle is not None
|
|
@@ -234,8 +235,8 @@ class ABPolyblock(ABC):
|
|
|
234
235
|
|
|
235
236
|
## initial checks
|
|
236
237
|
x_l, x_u = np.broadcast_arrays(x_l, x_u)
|
|
237
|
-
if (x_u
|
|
238
|
-
raise ValueError("`x_u` must be
|
|
238
|
+
if (x_u <= x_l).any():
|
|
239
|
+
raise ValueError("`x_u` must be greater than `x_l` element-wise.")
|
|
239
240
|
elif not ub_oracle(x_l[None]):
|
|
240
241
|
sol.success = True
|
|
241
242
|
sol.status = Status.INFEASIBLE_X_L
|
|
@@ -248,7 +249,7 @@ class ABPolyblock(ABC):
|
|
|
248
249
|
x_l = tighten(x_u, x_l, oracle=lb_oracle) if lb_exists else x_l.copy()
|
|
249
250
|
|
|
250
251
|
mono_proj = partial(monotone_proj, oracle=ub_oracle, eps=eps_ls, delta=delta)
|
|
251
|
-
polyblock = cls(x_l, x_u)
|
|
252
|
+
polyblock = cls(x_l, x_u, obj(x_u[None])[0])
|
|
252
253
|
min_obj = -np.inf
|
|
253
254
|
|
|
254
255
|
## main loop
|
|
@@ -13,10 +13,10 @@ class DynamicArray:
|
|
|
13
13
|
|
|
14
14
|
__slots__ = ("data", "length")
|
|
15
15
|
|
|
16
|
-
def __init__(self, dim=1, dtype: DTypeLike = float, start_sz=100):
|
|
17
|
-
"""Initialise dynamic container consisting of `dim` dimensional vectors of type `dtype`"""
|
|
16
|
+
def __init__(self, dim=1, dtype: DTypeLike = float, start_sz=100, keepdims=False):
|
|
17
|
+
"""Initialise dynamic container consisting of `dim` dimensional vectors of type `dtype`. Flattens array when `dim` is 1 and `keepdims` is False."""
|
|
18
18
|
|
|
19
|
-
size = (start_sz,) if dim == 1 else (start_sz, dim)
|
|
19
|
+
size = (start_sz,) if dim == 1 and not keepdims else (start_sz, dim)
|
|
20
20
|
self.data = np.empty(size, dtype=dtype)
|
|
21
21
|
self.length = 0
|
|
22
22
|
|
|
@@ -65,8 +65,12 @@ class Tree:
|
|
|
65
65
|
IDX_TYPE = np.int32
|
|
66
66
|
COMPONENT_TYPE = np.int8
|
|
67
67
|
|
|
68
|
-
def __init__(
|
|
69
|
-
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
first: NDArray[np.float32 | np.float64],
|
|
71
|
+
upper_obj: np.float32 | np.float64,
|
|
72
|
+
):
|
|
73
|
+
"""Initialise tree using the root node vertex and its objective value."""
|
|
70
74
|
|
|
71
75
|
self.first = first
|
|
72
76
|
|
|
@@ -81,7 +85,7 @@ class Tree:
|
|
|
81
85
|
|
|
82
86
|
## add first points
|
|
83
87
|
self.idx_range.append([-1, -1])
|
|
84
|
-
self.cvo.append((-1, -1,
|
|
88
|
+
self.cvo.append((-1, -1, upper_obj))
|
|
85
89
|
self.parent.append(-1)
|
|
86
90
|
|
|
87
91
|
def query(self, x: NDArray, lower: NDArray, min_obj=-np.inf, delta=1e-3) -> tuple:
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import numpy as np
|
|
2
1
|
from numpy.typing import NDArray
|
|
3
2
|
|
|
4
3
|
from .abstract import ABPolyblock
|
|
@@ -12,17 +11,19 @@ class BasePOA(ABPolyblock):
|
|
|
12
11
|
POLYBLOCK_LIMIT = 3 * int(1e6)
|
|
13
12
|
RHO = 0.2
|
|
14
13
|
|
|
15
|
-
def __init__(self, lower, upper):
|
|
14
|
+
def __init__(self, lower, upper, upper_obj):
|
|
16
15
|
self.lower = lower
|
|
17
16
|
const = self.RHO / (1 - self.RHO)
|
|
18
17
|
self.lower_offset = lower - const * (upper - lower).max()
|
|
19
18
|
self.lower_offset = self.lower_offset[None]
|
|
20
19
|
self.new: NDArray
|
|
21
20
|
|
|
22
|
-
self.vertices = DynamicArray(
|
|
21
|
+
self.vertices = DynamicArray(
|
|
22
|
+
dim=lower.shape[0], dtype=lower.dtype, keepdims=True
|
|
23
|
+
)
|
|
23
24
|
self.obj_vals = DynamicArray(dim=1, dtype=lower.dtype)
|
|
24
25
|
self.vertices.append(upper)
|
|
25
|
-
self.obj_vals.append(
|
|
26
|
+
self.obj_vals.append(upper_obj)
|
|
26
27
|
|
|
27
28
|
def projection_pairs(self) -> tuple[NDArray, NDArray]:
|
|
28
29
|
best_vtx = self.obj_vals.array.argmax()
|
|
@@ -35,9 +35,9 @@ class TreePOA(ABPolyblock):
|
|
|
35
35
|
REBUILD_GAP: int = 10000
|
|
36
36
|
PROJECTED_VERTICES: int = 8
|
|
37
37
|
|
|
38
|
-
def __init__(self, lower, upper) -> None:
|
|
38
|
+
def __init__(self, lower, upper, upper_obj) -> None:
|
|
39
39
|
self.lower = lower
|
|
40
|
-
self.tree = Tree(upper)
|
|
40
|
+
self.tree = Tree(upper, upper_obj)
|
|
41
41
|
self.best_obj = -np.inf
|
|
42
42
|
self.clean_counter = 0
|
|
43
43
|
|
|
@@ -7,8 +7,8 @@ from numpy.typing import NDArray
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def monotone_proj(
|
|
10
|
-
vertices: NDArray,
|
|
11
10
|
anchors: NDArray,
|
|
11
|
+
vertices: NDArray,
|
|
12
12
|
oracle: Callable,
|
|
13
13
|
eps=1e-4,
|
|
14
14
|
delta=0.0,
|
|
@@ -17,8 +17,8 @@ def monotone_proj(
|
|
|
17
17
|
Perform a vectorised bisection search to compute monotone projections onto the `delta`-eroded normal set corresponding to the given oracle.
|
|
18
18
|
|
|
19
19
|
Args:
|
|
20
|
-
vertices: Vertices to project using paired anchors, of shape `(num_pairs, dim)`.
|
|
21
20
|
anchors: Feasible anchor points for projecting paired vertex, of shape `(num_pairs, dim)`. Shifted by `delta` to account for erosion.
|
|
21
|
+
vertices: Vertices to project using paired anchors, of shape `(num_pairs, dim)`.
|
|
22
22
|
oracle: An oracle for querying normal set feasibility.
|
|
23
23
|
eps: The numerical tolerance for the line search.
|
|
24
24
|
delta: The erosion factor for the normal set.
|
|
@@ -27,37 +27,34 @@ def monotone_proj(
|
|
|
27
27
|
Batched monotone projections of shape `(num_pairs, dim)`.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
anchors = anchors - delta
|
|
30
|
+
## feasible anchors for eroded set
|
|
31
|
+
offset_anchors = anchors - delta
|
|
33
32
|
|
|
34
33
|
## scale epsilon
|
|
35
|
-
diff =
|
|
34
|
+
diff = vertices - offset_anchors
|
|
36
35
|
eps = eps / norm(diff, ord=2, axis=-1, keepdims=True)
|
|
37
36
|
|
|
38
37
|
ub = np.ones_like(eps)
|
|
39
|
-
lb = -eps
|
|
40
|
-
x = np.full_like(eps, 0.5)
|
|
38
|
+
lb = np.where(oracle(vertices + delta)[:, None], ub, -eps)
|
|
41
39
|
|
|
42
40
|
while (ub - lb > eps).any():
|
|
43
|
-
|
|
41
|
+
x = (ub + lb) / 2
|
|
42
|
+
mask = oracle(diff * x + anchors)
|
|
44
43
|
n_mask = ~mask
|
|
45
44
|
ub[n_mask] = x[n_mask]
|
|
46
45
|
lb[mask] = x[mask]
|
|
47
|
-
x = (ub + lb) / 2
|
|
48
46
|
|
|
49
|
-
return lb * diff +
|
|
47
|
+
return lb * diff + offset_anchors
|
|
50
48
|
|
|
51
49
|
|
|
52
50
|
def tighten(root: NDArray, reduced: NDArray, oracle: Callable, eps=1e-4) -> NDArray:
|
|
53
51
|
"""Tighten the box anchored at `root` using a monotone oracle."""
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return reduced
|
|
53
|
+
root = np.tile(root, (root.shape[0], 1))
|
|
54
|
+
diag = root.copy()
|
|
55
|
+
np.fill_diagonal(diag, reduced)
|
|
56
|
+
proj = monotone_proj(root, diag, oracle=oracle, eps=eps)
|
|
57
|
+
return proj.diagonal().copy()
|
|
61
58
|
|
|
62
59
|
|
|
63
60
|
def _center(s: str, width: int) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|