polyblocks 0.1.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.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: polyblocks
3
+ Version: 0.1.0
4
+ Summary: Solvers for global monotonic optimisation problems
5
+ Author: Ahmed Rashwan
6
+ Author-email: Ahmed Rashwan <ar3009@bath.ac.uk>
7
+ License-Expression: MIT
8
+ Requires-Dist: numba>=0.65.1
9
+ Requires-Dist: numpy>=2.2.6
10
+ Requires-Python: >=3.10
11
+ Project-URL: Homepage, https://github.com/RashwanA/polyblocks
12
+ Description-Content-Type: text/markdown
13
+
14
+ Polyblocks
15
+ =============
16
+
17
+ This package provides a set of solvers for monotonic optimisation problems, which take the general form:
18
+
19
+ $$
20
+ \begin{aligned}
21
+ \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
25
+ \end{aligned}
26
+ $$
27
+
28
+ where $f$, $g$, and $h$ are non-decreasing in each coordinate.
29
+ This covers a broad class of non-convex problems, including polynomial programming along with many radio resource allocation problems in communications.
30
+
31
+ 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
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install polyblocks
38
+ ```
39
+
40
+ or, for local development:
41
+
42
+ ```bash
43
+ git clone https://github.com/RashwanA/polyblocks.git
44
+ cd polyblocks
45
+ pip install .
46
+ ```
47
+
48
+ ## Solvers
49
+
50
+ The package includes three built-in solvers: `BasePOA`, `BalancedPOA`, and `TreePOA`.
51
+ 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
+ Users may also define custom solvers by implementing this interface.
54
+
55
+ ## Usage
56
+
57
+ Each solver is called via its `solve` classmethod, which takes the objective, the bounding box, and oracles for querying feasibility.
58
+
59
+ Ball optimisation example:
60
+
61
+ ```python
62
+ from polyblocks import TreePOA
63
+
64
+ # maximise 3x + 4y
65
+ # subject to x^2 + y^2 <= 1
66
+
67
+ def obj(x):
68
+ return x @ [3, 4]
69
+
70
+ def ub_oracle(x):
71
+ return (x ** 2).sum(-1) <= 1
72
+
73
+ sol = TreePOA.solve(
74
+ obj=obj,
75
+ ub_oracle=ub_oracle,
76
+ x_l=(0., 0.),
77
+ x_u=(1., 1.),
78
+ )
79
+
80
+ print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
81
+ ```
82
+
83
+ Geometric programming example:
84
+
85
+ ```python
86
+ from polyblocks import TreePOA
87
+
88
+ # maximise x y z
89
+ # subject to x (y + z) <= 4,
90
+ # y z <= 4,
91
+ # x, y, z ∈ [0, 4]
92
+
93
+ def obj(x):
94
+ return x.prod(1)
95
+
96
+ def ub_oracle(x):
97
+ cons1 = x[:,0] * (x[:,1:].sum(1)) <= 4.
98
+ cons2 = x[:,1:].prod(1) <= 4.
99
+ return cons1 * cons2
100
+
101
+ sol = TreePOA.solve(
102
+ obj=obj,
103
+ ub_oracle=ub_oracle,
104
+ x_l=(0., 0., 0.),
105
+ x_u=4., # bounds are broadcast
106
+ verbose_gap=10, # print progress every 10 iterations
107
+ )
108
+ print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
109
+ ```
110
+
111
+ Objectives and oracles are expected to accept batched inputs of shape `(num_points, dim)`.
@@ -0,0 +1,98 @@
1
+ Polyblocks
2
+ =============
3
+
4
+ This package provides a set of solvers for monotonic optimisation problems, which take the general form:
5
+
6
+ $$
7
+ \begin{aligned}
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
12
+ \end{aligned}
13
+ $$
14
+
15
+ where $f$, $g$, and $h$ are non-decreasing in each coordinate.
16
+ This covers a broad class of non-convex problems, including polynomial programming along with many radio resource allocation problems in communications.
17
+
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**
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install polyblocks
25
+ ```
26
+
27
+ or, for local development:
28
+
29
+ ```bash
30
+ git clone https://github.com/RashwanA/polyblocks.git
31
+ cd polyblocks
32
+ pip install .
33
+ ```
34
+
35
+ ## Solvers
36
+
37
+ The package includes three built-in solvers: `BasePOA`, `BalancedPOA`, and `TreePOA`.
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.
40
+ Users may also define custom solvers by implementing this interface.
41
+
42
+ ## Usage
43
+
44
+ Each solver is called via its `solve` classmethod, which takes the objective, the bounding box, and oracles for querying feasibility.
45
+
46
+ Ball optimisation example:
47
+
48
+ ```python
49
+ from polyblocks import TreePOA
50
+
51
+ # maximise 3x + 4y
52
+ # subject to x^2 + y^2 <= 1
53
+
54
+ def obj(x):
55
+ return x @ [3, 4]
56
+
57
+ def ub_oracle(x):
58
+ return (x ** 2).sum(-1) <= 1
59
+
60
+ sol = TreePOA.solve(
61
+ obj=obj,
62
+ ub_oracle=ub_oracle,
63
+ x_l=(0., 0.),
64
+ x_u=(1., 1.),
65
+ )
66
+
67
+ print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
68
+ ```
69
+
70
+ Geometric programming example:
71
+
72
+ ```python
73
+ from polyblocks import TreePOA
74
+
75
+ # maximise x y z
76
+ # subject to x (y + z) <= 4,
77
+ # y z <= 4,
78
+ # x, y, z ∈ [0, 4]
79
+
80
+ def obj(x):
81
+ return x.prod(1)
82
+
83
+ def ub_oracle(x):
84
+ cons1 = x[:,0] * (x[:,1:].sum(1)) <= 4.
85
+ cons2 = x[:,1:].prod(1) <= 4.
86
+ return cons1 * cons2
87
+
88
+ sol = TreePOA.solve(
89
+ obj=obj,
90
+ ub_oracle=ub_oracle,
91
+ x_l=(0., 0., 0.),
92
+ x_u=4., # bounds are broadcast
93
+ verbose_gap=10, # print progress every 10 iterations
94
+ )
95
+ print(f"status={sol.status}, obj={sol.obj:.2f}, x={sol.x.round(2)}")
96
+ ```
97
+
98
+ Objectives and oracles are expected to accept batched inputs of shape `(num_points, dim)`.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "polyblocks"
3
+ version = "0.1.0"
4
+ description = "Solvers for global monotonic optimisation problems"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "numba>=0.65.1",
9
+ "numpy>=2.2.6",
10
+ ]
11
+ license = "MIT"
12
+
13
+ [[project.authors]]
14
+ name = "Ahmed Rashwan"
15
+ email = "ar3009@bath.ac.uk"
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/RashwanA/polyblocks"
19
+
20
+ [build-system]
21
+ requires = ["uv_build>=0.9.4,<0.11.26"]
22
+ build-backend = "uv_build"
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "pandas>=2.3.0",
27
+ "pytest>=9.1.1",
28
+ ]
29
+ docs = [
30
+ "mkdocs>=1.6.1",
31
+ "mkdocs-material>=9.6.0",
32
+ "mkdocstrings[python]>=0.30.0",
33
+ ]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "polyblocks"
3
+ version = "0.1.0"
4
+ description = "Solvers for global monotonic optimisation problems"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Ahmed Rashwan", email = "ar3009@bath.ac.uk" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "numba>=0.65.1",
12
+ "numpy>=2.2.6",
13
+ ]
14
+ license = "MIT"
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/RashwanA/polyblocks"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.9.4,<0.11.26"]
21
+ build-backend = "uv_build"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pandas>=2.3.0",
26
+ "pytest>=9.1.1",
27
+ ]
28
+ docs = [
29
+ "mkdocs>=1.6.1",
30
+ "mkdocs-material>=9.6.0",
31
+ "mkdocstrings[python]>=0.30.0",
32
+ ]
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
@@ -0,0 +1,5 @@
1
+ from .abstract import ABPolyblock, Solution
2
+ from .naive import BalancedPOA, BasePOA
3
+ from .tree import TreePOA
4
+
5
+ __all__ = ["ABPolyblock", "BalancedPOA", "BasePOA", "Solution", "TreePOA"]
@@ -0,0 +1,301 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Callable
3
+ from dataclasses import dataclass
4
+ from functools import partial
5
+ from time import perf_counter
6
+
7
+ import numpy as np
8
+ from numpy.typing import ArrayLike, NDArray
9
+
10
+ from .utils import monotone_proj, print_row, tighten
11
+
12
+
13
+ @dataclass
14
+ class Solution:
15
+ """
16
+ The result returned by `ABPolyblock.solve`.
17
+
18
+ Attributes:
19
+ x: Optimal (or best attained) solution.
20
+ obj: Optimal (or best attained) objective value.
21
+ best_bound: Best upper-bound on the optimal objective.
22
+ success: Whether the solver returned an optimal solution.
23
+ status: Description of the termination status.
24
+ n_iter: Number of iterations executed.
25
+ """
26
+
27
+ x: NDArray | None = None
28
+ obj: float = -np.inf
29
+ best_bound: float = np.inf
30
+ success: bool = False
31
+ status: str = "Maximum iterations reached"
32
+ n_iter: int = 0
33
+
34
+
35
+ class ABPolyblock(ABC):
36
+ """
37
+ An abstract class for custom implementations of the Polyblock Outer-approximation (POA) algorithm.
38
+
39
+ 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`.
40
+ 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.
41
+ Each iteration projects vertices of `V` onto the boundary of `G` and cuts the
42
+ infeasible cone strictly above each projection out of the polyblock, so the outer-approximation tightens until `V` empties and the incumbent is certified optimal.
43
+
44
+ Child classes choose how that vertex set is stored and refined, and `solve` drives the representation through one cycle per iteration:
45
+
46
+ 1. `projection_pairs` selects the vertices to refine, each paired with a feasible anchor below it.
47
+ 2. `solve` projects each vertex towards its anchor onto the boundary of the eroded set `G_delta`, giving `proj`, and takes the shifted candidates `min(proj + delta, x_u)` as feasible solutions.
48
+ 3. `set_min_obj` is called with a new objective cut-off, but only when a candidate improves the incumbent.
49
+ 4. `new_vertices` cuts the cones above `proj` out of the polyblock and returns the replacement vertices.
50
+ 5. `solve` evaluates the objective on those vertices and passes a feasibility mask to `update`, which reports whether the vertex set is now exhausted.
51
+
52
+ Implementing `__init__`, `projection_pairs`, `set_min_obj`, `new_vertices`, and `update` is therefore enough to define a solver.
53
+ The `size` property is used for limiting memory usage, while the `best_bound` property is optional and only used for progress reporting.
54
+
55
+ Attributes:
56
+ POLYBLOCK_LIMIT: Maximum permitted `size` of the polyblock container.
57
+ """
58
+
59
+ POLYBLOCK_LIMIT = 2 * int(1e8)
60
+
61
+ @abstractmethod
62
+ def __init__(self, lower: NDArray, upper: NDArray) -> None:
63
+ """
64
+ Initialises polyblock representation using lower and upper points which define the feasible rectangle.
65
+
66
+ Both points have already been tightened against the oracles by `solve`, so the initial polyblock is the single vertex `upper`.
67
+
68
+ Args:
69
+ lower: Lower point of the feasible rectangle, of shape `(dim,)`.
70
+ upper: Upper point of the feasible rectangle, of shape `(dim,)`.
71
+ """
72
+
73
+ @abstractmethod
74
+ def projection_pairs(self) -> tuple[NDArray, NDArray]:
75
+ """
76
+ Returns pairs of anchors and vertices for computing monotone projections.
77
+
78
+ Called once at the start of each iteration to choose which vertices to refine.
79
+ The selection policy is free, subject only to the requirement that the *maximal* vertex, the one of greatest objective value in the polyblock, is always among those returned; convergence rests on it.
80
+ Returning several pairs cuts several cones per iteration, where downstream routines apply in a vectorised fashion.
81
+
82
+ Each anchor need only lie in `G` and strictly below its vertex; the `delta` shift keeping it feasible for the eroded set is applied downstream.
83
+
84
+ Returns:
85
+ A tuple `(anchors, vertices)`, each of shape `(num_pairs, dim)`:
86
+ anchors: Feasible points which project their paired vertex.
87
+ vertices: Polyblock vertices to project onto their paired anchor.
88
+ """
89
+
90
+ @abstractmethod
91
+ def set_min_obj(self, obj: float) -> None:
92
+ """
93
+ Update minimum objective value of future candidate solutions, discarding those which fall below it.
94
+
95
+ The polyblock only has to cover feasible points which beat the incumbent by the optimality tolerance, so a vertex falling below `obj` may be dropped: its objective bounds the whole box below it.
96
+ Called only when the incumbent improves, so `obj` increases monotonically over a solve.
97
+
98
+ Args:
99
+ obj: Objective cut-off for retained vertices.
100
+ """
101
+
102
+ @abstractmethod
103
+ def new_vertices(self, proj: NDArray, delta: float) -> NDArray:
104
+ """
105
+ Returns refined polyblock vertices to be checked for feasibility.
106
+
107
+ Cuts the cones above `proj` out of the polyblock, returning the vertices which replace those they remove.
108
+ A vertex lying above a projection is refined by reducing each of its components in turn to the matching component of that projection, giving up to `dim` replacements.
109
+ The choice of which vertices are refined, a vertex lying above several projections is handled is left to the implementation.
110
+ The implementation decides which vertices are refined, the projections used to refine them, and how redundancy checking is handled.
111
+
112
+ New vertiecs are internally retained, since `update` reports feasibility against it positionally.
113
+
114
+ Args:
115
+ proj: Monotone projections of this iteration's vertices, of shape `(num_pairs, dim)`.
116
+ delta: Erosion factor, giving the margin within which a vertex need not be refined for partial refinements.
117
+
118
+ Returns:
119
+ Candidate vertices of shape `(num_new, dim)`, pending the feasibility check in `update`.
120
+ All vertices should lie within the feasible box.
121
+ """
122
+
123
+ @abstractmethod
124
+ def update(self, new_mask: NDArray[np.bool], new_obj: NDArray) -> bool:
125
+ """
126
+ Updates internal polyblock representation after checking new vertex feasibility.
127
+
128
+ `new_mask` indexes the array returned by `new_vertices` in the same order, while `new_obj` holds objective values for the masked entries alone.
129
+ Masked-out vertices either violate the co-normal constraints or fall below the objective cut-off, and should be discarded rather than stored.
130
+
131
+ Args:
132
+ new_mask: Refined vertex feasibility, of shape `(num_new,)`.
133
+ new_obj: Objective values of feasible refined vertices, of shape `(new_mask.sum(),)`.
134
+
135
+ Returns:
136
+ True once the vertex set is exhausted, which terminates `solve` and certifies the incumbent as optimal, or the problem as infeasible if no candidate was ever found.
137
+ """
138
+
139
+ @property
140
+ @abstractmethod
141
+ def size(self) -> int:
142
+ """
143
+ Size of the container used to store polyblocks. Used for limiting memory usage.
144
+
145
+ Polled every iteration, and `solve` gives up once it exceeds `POLYBLOCK_LIMIT`.
146
+ Units are left to the implementation, as the limit is only ever compared against this property.
147
+ """
148
+
149
+ @property
150
+ def best_bound(self) -> float:
151
+ """
152
+ Best upper-bound on optimal objective value (optional).
153
+
154
+ Defaults to `nan` for implementations which do not track a bound.
155
+ This property is only used for reporting purposes and does not affect the solve.
156
+ """
157
+ return np.nan
158
+
159
+ @classmethod
160
+ def solve(
161
+ cls,
162
+ obj: Callable[[NDArray[np.floating]], NDArray[np.floating]],
163
+ x_l: ArrayLike,
164
+ x_u: ArrayLike,
165
+ ub_oracle: Callable[[NDArray[np.floating]], NDArray[np.bool]],
166
+ lb_oracle: Callable[[NDArray[np.floating]], NDArray[np.bool]] | None = None,
167
+ eps_obj_abs: float = 1e-6,
168
+ eps_obj_rel: float = 1e-2,
169
+ eps_ls: float = 1e-3,
170
+ delta: float = 1e-3,
171
+ verbose_gap: int | None = None,
172
+ time_limit: int = 3600,
173
+ iteration_limit: int = int(1e8),
174
+ ) -> Solution:
175
+ """
176
+ A template for the Polyblock Outer-approximation algorithm.
177
+
178
+ Maximises `obj` over the points of `[x_l, x_u]` accepted by both oracles.
179
+ The objective must be non-decreasing in every coordinate, while `ub_oracle` and `lb_oracle` should be boolean-valued functions querying upper- and lower-bound constraints, respectively.
180
+ All three functions are called with 2D arrays batching points along the first axis.
181
+
182
+ A returned solution is always feasible, and no feasible point beats it by more than the objective tolerances, except possibly points lying within `delta` of the upper-bound constraint boundary.
183
+
184
+ Args:
185
+ obj: Non-decreasing objective function.
186
+ x_l: Lower point defining feasible rectangle [x_l, x_u], must broadcast with `x_u` to a valid input for the objective and oracles.
187
+ x_u: Upper point defining feasible rectangle [x_l, x_u], must broadcast with `x_l` to a valid input for the objective and oracles.
188
+ ub_oracle: Oracle querying upper-bound (normal) constraints.
189
+ lb_oracle: Oracle querying lower-bound (co-normal) constraints (optional).
190
+ eps_obj_abs: Absolute tolerance between incumbent objective and upper-bound.
191
+ eps_obj_rel: Relative tolerance between incumbent objective and upper-bound.
192
+ eps_ls: Line-search tolerance.
193
+ delta: Optimality relaxation for upper-bound constraints. The objective is optimised only over the `delta` eroded normal set.
194
+ verbose_gap: Number of iterations between printout (optional).
195
+ iteration_limit: Maximum number of POA iterations before the solver gives up.
196
+ time_limit: Maximum solver runtime in seconds.
197
+
198
+ Returns:
199
+ 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.
200
+
201
+ Raises:
202
+ ValueError: If `x_u` is smaller than `x_l` in any coordinate.
203
+ """
204
+
205
+ lb_exists = lb_oracle is not None
206
+ verbose = verbose_gap is not None
207
+
208
+ ## returned solution
209
+ sol = Solution()
210
+
211
+ ## initial checks
212
+ x_l, x_u = np.broadcast_arrays(x_l, x_u)
213
+ if (x_u - x_l < 0).any():
214
+ raise ValueError("`x_u` must be no smaller than `x_l` element-wise.")
215
+ elif not ub_oracle(x_l[None]):
216
+ sol.success = True
217
+ sol.status = "Problem Infeasible since `x_l` not in normal set."
218
+ if verbose:
219
+ print(sol.status)
220
+ return sol
221
+
222
+ ## tighten initial box
223
+ x_u = tighten(x_l, x_u, oracle=ub_oracle)
224
+ x_l = tighten(x_u, x_l, oracle=lb_oracle) if lb_exists else x_l.copy()
225
+
226
+ mono_proj = partial(monotone_proj, oracle=ub_oracle, eps=eps_ls, delta=delta)
227
+ polyblock = cls(x_l, x_u)
228
+ min_obj = -np.inf
229
+
230
+ ## main loop
231
+ i = -1
232
+ start_time = perf_counter()
233
+ for i in range(iteration_limit):
234
+ ## compute projections from lower and upper points
235
+ lower_data, upper_data = polyblock.projection_pairs()
236
+ proj = mono_proj(lower_data, upper_data)
237
+ candidates = (proj + delta).clip(max=x_u)
238
+
239
+ ## find feasible candidates
240
+ feas_mask = (candidates >= x_l).all(-1)
241
+ if lb_exists:
242
+ feas_mask &= lb_oracle(candidates)
243
+
244
+ ## update best solution
245
+ if feas_mask.any():
246
+ cand_feas = candidates[feas_mask]
247
+ cand_obj = obj(cand_feas)
248
+ best_cand = cand_obj.argmax()
249
+ best_obj_cand = cand_obj[best_cand]
250
+ if best_obj_cand > sol.obj:
251
+ sol.obj = best_obj_cand.item()
252
+ sol.x = cand_feas[best_cand].copy()
253
+ min_obj = eps_obj_abs + sol.obj * (eps_obj_rel + 1)
254
+ polyblock.set_min_obj(min_obj)
255
+
256
+ ## update polyblock representation
257
+ new = polyblock.new_vertices(proj, delta=delta)
258
+ new_obj = obj(new).flatten()
259
+ new_mask = new_obj >= min_obj
260
+ if lb_exists:
261
+ new_mask &= lb_oracle(new)
262
+ new_obj = new_obj[new_mask]
263
+ empty = polyblock.update(new_mask, new_obj)
264
+
265
+ ## check termination
266
+ if empty:
267
+ sol.success = True
268
+ if sol.x is not None:
269
+ sol.status = "Optimal!"
270
+ else:
271
+ sol.status = "Infeasible under current relaxation."
272
+ break
273
+ elif polyblock.size > cls.POLYBLOCK_LIMIT:
274
+ sol.status = "Maximum polyblock size exceeded."
275
+ break
276
+ elif perf_counter() - start_time > time_limit:
277
+ sol.status = "Time limit exceeded."
278
+ break
279
+
280
+ if verbose and i % verbose_gap == 0:
281
+ header = i % (verbose_gap * 50) == 0
282
+ print_row(
283
+ header=header,
284
+ iter=i,
285
+ obj=sol.obj,
286
+ best_bound=polyblock.best_bound,
287
+ poly_size=polyblock.size,
288
+ dist_to_boundary=(upper_data - proj).mean().item(),
289
+ )
290
+
291
+ sol.n_iter = i + 1
292
+ sol.best_bound = polyblock.best_bound
293
+ if verbose:
294
+ print(
295
+ f"{sol.status}, "
296
+ f"itr: {sol.n_iter}, "
297
+ f"Best solution: {sol.x}, "
298
+ f"Obj: {sol.obj:.3f}"
299
+ )
300
+
301
+ return sol