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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyblocks
3
- Version: 0.2.0
3
+ Version: 0.3.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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polyblocks"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Solvers for global monotonic optimisation problems"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polyblocks"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Solvers for global monotonic optimisation problems"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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` and `H` a co-normal set given by `lb_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 smaller than `x_l` in any coordinate.
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 - x_l < 0).any():
238
- raise ValueError("`x_u` must be no smaller than `x_l` element-wise.")
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__(self, first: NDArray[np.float32 | np.float64]):
69
- """Initialise tree using the root node vertex."""
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, np.inf))
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(dim=lower.shape[0], dtype=lower.dtype)
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(np.inf)
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
- ## resolve erosion
31
- vert_offset = vertices + delta
32
- anchors = anchors - delta
30
+ ## feasible anchors for eroded set
31
+ offset_anchors = anchors - delta
33
32
 
34
33
  ## scale epsilon
35
- diff = anchors - vertices
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
- mask = oracle(diff * x + vert_offset)
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 + vertices
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
- reduced = reduced.copy()
56
- for ind in range(reduced.shape[-1]):
57
- c_end = root.copy()
58
- c_end[ind] = reduced[ind]
59
- reduced[ind] = monotone_proj(root[None], c_end[None], oracle, eps=eps)[0, ind]
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