One INIT: globmp/init.py serves the grid start and the synthetic study
Browse files- globmp/init.py: INIT (Algorithm 3) as one function, the shortest path over the joints for given
per-joint state sets, with exact piece tests, budget pruning (cost-to-go rule by default, plain
budget rule as an argument) and back-pointers.
- globmp/gridstart: instance.py supplies the grid values per joint, the piece cost and the piece
test (GridStates, grid_start); plain_grid.py holds the plain grid; init.py removed. reference/
reproduces (same z_h and test counts in 36 of 36 cases).
- globmp/synthetic: joints.py supplies the per-pair states (positions on a grid, zero derivatives),
piece cost, piece test and route label; init.py removed. Same class and joints as before on all
80 instances; INIT takes 0.5 to 1.3 s per instance (median per robot count).
- The name INIT is used everywhere; the ranking file is N*_i*_init.json.
- gridstart run.py --first-order calls the current Algorithm 2 again.
- README.md +3 -2
- globmp/gridstart/README.md +10 -6
- globmp/gridstart/instance.py +67 -1
- globmp/gridstart/{init.py → plain_grid.py} +3 -82
- globmp/gridstart/run.py +8 -8
- globmp/init.py +98 -0
- globmp/synthetic/README.md +20 -15
- globmp/synthetic/__init__.py +2 -2
- globmp/synthetic/{init.py → joints.py} +111 -100
- globmp/synthetic/reference/init_summary.json +1 -1
- globmp/synthetic/reference/smoke_reference.json +1 -1
- globmp/synthetic/report.py +2 -2
- globmp/synthetic/run.py +14 -14
- globmp/synthetic/smoke.py +3 -3
- globmp/tests/test_core.py +43 -0
|
@@ -28,8 +28,9 @@ globmp/ the method (pip install -e .)
|
|
| 28 |
certificate.py contacts, multipliers, global-optimality test
|
| 29 |
check.py exact continuous-time feasibility check
|
| 30 |
seed.py initial curves from waypoints
|
|
|
|
| 31 |
synthetic/ Section 4.1: robots exchanging places among spheres (2 to 200 robots)
|
| 32 |
-
gridstart/ Section 3
|
| 33 |
tests/ fast unit tests
|
| 34 |
applications/
|
| 35 |
multi_robot/ Section 4.2 and appendix: multi-robot planning
|
|
@@ -73,7 +74,7 @@ continuity `C^ell_j` at junctions.
|
|
| 73 |
|
| 74 |
| paper | code | check against the paper's runs |
|
| 75 |
|---|---|---|
|
| 76 |
-
| Sec. 4.1, Figs. 2-3 (pairs of robots among spheres, 2 to 200 robots; RRT-Connect, RRT*, GCS, SDP-MP) | `globmp/synthetic/` | default: INIT
|
| 77 |
| Sec. 3, Algorithm 3 (INIT, the grid start of Theorem 1) and its appendix table | `globmp/gridstart/` | start of the same cost as the plain grid in 27 of 27 cases (same curve except one tie); 16 free coordinates, 5.9e18 grid points: 208,929 exact piece tests |
|
| 78 |
| Sec. 4.2 and appendix (Robotarium arena; simulated suite) | `applications/multi_robot/robotarium/`, `applications/multi_robot/multi_robot_extension/` | suite cells: same success, path length to 3e-5; Robotarium: saved plans rescored to the paper's numbers on all 20 cells |
|
| 79 |
| appendix (Maze2D) | `applications/maze2d/` | score, success and violations identical; plans match the figure data to 1e-11 |
|
|
|
|
| 28 |
certificate.py contacts, multipliers, global-optimality test
|
| 29 |
check.py exact continuous-time feasibility check
|
| 30 |
seed.py initial curves from waypoints
|
| 31 |
+
init.py Algorithm 3 (INIT): shortest path over the joints for given per-joint state sets
|
| 32 |
synthetic/ Section 4.1: robots exchanging places among spheres (2 to 200 robots)
|
| 33 |
+
gridstart/ Section 3: INIT on the grid of Theorem 1 (the grid start), against the plain grid
|
| 34 |
tests/ fast unit tests
|
| 35 |
applications/
|
| 36 |
multi_robot/ Section 4.2 and appendix: multi-robot planning
|
|
|
|
| 74 |
|
| 75 |
| paper | code | check against the paper's runs |
|
| 76 |
|---|---|---|
|
| 77 |
+
| Sec. 4.1, Figs. 2-3 (pairs of robots among spheres, 2 to 200 robots; RRT-Connect, RRT*, GCS, SDP-MP) | `globmp/synthetic/` | default: INIT picks one route class, then one first-order run of Algorithms 1-2 (median 47 / 66 / 78 / 78 s per instance for 2 / 4 / 6 / 8 robots); 77 of 80 instances reach the cost of the all-class run within 1e-5, the other 3 are 0.2-3.0% above. All-class run (`--routes all`, Fig. 2): 2472 route classes, all feasible, median 58 / 76 / 79 / 95 s per class. Baselines bit-identical. Fig. 3 (scaling) is from the paper's runs, not rerun; 200 robots on CPU: 65 restoration iterations in 70 s (paper 71 s) |
|
| 78 |
| Sec. 3, Algorithm 3 (INIT, the grid start of Theorem 1) and its appendix table | `globmp/gridstart/` | start of the same cost as the plain grid in 27 of 27 cases (same curve except one tie); 16 free coordinates, 5.9e18 grid points: 208,929 exact piece tests |
|
| 79 |
| Sec. 4.2 and appendix (Robotarium arena; simulated suite) | `applications/multi_robot/robotarium/`, `applications/multi_robot/multi_robot_extension/` | suite cells: same success, path length to 3e-5; Robotarium: saved plans rescored to the paper's numbers on all 20 cells |
|
| 80 |
| appendix (Maze2D) | `applications/maze2d/` | score, success and violations identical; plans match the figure data to 1e-11 |
|
|
@@ -5,6 +5,8 @@ points. INIT (Algorithm 3, Section 3) computes the same `z_h` without listing th
|
|
| 5 |
per joint, a piece is determined by the values at its two ends, the cost is a sum over the pieces, and a curve is
|
| 6 |
feasible exactly when every piece is. INIT is the shortest path over the joints; it tests single pieces exactly and
|
| 7 |
skips a piece when the cost so far plus the piece cost exceeds a budget `U` (the cost of a known feasible curve).
|
|
|
|
|
|
|
| 8 |
|
| 9 |
Instance: one robot in the plane passes two discs, acceleration cost (k = 2), endpoint positions fixed, `N` cubic
|
| 10 |
pieces joined C^1, so 4N free coordinates. Budget `U = 4 E*_N`, where `E*_N` is the best certified cost over the
|
|
@@ -14,8 +16,9 @@ four route classes.
|
|
| 14 |
|
| 15 |
| file | content |
|
| 16 |
|---|---|
|
| 17 |
-
| `init.py` | INIT (
|
| 18 |
-
| `instance.py` | the instance, the chart of free control points, the cost, the exact test of one piece |
|
|
|
|
| 19 |
| `exact_repair.py` | the exact repair step (5) as a semidefinite program (Clarabel), and GlobMP with that repair |
|
| 20 |
| `run.py` | sanity checks, `E*_N`, one job per `(N, h)`, summary table |
|
| 21 |
| `reference/` | `optimum.json` and `summary.json` of the run below |
|
|
@@ -60,8 +63,9 @@ Counts are deterministic; times depend on the machine.
|
|
| 60 |
## Limits
|
| 61 |
|
| 62 |
- With one piece INIT is the plain grid. The work is at most `N S^2` piece tests, `S` the number of grid values at
|
| 63 |
-
one joint: linear in `N` and exponential in the number of coordinates per joint.
|
| 64 |
-
|
|
|
|
| 65 |
- `E*_N` is the best certified cost found, not a proved optimum.
|
| 66 |
-
- `--first-order` also runs the released Algorithms 1-2 from `z_h`.
|
| 67 |
-
|
|
|
|
| 5 |
per joint, a piece is determined by the values at its two ends, the cost is a sum over the pieces, and a curve is
|
| 6 |
feasible exactly when every piece is. INIT is the shortest path over the joints; it tests single pieces exactly and
|
| 7 |
skips a piece when the cost so far plus the piece cost exceeds a budget `U` (the cost of a known feasible curve).
|
| 8 |
+
The default rule of `globmp.init` also adds a lower bound on the cost of the remaining pieces (cost-to-go): same
|
| 9 |
+
curve, fewer piece tests.
|
| 10 |
|
| 11 |
Instance: one robot in the plane passes two discs, acceleration cost (k = 2), endpoint positions fixed, `N` cubic
|
| 12 |
pieces joined C^1, so 4N free coordinates. Budget `U = 4 E*_N`, where `E*_N` is the best certified cost over the
|
|
|
|
| 16 |
|
| 17 |
| file | content |
|
| 18 |
|---|---|
|
| 19 |
+
| `../init.py` | INIT (Algorithm 3), the package's one implementation: shortest path over the joints for given per-joint state sets |
|
| 20 |
+
| `instance.py` | the instance, the chart of free control points, the cost, the exact test of one piece; `GridStates` hands INIT the grid values per joint, the piece cost and the piece test (`grid_start`) |
|
| 21 |
+
| `plain_grid.py` | the plain grid (`plain_grid`), for comparison |
|
| 22 |
| `exact_repair.py` | the exact repair step (5) as a semidefinite program (Clarabel), and GlobMP with that repair |
|
| 23 |
| `run.py` | sanity checks, `E*_N`, one job per `(N, h)`, summary table |
|
| 24 |
| `reference/` | `optimum.json` and `summary.json` of the run below |
|
|
|
|
| 63 |
## Limits
|
| 64 |
|
| 65 |
- With one piece INIT is the plain grid. The work is at most `N S^2` piece tests, `S` the number of grid values at
|
| 66 |
+
one joint: linear in `N` and exponential in the number of coordinates per joint. On the team instances of
|
| 67 |
+
`globmp/synthetic/` a joint has 16 to 64 coordinates, and INIT runs there on smaller per-joint state sets
|
| 68 |
+
(`globmp/synthetic/joints.py`).
|
| 69 |
- `E*_N` is the best certified cost found, not a proved optimum.
|
| 70 |
+
- `--first-order` also runs the released Algorithms 1-2 (`globmp.synthetic.run.alg2`) from `z_h`. Checked at
|
| 71 |
+
`h = 0.5`: they end within 1e-5 of `E*_N` for N = 2 and 3.
|
|
@@ -10,9 +10,11 @@ Piece i has control points (Q_i, o_i, a_{i+1}, Q_{i+1}) with Q_0 = START, Q_N =
|
|
| 10 |
Time scale: the package gives every piece unit duration, so cost(total duration 1) = N^3 * cost(package).
|
| 11 |
|
| 12 |
Contents: the program (`build`), the chart, the cost as a quadratic (`Quad`), the exact test of one piece
|
| 13 |
-
(`pieces_free`),
|
|
|
|
| 14 |
"""
|
| 15 |
import math
|
|
|
|
| 16 |
|
| 17 |
import numpy as np
|
| 18 |
import torch
|
|
@@ -20,6 +22,7 @@ import torch
|
|
| 20 |
from .. import Problem, DT
|
| 21 |
from .. import spline as bern
|
| 22 |
from ..check import block_minima
|
|
|
|
| 23 |
|
| 24 |
torch.set_num_threads(1)
|
| 25 |
|
|
@@ -187,6 +190,69 @@ def pieces_free(B):
|
|
| 187 |
return ok, nfb
|
| 188 |
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
# ------------------------------------------------------------------ route classes and checks
|
| 191 |
def side_of(p, Z):
|
| 192 |
"""route class: side of each disc at the curve's closest approach ('p' above the centre)."""
|
|
|
|
| 10 |
Time scale: the package gives every piece unit duration, so cost(total duration 1) = N^3 * cost(package).
|
| 11 |
|
| 12 |
Contents: the program (`build`), the chart, the cost as a quadratic (`Quad`), the exact test of one piece
|
| 13 |
+
(`pieces_free`), what INIT needs (`GridStates`: the grid values per joint, the piece cost, the piece test; `grid_start`
|
| 14 |
+
calls globmp.init), route classes, and the exact rational check of a whole curve.
|
| 15 |
"""
|
| 16 |
import math
|
| 17 |
+
import time
|
| 18 |
|
| 19 |
import numpy as np
|
| 20 |
import torch
|
|
|
|
| 22 |
from .. import Problem, DT
|
| 23 |
from .. import spline as bern
|
| 24 |
from ..check import block_minima
|
| 25 |
+
from ..init import init
|
| 26 |
|
| 27 |
torch.set_num_threads(1)
|
| 28 |
|
|
|
|
| 190 |
return ok, nfb
|
| 191 |
|
| 192 |
|
| 193 |
+
# ------------------------------------------------------------------ INIT on the grid h Z^{4N}
|
| 194 |
+
def lattice(lo, hi, h):
|
| 195 |
+
ax = [np.arange(l, u + 1) * h for l, u in zip(lo, hi)]
|
| 196 |
+
return np.stack(np.meshgrid(*ax, indexing='ij'), -1).reshape(-1, len(ax))
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class GridStates:
|
| 200 |
+
"""The grid h Z^{4N} inside the bounding box of {f <= U}, as INIT's input (globmp.init): the state at joint j
|
| 201 |
+
is the grid value of (a_j, Q_j) (c0 at joint 0, a_N at joint N); piece i has control points
|
| 202 |
+
(Q_i, o_i, a_{i+1}, Q_{i+1}); its cost is the acceleration cost, its test is `pieces_free`."""
|
| 203 |
+
|
| 204 |
+
def __init__(self, N, h, U):
|
| 205 |
+
self.q = q = Quad(N); self.N = self.n_pieces = N; self.lo, self.hi = lo, hi = q.box(U, h)
|
| 206 |
+
self.T = [lattice(lo[idx_joint(j, N)], hi[idx_joint(j, N)], h) for j in range(N + 1)]
|
| 207 |
+
self.root_fallback = 0; self._piece = (None,)
|
| 208 |
+
|
| 209 |
+
def n_states(self, j): return len(self.T[j])
|
| 210 |
+
|
| 211 |
+
def _source(self, i, s):
|
| 212 |
+
"""(Q_i, o_i) of the states s at joint i."""
|
| 213 |
+
S = self.T[i][s]
|
| 214 |
+
return (np.broadcast_to(START, S.shape), S) if i == 0 else (S[:, 2:], 2 * S[:, 2:] - S[:, :2])
|
| 215 |
+
|
| 216 |
+
def _target(self, i):
|
| 217 |
+
"""(a_{i+1}, Q_{i+1}) of every state at joint i+1, the target part of the piece cost and the target factor
|
| 218 |
+
of its cross term."""
|
| 219 |
+
if self._piece[0] != i:
|
| 220 |
+
T = self.T[i + 1]; Ta, Tq = (T[:, :2], T[:, 2:]) if i + 1 < self.N else (T, np.broadcast_to(GOAL, T.shape))
|
| 221 |
+
v = np.stack([Ta, Tq], 1)
|
| 222 |
+
self._piece = (i, Ta, Tq, np.einsum('pq,kpc,kqc->k', K4[2:, 2:], v, v), v.reshape(len(v), 4))
|
| 223 |
+
return self._piece[1:]
|
| 224 |
+
|
| 225 |
+
def cost(self, i, s, V):
|
| 226 |
+
Ta, Tq, wt, vf = self._target(i); u = np.stack(self._source(i, s), 1)
|
| 227 |
+
ws = np.einsum('pq,kpc,kqc->k', K4[:2, :2], u, u); uK = np.einsum('kpc,pq->kqc', u, K4[:2, 2:]).reshape(len(u), 4)
|
| 228 |
+
tot = 2 * uK @ vf.T; tot += (V + ws)[:, None] + wt[None, :]
|
| 229 |
+
return tot
|
| 230 |
+
|
| 231 |
+
def free_pairs(self, i, s, t):
|
| 232 |
+
Sq, So = self._source(i, s); Ta, Tq = self._target(i)[:2]
|
| 233 |
+
ok, nfb = pieces_free(np.stack([Sq, So, Ta[t], Tq[t]], 1)); self.root_fallback += nfb
|
| 234 |
+
return ok
|
| 235 |
+
|
| 236 |
+
def togo(self, j): return self.q.togo(j, self.T[j])
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def grid_start(N, h, U, rule='togo', improve_only=True):
|
| 240 |
+
"""z_h by INIT (globmp.init) on the grid of spacing h with budget U. Returns the cheapest feasible grid point x
|
| 241 |
+
(or None = FAIL: no feasible grid point with cost <= U), its cost, and the counts."""
|
| 242 |
+
t0 = time.time(); g = GridStates(N, h, U)
|
| 243 |
+
r = init(g, U * (1 + 1e-12), rule=rule, improve_only=improve_only)
|
| 244 |
+
st = dict(layers=r['layers'], fail=not r['curves'], box=[(int(a), int(b)) for a, b in zip(g.lo, g.hi)],
|
| 245 |
+
root_fallback=g.root_fallback)
|
| 246 |
+
if not r['curves']: return None, None, dict(st, time=time.time() - t0)
|
| 247 |
+
cost, path = r['curves'][0]; x = np.zeros(4 * N)
|
| 248 |
+
for j in range(N + 1): x[idx_joint(j, N)] = g.T[j][path[j]]
|
| 249 |
+
npts = 1
|
| 250 |
+
for a, b in zip(g.lo, g.hi): npts *= int(b - a + 1)
|
| 251 |
+
st.update(time=time.time() - t0, grid_points_box=npts, tests=sum(l['tested'] for l in r['layers']),
|
| 252 |
+
pairs=sum(l['pairs'] for l in r['layers']), budget_pass=sum(l['budget_pass'] for l in r['layers']))
|
| 253 |
+
return x, cost, st
|
| 254 |
+
|
| 255 |
+
|
| 256 |
# ------------------------------------------------------------------ route classes and checks
|
| 257 |
def side_of(p, Z):
|
| 258 |
"""route class: side of each disc at the curve's closest approach ('p' above the centre)."""
|
|
@@ -1,88 +1,9 @@
|
|
| 1 |
-
"""
|
| 2 |
-
plain grid it replaces.
|
| 3 |
-
|
| 4 |
-
The grid h Z^{4N} in the free control points is a product of finite sets, one per joint. A piece is determined by
|
| 5 |
-
the values at its two ends, the cost is a sum over the pieces, and a curve is feasible exactly when every piece
|
| 6 |
-
is. `init` keeps, for every grid value at a joint, the cheapest feasible way to reach it.
|
| 7 |
-
"""
|
| 8 |
import time
|
| 9 |
|
| 10 |
import numpy as np
|
| 11 |
|
| 12 |
-
from .instance import
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def _lattice(lo, hi, h):
|
| 16 |
-
ax = [np.arange(l, u + 1) * h for l, u in zip(lo, hi)]
|
| 17 |
-
return np.stack(np.meshgrid(*ax, indexing='ij'), -1).reshape(-1, len(ax))
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def _pair_cost_parts(Sq, So, Ta, Tq):
|
| 21 |
-
"""cost of the piece with control points (Sq, So, Ta, Tq): source part (ns,), target part (nt,), and the
|
| 22 |
-
factors of the cross term."""
|
| 23 |
-
Kss = K4[:2, :2]; Ktt = K4[2:, 2:]; Kst = K4[:2, 2:]
|
| 24 |
-
u = np.stack([Sq, So], 1) # (ns, 2, 2) [which, coord]
|
| 25 |
-
v = np.stack([Ta, Tq], 1) # (nt, 2, 2)
|
| 26 |
-
ws = np.einsum('pq,kpc,kqc->k', Kss, u, u); wt = np.einsum('pq,kpc,kqc->k', Ktt, v, v)
|
| 27 |
-
uK = np.einsum('kpc,pq->kqc', u, Kst) # (ns, 2, 2)
|
| 28 |
-
return ws, wt, uK.reshape(len(u), 4), v.reshape(len(v), 4)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def init(N, h, U, rule='budget', improve_only=False, chunk_entries=2e7, verbose=False):
|
| 32 |
-
"""INIT. rule 'budget' (the rule of the paper's algorithm box): skip a pair when V(s) + w > U.
|
| 33 |
-
rule 'togo': skip when V(s) + w + togo(s') > U and drop states with V(s) + togo(s) > U, where togo is the
|
| 34 |
-
obstacle-free cost of the remaining pieces (a lower bound, so the output is the same). improve_only: also
|
| 35 |
-
skip pairs that cannot lower the current V(s') (same output). Returns the cheapest feasible grid point x
|
| 36 |
-
(or None = FAIL: no feasible grid point with cost <= U), its cost, and the counts."""
|
| 37 |
-
q = Quad(N); lo, hi = q.box(U, h); Ue = U * (1 + 1e-12)
|
| 38 |
-
t0 = time.time(); layers = []
|
| 39 |
-
s0 = _lattice(lo[:2], hi[:2], h)
|
| 40 |
-
V = np.zeros(len(s0)); Sq = np.broadcast_to(START, s0.shape).copy(); So = s0
|
| 41 |
-
states = [s0]; preds = []
|
| 42 |
-
if rule == 'togo':
|
| 43 |
-
keep = q.togo(0, s0) <= Ue; V = V[keep]; Sq = Sq[keep]; So = So[keep]; states[0] = s0[keep]
|
| 44 |
-
for i in range(N):
|
| 45 |
-
tl = time.time(); ij = idx_joint(i + 1, N); T = _lattice(lo[ij], hi[ij], h)
|
| 46 |
-
if i + 1 < N: Ta, Tq = T[:, :2], T[:, 2:]
|
| 47 |
-
else: Ta, Tq = T, np.broadcast_to(GOAL, T.shape).copy()
|
| 48 |
-
tg = q.togo(i + 1, T) if rule == 'togo' else np.zeros(len(T))
|
| 49 |
-
ws, wt, uK, vf = _pair_cost_parts(Sq, So, Ta, Tq)
|
| 50 |
-
Vn = np.full(len(T), np.inf); pr = np.full(len(T), -1, int)
|
| 51 |
-
st = dict(piece=i, sources=len(V), targets_box=len(T), pairs=len(V) * len(T), budget_pass=0, tested=0, free=0, root_fallback=0)
|
| 52 |
-
order = np.argsort(V, kind='stable'); cs = max(1, int(chunk_entries // max(1, len(T))))
|
| 53 |
-
for b in range(0, len(order), cs):
|
| 54 |
-
si = order[b:b + cs]
|
| 55 |
-
tot = V[si][:, None] + ws[si][:, None] + wt[None, :] + 2 * uK[si] @ vf.T # V(s) + w (line 3)
|
| 56 |
-
mask = tot + tg[None, :] <= Ue # budget rule (line 4)
|
| 57 |
-
st['budget_pass'] += int(mask.sum())
|
| 58 |
-
if improve_only: mask &= tot < Vn[None, :]
|
| 59 |
-
a, t = np.nonzero(mask)
|
| 60 |
-
if len(a) == 0: continue
|
| 61 |
-
B = np.stack([Sq[si][a], So[si][a], Ta[t], Tq[t]], 1)
|
| 62 |
-
ok, nfb = pieces_free(B) # exact test (line 5)
|
| 63 |
-
st['tested'] += len(a); st['free'] += int(ok.sum()); st['root_fallback'] += nfb
|
| 64 |
-
a, t = a[ok], t[ok]
|
| 65 |
-
if len(a) == 0: continue
|
| 66 |
-
val = tot[a, t]
|
| 67 |
-
o2 = np.lexsort((val, t)); t2 = t[o2]; first = np.r_[True, t2[1:] != t2[:-1]]
|
| 68 |
-
tt = t2[first]; vv = val[o2][first]; aa = si[a[o2][first]]
|
| 69 |
-
better = vv < Vn[tt]; Vn[tt[better]] = vv[better]; pr[tt[better]] = aa[better] # update V(s') (line 6)
|
| 70 |
-
live = np.isfinite(Vn)
|
| 71 |
-
st.update(live_targets=int(live.sum()), time=time.time() - tl); layers.append(st)
|
| 72 |
-
if verbose: print(f' N{N} h{h} piece {i}: {st}', flush=True)
|
| 73 |
-
if not live.any():
|
| 74 |
-
return None, None, dict(layers=layers, time=time.time() - t0, fail=True, box=[(int(a), int(b)) for a, b in zip(lo, hi)])
|
| 75 |
-
states.append(T[live]); preds.append(pr[live]); V = Vn[live]
|
| 76 |
-
if i + 1 < N: Sq = T[live][:, 2:]; So = 2 * T[live][:, 2:] - T[live][:, :2]
|
| 77 |
-
j = int(np.argmin(V)); cost = float(V[j]); x = np.zeros(4 * N)
|
| 78 |
-
for lev in range(N, -1, -1):
|
| 79 |
-
x[idx_joint(lev, N)] = states[lev][j]
|
| 80 |
-
if lev > 0: j = int(preds[lev - 1][j])
|
| 81 |
-
npts = 1
|
| 82 |
-
for a, b in zip(lo, hi): npts *= int(b - a + 1)
|
| 83 |
-
return x, cost, dict(layers=layers, time=time.time() - t0, fail=False, grid_points_box=npts,
|
| 84 |
-
tests=sum(l['tested'] for l in layers), pairs=sum(l['pairs'] for l in layers),
|
| 85 |
-
budget_pass=sum(l['budget_pass'] for l in layers), box=[(int(a), int(b)) for a, b in zip(lo, hi)])
|
| 86 |
|
| 87 |
|
| 88 |
def plain_grid(N, h, U, chunk_entries=2e7):
|
|
@@ -90,7 +11,7 @@ def plain_grid(N, h, U, chunk_entries=2e7):
|
|
| 90 |
the cheapest feasible one (or None), its cost, and the counts."""
|
| 91 |
q = Quad(N); lo, hi = q.box(U, h); m = 4 * N; t0 = time.time(); Ue = U * (1 + 1e-12)
|
| 92 |
inner = list(range(m - 4, m)); outer = list(range(m - 4))
|
| 93 |
-
Xi =
|
| 94 |
fi = 2 * Xi @ q.g[inner] + np.einsum('km,mn,kn->k', Xi, q.H[np.ix_(inner, inner)], Xi)
|
| 95 |
axes = [np.arange(l, u + 1) * h for l, u in zip(lo[outer], hi[outer])]
|
| 96 |
shape = [len(a) for a in axes]; n_outer = int(np.prod(shape)) if outer else 1
|
|
|
|
| 1 |
+
"""The plain grid that INIT replaces: every grid point with cost <= U gets a whole-curve test."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import time
|
| 3 |
|
| 4 |
import numpy as np
|
| 5 |
|
| 6 |
+
from .instance import Quad, ctrl, lattice, pieces_free
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def plain_grid(N, h, U, chunk_entries=2e7):
|
|
|
|
| 11 |
the cheapest feasible one (or None), its cost, and the counts."""
|
| 12 |
q = Quad(N); lo, hi = q.box(U, h); m = 4 * N; t0 = time.time(); Ue = U * (1 + 1e-12)
|
| 13 |
inner = list(range(m - 4, m)); outer = list(range(m - 4))
|
| 14 |
+
Xi = lattice(lo[inner], hi[inner], h)
|
| 15 |
fi = 2 * Xi @ q.g[inner] + np.einsum('km,mn,kn->k', Xi, q.H[np.ix_(inner, inner)], Xi)
|
| 16 |
axes = [np.arange(l, u + 1) * h for l, u in zip(lo[outer], hi[outer])]
|
| 17 |
shape = [len(a) for a in axes]; n_outer = int(np.prod(shape)) if outer else 1
|
|
@@ -2,8 +2,8 @@
|
|
| 2 |
|
| 3 |
check chart, cost, cost-to-go and piece-test sanity checks; INIT against the plain grid on three small cases
|
| 4 |
optimum E*_N for the chosen N: GlobMP with the exact repair from the four route-class seeds
|
| 5 |
-
job one run per (N, h): INIT with the
|
| 6 |
-
when it is small enough, then GlobMP with the exact repair from z_h
|
| 7 |
(--first-order: also the released Algorithms 1-2 from z_h)
|
| 8 |
summary table of all jobs (markdown on stdout, summary.json in the output directory)
|
| 9 |
|
|
@@ -22,8 +22,8 @@ from multiprocessing import Pool
|
|
| 22 |
import numpy as np
|
| 23 |
|
| 24 |
from .exact_repair import exact_descent
|
| 25 |
-
from .init import init, plain_grid
|
| 26 |
from .instance import *
|
|
|
|
| 27 |
|
| 28 |
REL = 1e-4 # success: final cost <= E*(1 + REL)
|
| 29 |
PLAIN_MAX_PAIRS = 1e11 # run the plain budget rule only below this many state pairs
|
|
@@ -50,9 +50,9 @@ def descend_exact(args):
|
|
| 50 |
|
| 51 |
|
| 52 |
def descend_first_order(args):
|
| 53 |
-
from ..synthetic.run import alg2
|
| 54 |
N, G, T, t_max = args; p = build(N, whiten=True); Z = G_to_Z(p, G)
|
| 55 |
-
Za, ok, log, hist = alg2(p, Z, p.tolvec()
|
| 56 |
out = dict(start_ok=bool(ok), iters=log.get('iters'), stop=log.get('stop'), time=log.get('time'), n_restore_fail=log.get('n_restore_fail'))
|
| 57 |
if ok: out.update(finish(p, Za))
|
| 58 |
return out
|
|
@@ -75,7 +75,7 @@ def stage_check(a):
|
|
| 75 |
for N, h, U in ((1, 1.0, 600.0), (2, 2.0, 80.0), (2, 1.25, 60.0)):
|
| 76 |
xb, cb, sb = plain_grid(N, h, U)
|
| 77 |
for rule, imp in (('budget', False), ('togo', True)):
|
| 78 |
-
xd, cd, sd =
|
| 79 |
print(f'N={N} h={h} U={U} rule={rule}: grid {cb} ({sb["grid_points_sublevel"]} pts, {sb["time"]:.1f}s) | INIT {cd} '
|
| 80 |
f'({sd.get("tests")} tests, {sd["time"]:.1f}s) same_cost={cb == cd or (cb is not None and abs(cb - cd) < 1e-9 * cb)} '
|
| 81 |
f'same_point={xb is not None and np.allclose(xb, xd)}', flush=True)
|
|
@@ -104,9 +104,9 @@ def job(args):
|
|
| 104 |
S = [float(np.prod(cnt[idx_joint(j, N)])) for j in range(N + 1)]
|
| 105 |
pairs = sum(S[j] * S[j + 1] for j in range(N)); npts = float(np.prod(cnt))
|
| 106 |
res = dict(N=N, h=h, U=U, E_star_routes=Es, states_per_joint=S, pairs=pairs, grid_points_box=npts, grid_points_sublevel_est=q.sublevel_count(U, h))
|
| 107 |
-
x, c, st =
|
| 108 |
if pairs <= PLAIN_MAX_PAIRS:
|
| 109 |
-
x2, c2, st2 =
|
| 110 |
res['budget_equals_togo'] = bool((x is None and x2 is None) or (x is not None and x2 is not None and abs(c - c2) <= 1e-9 * abs(c)))
|
| 111 |
if npts <= BRUTE_MAX_POINTS:
|
| 112 |
xb, cb, sb = plain_grid(N, h, U); res['grid'] = dict(cost=cb, x=None if xb is None else xb.tolist(), **sb)
|
|
|
|
| 2 |
|
| 3 |
check chart, cost, cost-to-go and piece-test sanity checks; INIT against the plain grid on three small cases
|
| 4 |
optimum E*_N for the chosen N: GlobMP with the exact repair from the four route-class seeds
|
| 5 |
+
job one run per (N, h): INIT (globmp.init) with the cost-to-go rule and with the plain budget rule, the
|
| 6 |
+
plain grid when it is small enough, then GlobMP with the exact repair from z_h
|
| 7 |
(--first-order: also the released Algorithms 1-2 from z_h)
|
| 8 |
summary table of all jobs (markdown on stdout, summary.json in the output directory)
|
| 9 |
|
|
|
|
| 22 |
import numpy as np
|
| 23 |
|
| 24 |
from .exact_repair import exact_descent
|
|
|
|
| 25 |
from .instance import *
|
| 26 |
+
from .plain_grid import plain_grid
|
| 27 |
|
| 28 |
REL = 1e-4 # success: final cost <= E*(1 + REL)
|
| 29 |
PLAIN_MAX_PAIRS = 1e11 # run the plain budget rule only below this many state pairs
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
def descend_first_order(args):
|
| 53 |
+
from ..synthetic.run import alg2
|
| 54 |
N, G, T, t_max = args; p = build(N, whiten=True); Z = G_to_Z(p, G)
|
| 55 |
+
Za, ok, log, hist = alg2(p, Z, p.tolvec(), t_max=t_max, T=T)
|
| 56 |
out = dict(start_ok=bool(ok), iters=log.get('iters'), stop=log.get('stop'), time=log.get('time'), n_restore_fail=log.get('n_restore_fail'))
|
| 57 |
if ok: out.update(finish(p, Za))
|
| 58 |
return out
|
|
|
|
| 75 |
for N, h, U in ((1, 1.0, 600.0), (2, 2.0, 80.0), (2, 1.25, 60.0)):
|
| 76 |
xb, cb, sb = plain_grid(N, h, U)
|
| 77 |
for rule, imp in (('budget', False), ('togo', True)):
|
| 78 |
+
xd, cd, sd = grid_start(N, h, U, rule=rule, improve_only=imp)
|
| 79 |
print(f'N={N} h={h} U={U} rule={rule}: grid {cb} ({sb["grid_points_sublevel"]} pts, {sb["time"]:.1f}s) | INIT {cd} '
|
| 80 |
f'({sd.get("tests")} tests, {sd["time"]:.1f}s) same_cost={cb == cd or (cb is not None and abs(cb - cd) < 1e-9 * cb)} '
|
| 81 |
f'same_point={xb is not None and np.allclose(xb, xd)}', flush=True)
|
|
|
|
| 104 |
S = [float(np.prod(cnt[idx_joint(j, N)])) for j in range(N + 1)]
|
| 105 |
pairs = sum(S[j] * S[j + 1] for j in range(N)); npts = float(np.prod(cnt))
|
| 106 |
res = dict(N=N, h=h, U=U, E_star_routes=Es, states_per_joint=S, pairs=pairs, grid_points_box=npts, grid_points_sublevel_est=q.sublevel_count(U, h))
|
| 107 |
+
x, c, st = grid_start(N, h, U, rule='togo', improve_only=True); res['init_togo'] = dict(cost=c, x=None if x is None else x.tolist(), **st)
|
| 108 |
if pairs <= PLAIN_MAX_PAIRS:
|
| 109 |
+
x2, c2, st2 = grid_start(N, h, U, rule='budget', improve_only=False); res['init_budget'] = dict(cost=c2, x=None if x2 is None else x2.tolist(), **st2)
|
| 110 |
res['budget_equals_togo'] = bool((x is None and x2 is None) or (x is not None and x2 is not None and abs(c - c2) <= 1e-9 * abs(c)))
|
| 111 |
if npts <= BRUTE_MAX_POINTS:
|
| 112 |
xb, cb, sb = plain_grid(N, h, U); res['grid'] = dict(cost=cb, x=None if xb is None else xb.tolist(), **sb)
|
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""INIT (Algorithm 3 of the paper): the cheapest feasible curve over finite per-joint state sets, by a shortest
|
| 2 |
+
path over the joints.
|
| 3 |
+
|
| 4 |
+
A candidate curve is one state per joint. Piece i is determined by the states at joints i and i+1, the cost is a
|
| 5 |
+
sum over the pieces, and a curve is feasible exactly when every piece is. INIT keeps, for every state at a joint,
|
| 6 |
+
the cheapest feasible way to reach it (V) and a back-pointer. A pair (s, s') is skipped without a piece test when
|
| 7 |
+
V(s) + w(s, s') exceeds the budget U (the cost of a known feasible curve). With the cost-to-go rule the test is
|
| 8 |
+
V(s) + w(s, s') + togo(s') > U, where togo(s') is a lower bound on the cost of the remaining pieces; it skips more
|
| 9 |
+
pairs and returns the same curve.
|
| 10 |
+
|
| 11 |
+
The instance supplies the state sets, the piece cost and the exact piece test (an object `inst`):
|
| 12 |
+
|
| 13 |
+
inst.n_pieces M; the joints are 0..M
|
| 14 |
+
inst.n_states(j) number of states at joint j
|
| 15 |
+
inst.cost(i, s, V) V(s) + w(s, s') for the states s (indices at joint i, costs so far V) and every state s'
|
| 16 |
+
at joint i+1, w the piece cost: float (len(s), n_states(i+1))
|
| 17 |
+
inst.free(i, s) exact test of piece i: bool (len(s), n_states(i+1)), True where the piece from s to s' is
|
| 18 |
+
feasible (for tests that are table look-ups); or instead
|
| 19 |
+
inst.free_pairs(i, s, t) the same test for the listed pairs (s[k], t[k]) only: bool (len(s),) (for tests that
|
| 20 |
+
cost work per pair; INIT then lists the pairs that pass the budget)
|
| 21 |
+
inst.togo(j) cost-to-go lower bound of every state at joint j, (n_states(j),) (rule 'togo')
|
| 22 |
+
inst.n_labels, inst.label(i, s, lab) (optional)
|
| 23 |
+
a state can carry a label fixed by the pieces before it (e.g. the route class so far):
|
| 24 |
+
label returns the label of the end state of every pair, int (len(s), n_states(i+1)),
|
| 25 |
+
or (len(s), 1) on a piece that changes no label.
|
| 26 |
+
INIT then keeps V per (label, state) and returns the cheapest curve per final label.
|
| 27 |
+
"""
|
| 28 |
+
import time
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def init(inst, U=np.inf, rule='togo', improve_only=True, ties='first', chunk=2e7):
|
| 34 |
+
"""INIT. rule 'togo' (default): budget with cost-to-go; 'budget': the plain budget rule V(s) + w > U (same
|
| 35 |
+
curve, more piece tests). improve_only: also skip pairs that cannot lower the current V(s') (same curve).
|
| 36 |
+
Sources are processed cheapest first, `chunk` matrix entries at a time. ties: among ways of equal cost into a
|
| 37 |
+
state, keep the first one processed ('first', fewest piece tests) or the one from the lowest (label, state)
|
| 38 |
+
index ('state', independent of the processing order).
|
| 39 |
+
|
| 40 |
+
Returns dict(curves, layers, time): curves = {label: (cost, [state index at joint 0..M])}, the cheapest
|
| 41 |
+
feasible curve with cost <= U per final label (empty = FAIL); layers = per-piece counts (sources, targets,
|
| 42 |
+
pairs, budget_pass, tested, free, live)."""
|
| 43 |
+
M = inst.n_pieces; L = getattr(inst, 'n_labels', 1); label = getattr(inst, 'label', None); t0 = time.time()
|
| 44 |
+
pairs = hasattr(inst, 'free_pairs'); first = ties == 'first'
|
| 45 |
+
idx = np.arange(inst.n_states(0)); lab = np.zeros(len(idx), np.int64); V = np.zeros(len(idx))
|
| 46 |
+
if rule == 'togo':
|
| 47 |
+
keep = inst.togo(0) <= U; idx, lab, V = idx[keep], lab[keep], V[keep]
|
| 48 |
+
back = [(idx, lab, None)]; layers = []
|
| 49 |
+
for i in range(M):
|
| 50 |
+
tl = time.time(); nt = inst.n_states(i + 1); cols = np.arange(nt)
|
| 51 |
+
tg = inst.togo(i + 1)[None, :] if rule == 'togo' else 0.0
|
| 52 |
+
Vn = np.full((L, nt), np.inf); pred = np.full((L, nt), -1, np.int64)
|
| 53 |
+
st = dict(piece=i, sources=len(V), targets=nt, pairs=len(V) * nt, budget_pass=0, tested=0, free=0)
|
| 54 |
+
od = np.argsort(V, kind='stable'); cs = max(1, int(chunk // max(1, nt)))
|
| 55 |
+
for b in range(0, len(od), cs):
|
| 56 |
+
si = od[b:b + cs] if first else np.sort(od[b:b + cs]); s = idx[si]
|
| 57 |
+
tot = inst.cost(i, s, V[si]) # V(s) + w(s, s')
|
| 58 |
+
need = tot + tg <= U if U < np.inf else np.ones(tot.shape, bool) # budget rule
|
| 59 |
+
st['budget_pass'] += int(np.count_nonzero(need))
|
| 60 |
+
lb = label(i, s, lab[si]) if label else None
|
| 61 |
+
if improve_only:
|
| 62 |
+
cur = Vn[0][None, :] if lb is None else Vn[lb[:, 0]] if lb.shape[1] == 1 else Vn[lb, cols[None, :]]
|
| 63 |
+
need &= (tot < cur) if first else (tot <= cur)
|
| 64 |
+
n_need = int(np.count_nonzero(need)); st['tested'] += n_need
|
| 65 |
+
if n_need == 0: continue
|
| 66 |
+
if pairs: # exact piece test, listed pairs
|
| 67 |
+
a, t = np.nonzero(need); ok = inst.free_pairs(i, s[a], t); a, t = a[ok], t[ok]; st['free'] += len(a)
|
| 68 |
+
else: # exact piece test, whole rows
|
| 69 |
+
ok = inst.free(i, s) & need; n_ok = int(np.count_nonzero(ok)); st['free'] += n_ok
|
| 70 |
+
if n_ok * 20 >= ok.size: # many pairs: reduce over the matrix
|
| 71 |
+
val = np.where(ok, tot, np.inf)
|
| 72 |
+
if lb is not None and lb.shape[1] == 1: lb = np.broadcast_to(lb, ok.shape)
|
| 73 |
+
for l in ([0] if lb is None else np.flatnonzero(np.bincount(lb[ok], minlength=L))):
|
| 74 |
+
vl = val if lb is None else np.where(lb == l, val, np.inf)
|
| 75 |
+
a = vl.argmin(0); v = vl[a, cols]; better = v < Vn[l]
|
| 76 |
+
if not first: better |= (v == Vn[l]) & (si[a] < pred[l])
|
| 77 |
+
Vn[l, better] = v[better]; pred[l, better] = si[a[better]]
|
| 78 |
+
continue
|
| 79 |
+
a, t = np.nonzero(ok)
|
| 80 |
+
if len(a) == 0: continue
|
| 81 |
+
val = tot[a, t]; k = t if lb is None else lb[a, t if lb.shape[1] > 1 else 0] * nt + t # key: (label, s')
|
| 82 |
+
o = np.lexsort((val, k)); k = k[o]; head = np.r_[True, k[1:] != k[:-1]] # cheapest per key
|
| 83 |
+
a, k, val = a[o][head], k[head], val[o][head]
|
| 84 |
+
better = val < Vn.ravel()[k]
|
| 85 |
+
if not first: better |= (val == Vn.ravel()[k]) & (si[a] < pred.ravel()[k])
|
| 86 |
+
Vn.ravel()[k[better]] = val[better]; pred.ravel()[k[better]] = si[a[better]]
|
| 87 |
+
ll, tt = np.nonzero(np.isfinite(Vn))
|
| 88 |
+
st.update(live=len(tt), time=time.time() - tl); layers.append(st)
|
| 89 |
+
if len(tt) == 0: return dict(curves={}, layers=layers, time=time.time() - t0)
|
| 90 |
+
idx, lab, V = tt, ll, Vn[ll, tt]; back.append((idx, lab, pred[ll, tt]))
|
| 91 |
+
curves = {}
|
| 92 |
+
for l in np.unique(lab):
|
| 93 |
+
j = int(np.flatnonzero(lab == l)[np.argmin(V[lab == l])]); cost = float(V[j]); path = [0] * (M + 1)
|
| 94 |
+
for lev in range(M, -1, -1):
|
| 95 |
+
path[lev] = int(back[lev][0][j])
|
| 96 |
+
if lev > 0: j = int(back[lev][2][j])
|
| 97 |
+
curves[int(l)] = (cost, path)
|
| 98 |
+
return dict(curves=curves, layers=layers, time=time.time() - t0)
|
|
@@ -4,9 +4,11 @@ Robots exchange places in pairs among static spheres (Problem B of the paper): m
|
|
| 4 |
degree-9 polynomials, C^3 junctions, derivatives 1..4 zero at both ends, inflation margin 0.5 cm, no workspace
|
| 5 |
bound. A route class fixes which side each pair and each crossed sphere is passed on.
|
| 6 |
|
| 7 |
-
GlobMP here: INIT
|
| 8 |
-
Algorithms 1-2 from the INIT
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
Baselines: RRT-Connect, RRT*, GCS on IRIS regions (their paths fitted by the cheapest collision-free curve of the
|
| 11 |
same class) and SDP-MP (Graesdal et al.).
|
| 12 |
|
|
@@ -15,8 +17,8 @@ same class) and SDP-MP (Graesdal et al.).
|
|
| 15 |
| file | content |
|
| 16 |
|---|---|
|
| 17 |
| `instances.py` | instance generators (`instance`: main comparison, M = 7, two spheres; `instance_scale`: scaling study, M = 4, three spheres), the program (`build`), route classes and their waypoint paths, exact evaluation on the true geometry |
|
| 18 |
-
| `
|
| 19 |
-
| `run.py` | GlobMP on a main-comparison instance: INIT
|
| 20 |
| `scale.py` | GlobMP end to end on a scaling instance, timed from process start; data of Fig. `fig_synth2` (GlobMP, panels a-c) |
|
| 21 |
| `baselines.py` | RRT-Connect, RRT* (OMPL), GCS on IRIS (Drake), SDP-MP (cvxpy + Clarabel), and the post-processing of planner paths |
|
| 22 |
| `report.py`, `figures.py` | tables and quoted numbers; `fig_synth1.pdf/png` and `fig_synth2.pdf/png` |
|
|
@@ -35,7 +37,7 @@ when it is installed, else Nimbus Roman or STIX. Every run is single-threaded. R
|
|
| 35 |
```bash
|
| 36 |
export PYTHONPATH=. OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1
|
| 37 |
|
| 38 |
-
# GlobMP: INIT
|
| 39 |
python -m globmp.synthetic.run --N 6 --inst $(seq 0 19) --jobs 16 --out results/init
|
| 40 |
|
| 41 |
# GlobMP on every route class (reproduces reference/ and fig_synth1)
|
|
@@ -60,17 +62,17 @@ python -m globmp.synthetic.figures --res $R --out figures
|
|
| 60 |
python -m globmp.synthetic.smoke --out results/smoke --jobs 8 # add --full for the 4-robot instance
|
| 61 |
```
|
| 62 |
|
| 63 |
-
`--top K` solves the K best INIT
|
| 64 |
directories per mode: the answer is the cheapest class found in the directory. Do not run `scale.py` next to other
|
| 65 |
heavy jobs: its wall-clock time is a reported quantity.
|
| 66 |
|
| 67 |
## Settings
|
| 68 |
|
| 69 |
* Program: `Problem(2N, d=9, k=4, ell=4, M, ell_j=3, delta=0.005)` in the whitened chart; separation radius 1.2 m.
|
| 70 |
-
* INIT
|
| 71 |
-
from the obstacle-free optimum,
|
| 72 |
-
|
| 73 |
-
feasible and lies in the class it reports.
|
| 74 |
* Algorithm 2 (`run.py`): `Y = W - (theta / beta) grad f(W)`, `z = Algorithm1(Y)`; stages theta = 0.5, 0.2, 0.05,
|
| 75 |
0.01, 0.002; Nesterov extrapolation with a monotone restart in the last two stages; Algorithm-1 tolerance
|
| 76 |
continuation (package tolerance x 1e-3, 1e-3, 1e-4, 1e-6, 1e-6); Algorithm-1 steps Polyak-capped (stages 1, 4, 5)
|
|
@@ -96,13 +98,14 @@ Times are per instance or per class on one core (AMD EPYC 7502).
|
|
| 96 |
|
| 97 |
| robots | 2 | 4 | 6 | 8 |
|
| 98 |
|---|---|---|---|---|
|
| 99 |
-
| default (INIT
|
| 100 |
| default: median (max) time per instance, s | 47 (100) | 66 (111) | 78 (131) | 78 (131) |
|
| 101 |
| all classes: route classes, all feasible | 168 | 320 | 640 | 1344 |
|
| 102 |
| all classes: median (max) time per class, s | 58 (633) | 76 (639) | 79 (618) | 95 (559) |
|
| 103 |
|
| 104 |
* Default: 77 of 80 instances reach the all-class best. The other 3 end in another class, 0.2% to 3.0% above it
|
| 105 |
-
(4 robots: instance 15, +3.0%; 6 robots: 8, +1.6%; 15, +0.25%). INIT
|
|
|
|
| 106 |
`--top 2` reaches the all-class best on 80/80 (median 48 / 82 / 87 / 86 s).
|
| 107 |
* All classes: the per-instance answer over the paper's GlobMP cost is 1.00000 at the median for 2, 4, 6 and 8
|
| 108 |
robots (max 1.00007); 9 instances end 5-13% cheaper (6 robots: 10, 13, 16, 18; 8 robots: 1, 9, 10, 11, 12). No
|
|
@@ -111,7 +114,7 @@ Times are per instance or per class on one core (AMD EPYC 7502).
|
|
| 111 |
|
| 112 |
| file | content |
|
| 113 |
|---|---|
|
| 114 |
-
| `init.csv`, `init_summary.json` | default run per instance (80): INIT
|
| 115 |
| `first_order_routes.csv` | every route class (2472): cost, minimum true clearance (mm), feasible, exactly certified, iterations, stop reason |
|
| 116 |
| `first_order_instances.csv` | per instance (80): cheapest feasible class and cost, and the cost of the GlobMP runs behind the paper (`paper_cost`, `ratio`) |
|
| 117 |
| `first_order_summary.json` | per robot count: ratios, certified classes, run times |
|
|
@@ -123,7 +126,7 @@ Times are per instance or per class on one core (AMD EPYC 7502).
|
|
| 123 |
|
| 124 |
Main comparison, `<S> = N<N>_i<i>`:
|
| 125 |
`<S>_ours_route<tag>.json/.npy` (tag in {p,m}^A: side of each pair, then of each crossed sphere;
|
| 126 |
-
p = near side / above), `<S>
|
| 127 |
and `<S>_rrt_post.json` (+ `_Z.npy`), the same for `rrtstar`, `<S>_gcs2.json` (+ `_curve.npy`, `_Z.npy`),
|
| 128 |
`<S>_sdp.json` (+ `.npy`).
|
| 129 |
Scaling, `<S> = scale_N<N>_s<seed>`: `<S>_ourscpu_b3.json` (+ `_alg12_Z.npy`), `<S>_rrtc_b3.json` and
|
|
@@ -148,6 +151,8 @@ Bernstein coefficients). `figures.py` and `report.py` also read the original cam
|
|
| 148 |
* `torch.linalg.lstsq` (the fit that starts every run) is not bitwise reproducible, so GlobMP reruns agree to
|
| 149 |
about 1e-8 relative on most classes, not bit for bit; a class with a flat end can stop up to about 2e-5 apart.
|
| 150 |
`smoke.py` compares at 1e-4 and checks the chosen class.
|
|
|
|
|
|
|
| 151 |
* RRT-Connect and its post-processing reproduce exactly. GCS uses Drake's convex solvers and SNOPT rounding, and
|
| 152 |
SDP-MP an interior-point solver: expect identical GCS results on the same Drake version and about 6 digits for
|
| 153 |
SDP-MP.
|
|
|
|
| 4 |
degree-9 polynomials, C^3 junctions, derivatives 1..4 zero at both ends, inflation margin 0.5 cm, no workspace
|
| 5 |
bound. A route class fixes which side each pair and each crossed sphere is passed on.
|
| 6 |
|
| 7 |
+
GlobMP here: INIT (Algorithm 3, `globmp/init.py`) picks one route class, then one first-order run of
|
| 8 |
+
Algorithms 1-2 from the INIT curve. No Newton step, no SQP, no polish. The per-joint states given to INIT here are
|
| 9 |
+
robot positions on a grid with zero derivatives, per exchanging pair, so the pick is not the exact grid optimum of
|
| 10 |
+
Theorem 1. With `--routes all`, GlobMP solves every route class from a waypoint path and keeps the cheapest; that
|
| 11 |
+
run gives `reference/` and Fig. `fig_synth1`.
|
| 12 |
Baselines: RRT-Connect, RRT*, GCS on IRIS regions (their paths fitted by the cheapest collision-free curve of the
|
| 13 |
same class) and SDP-MP (Graesdal et al.).
|
| 14 |
|
|
|
|
| 17 |
| file | content |
|
| 18 |
|---|---|
|
| 19 |
| `instances.py` | instance generators (`instance`: main comparison, M = 7, two spheres; `instance_scale`: scaling study, M = 4, three spheres), the program (`build`), route classes and their waypoint paths, exact evaluation on the true geometry |
|
| 20 |
+
| `joints.py` | what INIT takes here: the per-joint states (grid h = 0.05 m), the piece cost and the exact piece test of one exchanging pair; the route-class ranking |
|
| 21 |
+
| `run.py` | GlobMP on a main-comparison instance: INIT class (default), or every route class (`--routes all`) |
|
| 22 |
| `scale.py` | GlobMP end to end on a scaling instance, timed from process start; data of Fig. `fig_synth2` (GlobMP, panels a-c) |
|
| 23 |
| `baselines.py` | RRT-Connect, RRT* (OMPL), GCS on IRIS (Drake), SDP-MP (cvxpy + Clarabel), and the post-processing of planner paths |
|
| 24 |
| `report.py`, `figures.py` | tables and quoted numbers; `fig_synth1.pdf/png` and `fig_synth2.pdf/png` |
|
|
|
|
| 37 |
```bash
|
| 38 |
export PYTHONPATH=. OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1
|
| 39 |
|
| 40 |
+
# GlobMP: INIT class, one solve per instance (main way to run)
|
| 41 |
python -m globmp.synthetic.run --N 6 --inst $(seq 0 19) --jobs 16 --out results/init
|
| 42 |
|
| 43 |
# GlobMP on every route class (reproduces reference/ and fig_synth1)
|
|
|
|
| 62 |
python -m globmp.synthetic.smoke --out results/smoke --jobs 8 # add --full for the 4-robot instance
|
| 63 |
```
|
| 64 |
|
| 65 |
+
`--top K` solves the K best INIT classes; `--routes ppmp,pppp` solves the listed classes. Use separate output
|
| 66 |
directories per mode: the answer is the cheapest class found in the directory. Do not run `scale.py` next to other
|
| 67 |
heavy jobs: its wall-clock time is a reported quantity.
|
| 68 |
|
| 69 |
## Settings
|
| 70 |
|
| 71 |
* Program: `Problem(2N, d=9, k=4, ell=4, M, ell_j=3, delta=0.005)` in the whitened chart; separation radius 1.2 m.
|
| 72 |
+
* INIT states (`joints.py`): joint y-coordinates on a grid of spacing h = 0.05 m within 2 m of the rows, joint
|
| 73 |
+
x-coordinates from the obstacle-free optimum, derivatives 1..4 zero at every joint; one INIT run per exchanging
|
| 74 |
+
pair, with the route class so far as the state label, exact piece tests (segment against disc) and the budget
|
| 75 |
+
1.3 x the cost on the grid 2h. The returned curve is feasible and lies in the class it reports.
|
| 76 |
* Algorithm 2 (`run.py`): `Y = W - (theta / beta) grad f(W)`, `z = Algorithm1(Y)`; stages theta = 0.5, 0.2, 0.05,
|
| 77 |
0.01, 0.002; Nesterov extrapolation with a monotone restart in the last two stages; Algorithm-1 tolerance
|
| 78 |
continuation (package tolerance x 1e-3, 1e-3, 1e-4, 1e-6, 1e-6); Algorithm-1 steps Polyak-capped (stages 1, 4, 5)
|
|
|
|
| 98 |
|
| 99 |
| robots | 2 | 4 | 6 | 8 |
|
| 100 |
|---|---|---|---|---|
|
| 101 |
+
| default (INIT + one solve): instances with cost equal to the all-class best (within 1e-5) | 20/20 | 19/20 | 18/20 | 20/20 |
|
| 102 |
| default: median (max) time per instance, s | 47 (100) | 66 (111) | 78 (131) | 78 (131) |
|
| 103 |
| all classes: route classes, all feasible | 168 | 320 | 640 | 1344 |
|
| 104 |
| all classes: median (max) time per class, s | 58 (633) | 76 (639) | 79 (618) | 95 (559) |
|
| 105 |
|
| 106 |
* Default: 77 of 80 instances reach the all-class best. The other 3 end in another class, 0.2% to 3.0% above it
|
| 107 |
+
(4 robots: instance 15, +3.0%; 6 robots: 8, +1.6%; 15, +0.25%). INIT took 1 to 3 s of the instance time in that
|
| 108 |
+
run; it now takes 0.5 to 1.3 s (median per robot count).
|
| 109 |
`--top 2` reaches the all-class best on 80/80 (median 48 / 82 / 87 / 86 s).
|
| 110 |
* All classes: the per-instance answer over the paper's GlobMP cost is 1.00000 at the median for 2, 4, 6 and 8
|
| 111 |
robots (max 1.00007); 9 instances end 5-13% cheaper (6 robots: 10, 13, 16, 18; 8 robots: 1, 9, 10, 11, 12). No
|
|
|
|
| 114 |
|
| 115 |
| file | content |
|
| 116 |
|---|---|
|
| 117 |
+
| `init.csv`, `init_summary.json` | default run per instance (80): INIT class, cost, ratio to the all-class best, time (INIT + solve); the same for `--top 2` |
|
| 118 |
| `first_order_routes.csv` | every route class (2472): cost, minimum true clearance (mm), feasible, exactly certified, iterations, stop reason |
|
| 119 |
| `first_order_instances.csv` | per instance (80): cheapest feasible class and cost, and the cost of the GlobMP runs behind the paper (`paper_cost`, `ratio`) |
|
| 120 |
| `first_order_summary.json` | per robot count: ratios, certified classes, run times |
|
|
|
|
| 126 |
|
| 127 |
Main comparison, `<S> = N<N>_i<i>`:
|
| 128 |
`<S>_ours_route<tag>.json/.npy` (tag in {p,m}^A: side of each pair, then of each crossed sphere;
|
| 129 |
+
p = near side / above), `<S>_init.json` (INIT ranking, default mode), `<S>_rrt.json` (+ `_path.npy`, `.npy`)
|
| 130 |
and `<S>_rrt_post.json` (+ `_Z.npy`), the same for `rrtstar`, `<S>_gcs2.json` (+ `_curve.npy`, `_Z.npy`),
|
| 131 |
`<S>_sdp.json` (+ `.npy`).
|
| 132 |
Scaling, `<S> = scale_N<N>_s<seed>`: `<S>_ourscpu_b3.json` (+ `_alg12_Z.npy`), `<S>_rrtc_b3.json` and
|
|
|
|
| 151 |
* `torch.linalg.lstsq` (the fit that starts every run) is not bitwise reproducible, so GlobMP reruns agree to
|
| 152 |
about 1e-8 relative on most classes, not bit for bit; a class with a flat end can stop up to about 2e-5 apart.
|
| 153 |
`smoke.py` compares at 1e-4 and checks the chosen class.
|
| 154 |
+
* Reruns of the released code against `reference/`: default mode, the same class on 80/80 instances and costs
|
| 155 |
+
within 7e-7 (relative); all classes, 2472/2472 feasible, class costs within 1.2e-5, per-instance best within 4e-7.
|
| 156 |
* RRT-Connect and its post-processing reproduce exactly. GCS uses Drake's convex solvers and SNOPT rounding, and
|
| 157 |
SDP-MP an interior-point solver: expect identical GCS results on the same Drake version and about 6 digits for
|
| 158 |
SDP-MP.
|
|
@@ -3,9 +3,9 @@ pairs among static spheres (Problem B), GlobMP against RRT-Connect, RRT*, GCS an
|
|
| 3 |
from 2 to 200 robots.
|
| 4 |
|
| 5 |
instances instance generator, program, route classes and their waypoint openers, exact evaluation
|
| 6 |
-
run GlobMP on a main-comparison instance: INIT
|
| 7 |
(--routes all: every route class)
|
| 8 |
-
|
| 9 |
certify optional exact continuous-time check of returned curves (rational arithmetic); not part of the method
|
| 10 |
exact the exact nonnegativity test behind certify
|
| 11 |
scale GlobMP end to end on a scaling instance (time to feasibility, time per iteration)
|
|
|
|
| 3 |
from 2 to 200 robots.
|
| 4 |
|
| 5 |
instances instance generator, program, route classes and their waypoint openers, exact evaluation
|
| 6 |
+
run GlobMP on a main-comparison instance: INIT picks a route class, then first-order Algorithm 2
|
| 7 |
(--routes all: every route class)
|
| 8 |
+
joints what INIT (globmp.init) needs here: per-joint states, piece cost, piece test; the route ranking
|
| 9 |
certify optional exact continuous-time check of returned curves (rational arithmetic); not part of the method
|
| 10 |
exact the exact nonnegativity test behind certify
|
| 11 |
scale GlobMP end to end on a scaling instance (time to feasibility, time per iteration)
|
|
@@ -1,23 +1,22 @@
|
|
| 1 |
-
"""INIT
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
configurations Q, from which globmp.synthetic.run starts Algorithm 2 (poly_to_Z).
|
| 21 |
"""
|
| 22 |
import itertools
|
| 23 |
import json
|
|
@@ -29,10 +28,12 @@ import torch
|
|
| 29 |
|
| 30 |
from .. import DT
|
| 31 |
from .. import spline as bern
|
|
|
|
| 32 |
from . import instances as I
|
| 33 |
|
| 34 |
BETA = np.array([0.] * 5 + [1.] * 5)
|
| 35 |
CH = float(BETA @ bern.cost_gram(I.D, 4).numpy() @ BETA) # snap energy of the degree-9 smoothstep
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def setup(N, i):
|
|
@@ -65,74 +66,83 @@ def cross_piece(xs, c):
|
|
| 65 |
return (j - 1, 1.0) if j > 0 else (0, 0.0)
|
| 66 |
|
| 67 |
|
| 68 |
-
|
| 69 |
-
"""
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
for c, rad in balls:
|
| 81 |
-
ok &= seg_disc_dist(X[m, r], ya, X[m + 1, r], yb, c[0], c[1]) >= rad + delta
|
| 82 |
-
T['ok' + key] = ok
|
| 83 |
e0 = yrow[ri] - yrow[rj]; dg = h * np.arange(-2 * K, 2 * K + 1)
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
def clusters(ins):
|
|
@@ -154,9 +164,10 @@ def team_poly(X, ypaths, cls, N):
|
|
| 154 |
return Q
|
| 155 |
|
| 156 |
|
| 157 |
-
def
|
| 158 |
-
"""
|
| 159 |
-
|
|
|
|
| 160 |
t0 = time.time()
|
| 161 |
ins, p, X, yrow = setup(N, i); cls = clusters(ins); A = len(ins['active']); nb = A + len(I.crossings(ins))
|
| 162 |
constX = CH * float((np.diff(X, axis=0) ** 2).sum())
|
|
@@ -165,7 +176,7 @@ def init_p(N, i, h=0.1, Yr=2.0, slack=0.3, topk=3):
|
|
| 165 |
R = []
|
| 166 |
for c in cls:
|
| 167 |
Uc = np.inf if UU is not None else (1 + slack) * min(v[0] for v in res[2 * h][len(R)][0].values())
|
| 168 |
-
out, st =
|
| 169 |
st_all.append(dict(h=hh, rob=c['rob'], U=Uc, n_classes=len(out), **st))
|
| 170 |
res[hh] = R
|
| 171 |
fine = res[h]
|
|
@@ -182,7 +193,7 @@ def init_p(N, i, h=0.1, Yr=2.0, slack=0.3, topk=3):
|
|
| 182 |
sig = [0] * nb
|
| 183 |
for c, (mask, _) in zip(cls, pick):
|
| 184 |
for q, pos in enumerate(c['pos']): sig[pos] = 1 if (mask >> q) & 1 else -1
|
| 185 |
-
ranked.append(dict(tag=I.route_tag(sig),
|
| 186 |
touch=any(v[1][2] for v in pick)))
|
| 187 |
if len(ranked) >= topk: break
|
| 188 |
return dict(ins=ins, p=p, ranked=ranked, n_skip=n_skip, n_combos=len(combos), stats=st_all, time=time.time() - t0)
|
|
@@ -200,16 +211,16 @@ def poly_to_Z(p, Q):
|
|
| 200 |
return Z
|
| 201 |
|
| 202 |
|
| 203 |
-
def
|
| 204 |
-
"""INIT
|
| 205 |
-
ranked entry has the class tag, its
|
| 206 |
-
r =
|
| 207 |
-
ranked = [dict(tag=d['tag'],
|
| 208 |
rec = dict(N=N, i=i, h=h, top=top, time=r['time'], n_combos=r['n_combos'], n_skip=r['n_skip'],
|
| 209 |
-
ranked=[dict(tag=d['tag'],
|
| 210 |
for d in r['ranked']])
|
| 211 |
os.makedirs(out, exist_ok=True)
|
| 212 |
-
with open(os.path.join(out, f'N{N}_i{i}
|
| 213 |
-
print(f"[INIT
|
| 214 |
return dict(i=i, time=r['time'], ranked=ranked)
|
| 215 |
|
|
|
|
| 1 |
+
"""What INIT (globmp.init) needs on the main-comparison instances, and the start it gives (2..8 planar robots,
|
| 2 |
+
minimum snap, M = 7 pieces).
|
| 3 |
+
|
| 4 |
+
Per-joint states (`PairStates`, one object per exchanging pair): the y-coordinates of the pair's two robots on the
|
| 5 |
+
grid y_row + h Z, |y - y_row| <= 2 m, with the derivatives 1..4 at every joint equal to 0 and the x-coordinates at
|
| 6 |
+
the joints fixed to those of the obstacle-free optimum. A piece is then a + (b - a) H(s), H the degree-9 smoothstep
|
| 7 |
+
(Bernstein coefficients 0,0,0,0,0,1,1,1,1,1): its configuration path is a straight segment, the piece test is an
|
| 8 |
+
exact segment/disc distance (each robot against every sphere, the pair against R_S), and the piece cost is
|
| 9 |
+
c_H |b - a|^2. A state also carries the route class so far (INIT's label: the side of the pair and of the spheres
|
| 10 |
+
its robots have crossed), so INIT returns the cheapest curve of every route class of the pair. The budget is
|
| 11 |
+
1.3 x the cost of the curve INIT returns on the grid 2h.
|
| 12 |
+
|
| 13 |
+
`rank_routes` adds the pairs' costs per team route class (the cost is a sum over robots), checks the separations
|
| 14 |
+
between pairs exactly on the assembled curve and ranks the classes; `init_start` writes the ranking, and
|
| 15 |
+
`poly_to_Z` gives the chart point from which globmp.synthetic.run starts Algorithm 2.
|
| 16 |
+
|
| 17 |
+
These states are positions on a grid with zero derivatives, per exchanging pair: a strict subset of the grid of
|
| 18 |
+
Theorem 1, so the pick is not the exact grid optimum z_h. Every curve returned is feasible and lies in the class
|
| 19 |
+
it reports.
|
|
|
|
| 20 |
"""
|
| 21 |
import itertools
|
| 22 |
import json
|
|
|
|
| 28 |
|
| 29 |
from .. import DT
|
| 30 |
from .. import spline as bern
|
| 31 |
+
from ..init import init
|
| 32 |
from . import instances as I
|
| 33 |
|
| 34 |
BETA = np.array([0.] * 5 + [1.] * 5)
|
| 35 |
CH = float(BETA @ bern.cost_gram(I.D, 4).numpy() @ BETA) # snap energy of the degree-9 smoothstep
|
| 36 |
+
CHUNK = 2e5 # INIT matrix entries per step
|
| 37 |
|
| 38 |
|
| 39 |
def setup(N, i):
|
|
|
|
| 66 |
return (j - 1, 1.0) if j > 0 else (0, 0.0)
|
| 67 |
|
| 68 |
|
| 69 |
+
class PairStates:
|
| 70 |
+
"""States, piece cost and piece test of one exchanging pair rob = (ri, rj) for globmp.init.
|
| 71 |
+
bits: the route bits of the pair, ('pair', None) or ('cross', (robot, sphere centre)).
|
| 72 |
+
State at joints 1..M-1: grid indices (b0, b1) of the two robots' y, numbered b0 n + b1; at joints 0 and M the
|
| 73 |
+
single state (K, K), the robots' rows."""
|
| 74 |
+
|
| 75 |
+
def __init__(self, ins, X, yrow, rob, bits, h, Yr=2.0, delta=I.DELTA):
|
| 76 |
+
M = X.shape[0] - 1; K = int(round(Yr / h)); n = 2 * K + 1; g = h * np.arange(-K, K + 1)
|
| 77 |
+
ri, rj = rob; R = I.RS + delta; self.w = w = CH * h * h
|
| 78 |
+
self.n_pieces = M; self.n_labels = 1 << len(bits); self.K, self.n, self.g, self.rob, self.yrow = K, n, g, rob, yrow
|
| 79 |
+
self.W = w * (np.arange(n)[:, None] - np.arange(n)[None, :]) ** 2 # piece cost of one robot, [a, b]
|
| 80 |
+
self.b0 = np.repeat(np.arange(n), n); self.b1 = np.tile(np.arange(n), n); self.db = self.b0 - self.b1 + 2 * K
|
|
|
|
|
|
|
|
|
|
| 81 |
e0 = yrow[ri] - yrow[rj]; dg = h * np.arange(-2 * K, 2 * K + 1)
|
| 82 |
+
# exact piece tests as tables: each robot against the spheres [a, b], the pair against R_S [a0 - a1, b0 - b1]
|
| 83 |
+
self.ok0, self.ok1, self.okp = [], [], []
|
| 84 |
+
for m in range(M):
|
| 85 |
+
for r, tab in ((ri, self.ok0), (rj, self.ok1)):
|
| 86 |
+
o = np.ones((n, n), bool)
|
| 87 |
+
for c, rad in ins['balls']:
|
| 88 |
+
o &= seg_disc_dist(X[m, r], yrow[r] + g[:, None], X[m + 1, r], yrow[r] + g[None, :], c[0], c[1]) >= rad + delta
|
| 89 |
+
tab.append(o)
|
| 90 |
+
self.okp.append(seg_disc_dist(X[m, ri] - X[m, rj], e0 + dg[:, None], X[m + 1, ri] - X[m + 1, rj], e0 + dg[None, :], 0.0, 0.0) >= R)
|
| 91 |
+
# route bits: each is decided on one piece, by the side on which the segment passes
|
| 92 |
+
bi = np.zeros((M, n, n), np.int64); bj = np.zeros((M, n, n), np.int64); bp = np.zeros((M, 4 * K + 1, 4 * K + 1), np.int64)
|
| 93 |
+
for q, (kind, arg) in enumerate(bits):
|
| 94 |
+
if kind == 'pair':
|
| 95 |
+
m, lam = cross_piece(X[:, ri] - X[:, rj], 0.0)
|
| 96 |
+
bp[m] |= ((e0 + dg[:, None] + (dg[None, :] - dg[:, None]) * lam) < 0).astype(np.int64) << q
|
| 97 |
+
else:
|
| 98 |
+
r, (cx, cy) = arg; m, lam = cross_piece(X[:, r], cx)
|
| 99 |
+
(bi if r == ri else bj)[m] |= ((yrow[r] + g[:, None] + (g[None, :] - g[:, None]) * lam) > cy).astype(np.int64) << q
|
| 100 |
+
self.bits = [(bi[m], bj[m], bp[m]) if (bi[m].any() or bj[m].any() or bp[m].any()) else None for m in range(M)]
|
| 101 |
+
|
| 102 |
+
def _full(self, j): return 0 < j < self.n_pieces
|
| 103 |
+
|
| 104 |
+
def n_states(self, j): return self.n * self.n if self._full(j) else 1
|
| 105 |
+
|
| 106 |
+
def _ab(self, j, s):
|
| 107 |
+
"""grid indices (a0, a1) of the states s at joint j."""
|
| 108 |
+
return (self.b0[s], self.b1[s]) if self._full(j) else (np.full(len(s), self.K), np.full(len(s), self.K))
|
| 109 |
+
|
| 110 |
+
def _expand(self, i, A, B, op):
|
| 111 |
+
"""A [source, b0], B [source, b1] -> op(A, B) [source, target state of piece i]."""
|
| 112 |
+
if self._full(i + 1): return op(A[:, :, None], B[:, None, :]).reshape(len(A), -1)
|
| 113 |
+
return op(A[:, self.K], B[:, self.K])[:, None]
|
| 114 |
+
|
| 115 |
+
def _pair(self, i, P):
|
| 116 |
+
"""P [source, b0 - b1] -> [source, target state of piece i]."""
|
| 117 |
+
return P[:, self.db] if self._full(i + 1) else P[:, 2 * self.K:2 * self.K + 1]
|
| 118 |
+
|
| 119 |
+
def cost(self, i, s, V):
|
| 120 |
+
a0, a1 = self._ab(i, s)
|
| 121 |
+
return self._expand(i, V[:, None] + self.W[a0], self.W[a1], np.add)
|
| 122 |
+
|
| 123 |
+
def free(self, i, s):
|
| 124 |
+
a0, a1 = self._ab(i, s)
|
| 125 |
+
return self._expand(i, self.ok0[i][a0], self.ok1[i][a1], np.logical_and) & self._pair(i, self.okp[i][a0 - a1 + 2 * self.K])
|
| 126 |
+
|
| 127 |
+
def label(self, i, s, lab):
|
| 128 |
+
if self.bits[i] is None: return lab[:, None]
|
| 129 |
+
a0, a1 = self._ab(i, s); bi, bj, bp = self.bits[i]
|
| 130 |
+
return self._expand(i, lab[:, None] | bi[a0], bj[a1], np.bitwise_or) | self._pair(i, bp[a0 - a1 + 2 * self.K])
|
| 131 |
+
|
| 132 |
+
def togo(self, j):
|
| 133 |
+
if not self._full(j): return np.zeros(1)
|
| 134 |
+
return self.w * ((self.b0 - self.K) ** 2 + (self.b1 - self.K) ** 2) / (self.n_pieces - j)
|
| 135 |
+
|
| 136 |
+
def curves(self, U=np.inf):
|
| 137 |
+
"""INIT on this pair: {route mask: (cost, y of the two robots at the joints (M+1, 2), path touches the
|
| 138 |
+
grid border)} for every route class with a feasible curve of cost <= U, and INIT's counts."""
|
| 139 |
+
r = init(self, U, ties='state', chunk=CHUNK); out = {}
|
| 140 |
+
for mask, (cost, path) in r['curves'].items():
|
| 141 |
+
mid = np.array(path[1:-1]); ix = np.full((len(path), 2), self.K); ix[1:-1, 0] = self.b0[mid]; ix[1:-1, 1] = self.b1[mid]
|
| 142 |
+
y = np.stack([self.yrow[self.rob[0]] + self.g[ix[:, 0]], self.yrow[self.rob[1]] + self.g[ix[:, 1]]], 1)
|
| 143 |
+
out[mask] = (cost, y, bool((np.abs(ix - self.K) == self.K).any()))
|
| 144 |
+
L = r['layers']
|
| 145 |
+
return out, dict(pairs=sum(l['pairs'] for l in L), budget_pass=sum(l['budget_pass'] for l in L), tested=sum(l['tested'] for l in L))
|
| 146 |
|
| 147 |
|
| 148 |
def clusters(ins):
|
|
|
|
| 164 |
return Q
|
| 165 |
|
| 166 |
|
| 167 |
+
def rank_routes(N, i, h=0.05, Yr=2.0, slack=0.3, topk=1):
|
| 168 |
+
"""Route classes of instance (N, i) ranked by INIT: per pair, INIT on the grid 2h without a budget gives U,
|
| 169 |
+
then INIT on the grid h with budget U gives the cheapest curve per route class; team classes are ranked by
|
| 170 |
+
summed cost, with the separations between pairs tested on the assembled curve; the top-k feasible are returned."""
|
| 171 |
t0 = time.time()
|
| 172 |
ins, p, X, yrow = setup(N, i); cls = clusters(ins); A = len(ins['active']); nb = A + len(I.crossings(ins))
|
| 173 |
constX = CH * float((np.diff(X, axis=0) ** 2).sum())
|
|
|
|
| 176 |
R = []
|
| 177 |
for c in cls:
|
| 178 |
Uc = np.inf if UU is not None else (1 + slack) * min(v[0] for v in res[2 * h][len(R)][0].values())
|
| 179 |
+
out, st = PairStates(ins, X, yrow, c['rob'], c['bits'], hh, Yr).curves(Uc); R.append((out, st))
|
| 180 |
st_all.append(dict(h=hh, rob=c['rob'], U=Uc, n_classes=len(out), **st))
|
| 181 |
res[hh] = R
|
| 182 |
fine = res[h]
|
|
|
|
| 193 |
sig = [0] * nb
|
| 194 |
for c, (mask, _) in zip(cls, pick):
|
| 195 |
for q, pos in enumerate(c['pos']): sig[pos] = 1 if (mask >> q) & 1 else -1
|
| 196 |
+
ranked.append(dict(tag=I.route_tag(sig), init_cost=cost, Q=Q, sep_margin=sep, ball_margin=ball,
|
| 197 |
touch=any(v[1][2] for v in pick)))
|
| 198 |
if len(ranked) >= topk: break
|
| 199 |
return dict(ins=ins, p=p, ranked=ranked, n_skip=n_skip, n_combos=len(combos), stats=st_all, time=time.time() - t0)
|
|
|
|
| 211 |
return Z
|
| 212 |
|
| 213 |
|
| 214 |
+
def init_start(N, i, out, h=0.05, top=1):
|
| 215 |
+
"""INIT on instance (N, i): writes <out>/N<N>_i<i>_init.json and returns dict(i, time, ranked), where each
|
| 216 |
+
ranked entry has the class tag, its INIT cost and its joint configurations Q (M+1, N, 2)."""
|
| 217 |
+
r = rank_routes(N, i, h=h, topk=top)
|
| 218 |
+
ranked = [dict(tag=d['tag'], init_cost=d['init_cost'], Q=d['Q'].tolist()) for d in r['ranked']]
|
| 219 |
rec = dict(N=N, i=i, h=h, top=top, time=r['time'], n_combos=r['n_combos'], n_skip=r['n_skip'],
|
| 220 |
+
ranked=[dict(tag=d['tag'], init_cost=d['init_cost'], sep_margin=d['sep_margin'], ball_margin=d['ball_margin'])
|
| 221 |
for d in r['ranked']])
|
| 222 |
os.makedirs(out, exist_ok=True)
|
| 223 |
+
with open(os.path.join(out, f'N{N}_i{i}_init.json'), 'w') as fh: json.dump(rec, fh, indent=0, default=float)
|
| 224 |
+
print(f"[INIT N{N} i{i}] {r['time']:.1f}s " + ' '.join(f"{d['tag']}:{d['init_cost']:.3f}" for d in r['ranked']), flush=True)
|
| 225 |
return dict(i=i, time=r['time'], ranked=ranked)
|
| 226 |
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"_about": "INIT
|
| 3 |
"h": 0.05,
|
| 4 |
"per_N": {
|
| 5 |
"2": {
|
|
|
|
| 1 |
{
|
| 2 |
+
"_about": "INIT (h = 0.05 m) then one first-order Algorithm-2 solve per kept class (globmp.synthetic.run defaults: top 1; top 2 for comparison), against the cheapest class of the all-class run. ratio = cost / all-class best; equal = ratio <= 1 + 1e-5. time = INIT + solve(s) per instance, one core per solve (AMD EPYC 7502).",
|
| 3 |
"h": 0.05,
|
| 4 |
"per_N": {
|
| 5 |
"2": {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"_about": "Values from the runs behind the reference results (globmp.synthetic.run: --routes all for the route classes, the default INIT
|
| 3 |
"N2_i0_ours_routes": {
|
| 4 |
"mmm": {
|
| 5 |
"cost": 22.8338505551219,
|
|
|
|
| 1 |
{
|
| 2 |
+
"_about": "Values from the runs behind the reference results (globmp.synthetic.run: --routes all for the route classes, the default INIT mode for N2_i0_init; baselines and scaling as in the paper), for globmp.synthetic.smoke.",
|
| 3 |
"N2_i0_ours_routes": {
|
| 4 |
"mmm": {
|
| 5 |
"cost": 22.8338505551219,
|
|
@@ -3,7 +3,7 @@
|
|
| 3 |
Main table, per robot count (20 instances each): raw planner path collision-free (%), returned curve feasible at
|
| 4 |
every t (%), solved (feasible and within 1% of the lowest cost any method returns, %), median cost / GlobMP's over
|
| 5 |
feasible curves, median minimum clearance of feasible curves (cm), median time (baselines: planner plus
|
| 6 |
-
post-processing; GlobMP: INIT
|
| 7 |
runs killed at the one-hour cap (counted as not feasible).
|
| 8 |
Scaling table, per size: blocks, joint dimension, seeds feasible per method (baselines after post-processing),
|
| 9 |
GlobMP's median time to its first feasible curve from process start on CPU (and on GPU where such runs exist).
|
|
@@ -43,7 +43,7 @@ def rec(RES, N, i, m):
|
|
| 43 |
fe = [r for r in R if r['eval']['feasible']]
|
| 44 |
b = min(fe, key=lambda r: r['eval']['cost']) if fe else None
|
| 45 |
e = b['eval'] if b else None
|
| 46 |
-
ini = J(f'{RES}/N{N}_i{i}
|
| 47 |
return dict(clear=None, feas=bool(b), cost=e['cost'] if e else None, mincl=_mincl(e) if e else None,
|
| 48 |
time=t0 + sum(r['time'] for r in R), tmax=t0 + max(r['time'] for r in R))
|
| 49 |
if m in ('rrt', 'rrtstar'):
|
|
|
|
| 3 |
Main table, per robot count (20 instances each): raw planner path collision-free (%), returned curve feasible at
|
| 4 |
every t (%), solved (feasible and within 1% of the lowest cost any method returns, %), median cost / GlobMP's over
|
| 5 |
feasible curves, median minimum clearance of feasible curves (cm), median time (baselines: planner plus
|
| 6 |
+
post-processing; GlobMP: INIT plus every solved class, slowest class in parentheses). A dagger marks a method with
|
| 7 |
runs killed at the one-hour cap (counted as not feasible).
|
| 8 |
Scaling table, per size: blocks, joint dimension, seeds feasible per method (baselines after post-processing),
|
| 9 |
GlobMP's median time to its first feasible curve from process start on CPU (and on GPU where such runs exist).
|
|
|
|
| 43 |
fe = [r for r in R if r['eval']['feasible']]
|
| 44 |
b = min(fe, key=lambda r: r['eval']['cost']) if fe else None
|
| 45 |
e = b['eval'] if b else None
|
| 46 |
+
ini = J(f'{RES}/N{N}_i{i}_init.json'); t0 = ini['time'] if ini else 0.0 # INIT ranking (default mode)
|
| 47 |
return dict(clear=None, feas=bool(b), cost=e['cost'] if e else None, mincl=_mincl(e) if e else None,
|
| 48 |
time=t0 + sum(r['time'] for r in R), tmax=t0 + max(r['time'] for r in R))
|
| 49 |
if m in ('rrt', 'rrtstar'):
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
"""GlobMP on the main-comparison instances: paper Algorithms 1-2, first order.
|
| 2 |
|
| 3 |
Route classes solved (--routes):
|
| 4 |
-
init (default) INIT
|
| 5 |
-
Algorithm-2 solve runs from its
|
| 6 |
all every route class, each from its waypoint opener (instances.route_wp); the run behind
|
| 7 |
reference/ and fig_synth1.
|
| 8 |
<tag>[,<tag>] the listed route classes, from their waypoint openers.
|
|
@@ -29,11 +29,11 @@ Bernstein coefficients on each of 16 equal subintervals are nonnegative is in th
|
|
| 29 |
block coefficients once per trial point. Same steps, same values.
|
| 30 |
|
| 31 |
Writes <out>/N<N>_i<inst>_ours_route<tag>.json and .npy (the chart point Z in the package's default chart,
|
| 32 |
-
Problem.Gamma(Z) gives the Bernstein coefficients), one pair per solved class, plus <out>/N<N>_i<inst>
|
| 33 |
-
(the INIT
|
| 34 |
directories for the two modes: best_route takes the cheapest class found in the directory.
|
| 35 |
|
| 36 |
-
python -m globmp.synthetic.run --N 4 --inst 3 --out results/init # INIT
|
| 37 |
python -m globmp.synthetic.run --N 4 --inst 3 --routes all --jobs 16 --out results/synthetic
|
| 38 |
python -m globmp.synthetic.run --N 4 --inst 3 --routes ppmp --out results/synthetic
|
| 39 |
"""
|
|
@@ -199,7 +199,7 @@ def alg2(p, Z0, tv0, t_max=T_MAX, T=T_ITERS):
|
|
| 199 |
hist.append((it, c, bool(okr), time.time() - t0))
|
| 200 |
if okr and full and c < best[0]: best = (c, Z.clone())
|
| 201 |
log.update(iters=it, n_restore_fail=n_fail, n_restart=n_restart, stop=reason, time=time.time() - t0,
|
| 202 |
-
stages=stages, inner=int(p
|
| 203 |
if best[1] is None: return Z, False, log, hist
|
| 204 |
return best[1], True, log, hist
|
| 205 |
|
|
@@ -215,7 +215,7 @@ def route_stem(N, i, tag): return f"N{N}_i{i}_ours_route{tag}"
|
|
| 215 |
|
| 216 |
|
| 217 |
def run_route(N, i, tag, out, t_max=T_MAX, start=None):
|
| 218 |
-
"""One route class of main-comparison instance (N, i), started from its waypoint opener, or from the INIT
|
| 219 |
joint configurations start = (rank, Q); writes its json/npy files and returns the record."""
|
| 220 |
T0 = time.time(); ins = I.instance(N, i)
|
| 221 |
assert len(tag) == len(ins['active']) + len(I.crossings(ins)) and set(tag) <= set('pm'), tag
|
|
@@ -223,8 +223,8 @@ def run_route(N, i, tag, out, t_max=T_MAX, start=None):
|
|
| 223 |
if start is None:
|
| 224 |
Z0 = init_waypoints(p, I.route_wp(ins, I.sig_of_tag(tag))); how = 'route_wp'
|
| 225 |
else:
|
| 226 |
-
from .
|
| 227 |
-
Z0 = poly_to_Z(p, np.asarray(start[1])); how = f'
|
| 228 |
Z, ok, log, hist = alg2(p, Z0, p.tolvec(), t_max)
|
| 229 |
ev = I.evaluate(p, ins, Z)
|
| 230 |
stem = os.path.join(out, route_stem(N, i, tag)); os.makedirs(out, exist_ok=True)
|
|
@@ -252,8 +252,8 @@ def best_route(N, i, res):
|
|
| 252 |
|
| 253 |
def _init_job(args):
|
| 254 |
torch.set_num_threads(1)
|
| 255 |
-
from .
|
| 256 |
-
return
|
| 257 |
|
| 258 |
|
| 259 |
def _job(args):
|
|
@@ -273,9 +273,9 @@ def main(argv=None):
|
|
| 273 |
ap.add_argument('--N', type=int, required=True, help='number of robots (even)')
|
| 274 |
ap.add_argument('--inst', type=int, nargs='+', required=True, help='instance indices (the paper uses 0..19)')
|
| 275 |
ap.add_argument('--routes', '--route', dest='routes', default='init',
|
| 276 |
-
help="'init' (default: the INIT
|
| 277 |
ap.add_argument('--top', type=int, default=1, help='init mode: number of ranked classes solved (default 1)')
|
| 278 |
-
ap.add_argument('--h', type=float, default=0.05, help='init mode: INIT
|
| 279 |
ap.add_argument('--out', default=os.environ.get('GLOBMP_SYNTH_OUT', 'results/synthetic'))
|
| 280 |
ap.add_argument('--jobs', type=int, default=1, help='classes solved in parallel processes (one thread each)')
|
| 281 |
ap.add_argument('--skip-existing', action='store_true')
|
|
@@ -298,7 +298,7 @@ def main(argv=None):
|
|
| 298 |
_pool(_job, jobs, a.jobs)
|
| 299 |
for i in a.inst:
|
| 300 |
b = best_route(a.N, i, a.out)
|
| 301 |
-
t = f" (INIT
|
| 302 |
print(f"N{a.N} i{i}: " + (f"best route {b[0]} cost {b[1]['eval']['cost']:.6f}" if b else 'no feasible route') + t,
|
| 303 |
flush=True)
|
| 304 |
|
|
|
|
| 1 |
"""GlobMP on the main-comparison instances: paper Algorithms 1-2, first order.
|
| 2 |
|
| 3 |
Route classes solved (--routes):
|
| 4 |
+
init (default) INIT (globmp.init, with the per-joint states of joints.py, grid h = 0.05 m) picks one route
|
| 5 |
+
class, and one Algorithm-2 solve runs from its curve (--top K solves the K best-ranked classes).
|
| 6 |
all every route class, each from its waypoint opener (instances.route_wp); the run behind
|
| 7 |
reference/ and fig_synth1.
|
| 8 |
<tag>[,<tag>] the listed route classes, from their waypoint openers.
|
|
|
|
| 29 |
block coefficients once per trial point. Same steps, same values.
|
| 30 |
|
| 31 |
Writes <out>/N<N>_i<inst>_ours_route<tag>.json and .npy (the chart point Z in the package's default chart,
|
| 32 |
+
Problem.Gamma(Z) gives the Bernstein coefficients), one pair per solved class, plus <out>/N<N>_i<inst>_init.json
|
| 33 |
+
(the INIT ranking) in init mode; the layout that report.py, figures.py and certify.py read. Use separate output
|
| 34 |
directories for the two modes: best_route takes the cheapest class found in the directory.
|
| 35 |
|
| 36 |
+
python -m globmp.synthetic.run --N 4 --inst 3 --out results/init # INIT, one class
|
| 37 |
python -m globmp.synthetic.run --N 4 --inst 3 --routes all --jobs 16 --out results/synthetic
|
| 38 |
python -m globmp.synthetic.run --N 4 --inst 3 --routes ppmp --out results/synthetic
|
| 39 |
"""
|
|
|
|
| 199 |
hist.append((it, c, bool(okr), time.time() - t0))
|
| 200 |
if okr and full and c < best[0]: best = (c, Z.clone())
|
| 201 |
log.update(iters=it, n_restore_fail=n_fail, n_restart=n_restart, stop=reason, time=time.time() - t0,
|
| 202 |
+
stages=stages, inner=int(getattr(p, 'n_it', 0)), t_alg1=float(getattr(p, 't_it', 0.0)))
|
| 203 |
if best[1] is None: return Z, False, log, hist
|
| 204 |
return best[1], True, log, hist
|
| 205 |
|
|
|
|
| 215 |
|
| 216 |
|
| 217 |
def run_route(N, i, tag, out, t_max=T_MAX, start=None):
|
| 218 |
+
"""One route class of main-comparison instance (N, i), started from its waypoint opener, or from the INIT
|
| 219 |
joint configurations start = (rank, Q); writes its json/npy files and returns the record."""
|
| 220 |
T0 = time.time(); ins = I.instance(N, i)
|
| 221 |
assert len(tag) == len(ins['active']) + len(I.crossings(ins)) and set(tag) <= set('pm'), tag
|
|
|
|
| 223 |
if start is None:
|
| 224 |
Z0 = init_waypoints(p, I.route_wp(ins, I.sig_of_tag(tag))); how = 'route_wp'
|
| 225 |
else:
|
| 226 |
+
from .joints import poly_to_Z
|
| 227 |
+
Z0 = poly_to_Z(p, np.asarray(start[1])); how = f'init_rank{start[0]}'
|
| 228 |
Z, ok, log, hist = alg2(p, Z0, p.tolvec(), t_max)
|
| 229 |
ev = I.evaluate(p, ins, Z)
|
| 230 |
stem = os.path.join(out, route_stem(N, i, tag)); os.makedirs(out, exist_ok=True)
|
|
|
|
| 252 |
|
| 253 |
def _init_job(args):
|
| 254 |
torch.set_num_threads(1)
|
| 255 |
+
from .joints import init_start
|
| 256 |
+
return init_start(*args)
|
| 257 |
|
| 258 |
|
| 259 |
def _job(args):
|
|
|
|
| 273 |
ap.add_argument('--N', type=int, required=True, help='number of robots (even)')
|
| 274 |
ap.add_argument('--inst', type=int, nargs='+', required=True, help='instance indices (the paper uses 0..19)')
|
| 275 |
ap.add_argument('--routes', '--route', dest='routes', default='init',
|
| 276 |
+
help="'init' (default: the INIT class), 'all', or comma-separated route tags")
|
| 277 |
ap.add_argument('--top', type=int, default=1, help='init mode: number of ranked classes solved (default 1)')
|
| 278 |
+
ap.add_argument('--h', type=float, default=0.05, help='init mode: INIT grid spacing in m (default 0.05)')
|
| 279 |
ap.add_argument('--out', default=os.environ.get('GLOBMP_SYNTH_OUT', 'results/synthetic'))
|
| 280 |
ap.add_argument('--jobs', type=int, default=1, help='classes solved in parallel processes (one thread each)')
|
| 281 |
ap.add_argument('--skip-existing', action='store_true')
|
|
|
|
| 298 |
_pool(_job, jobs, a.jobs)
|
| 299 |
for i in a.inst:
|
| 300 |
b = best_route(a.N, i, a.out)
|
| 301 |
+
t = f" (INIT {t_init[i]:.1f} s)" if i in t_init else ''
|
| 302 |
print(f"N{a.N} i{i}: " + (f"best route {b[0]} cost {b[1]['eval']['cost']:.6f}" if b else 'no feasible route') + t,
|
| 303 |
flush=True)
|
| 304 |
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""Smoke test: rerun a few runs of the synthetic study and compare with the reference values
|
| 2 |
(reference/smoke_reference.json).
|
| 3 |
|
| 4 |
-
GlobMP on the 2-robot instance 0: the default (INIT
|
| 5 |
8 classes; with --full also the 4-robot instance 12, 16 classes); RRT-Connect plus post-processing on the
|
| 6 |
2-robot instance 0; the scaling run at 2 robots, seed 0.
|
| 7 |
|
|
@@ -48,8 +48,8 @@ def main(argv=None):
|
|
| 48 |
cases = [(2, 0)] + ([(4, 12)] if a.full else [])
|
| 49 |
q = ref['N2_i0_init']; d = os.path.join(a.out, 'init')
|
| 50 |
RUN.main(['--N', '2', '--inst', '0', '--out', d, '--t-max', str(a.t_max)])
|
| 51 |
-
b = RUN.best_route(2, 0, d); print(f"GlobMP default N=2 instance 0: INIT
|
| 52 |
-
good &= bool(b and b[0] == q['cls']) and _row('INIT
|
| 53 |
for N, i in cases:
|
| 54 |
RUN.main(['--N', str(N), '--inst', str(i), '--routes', 'all', '--jobs', str(a.jobs), '--out', a.out, '--t-max', str(a.t_max)])
|
| 55 |
R = ref[f'N{N}_i{i}_ours_routes']; print(f'GlobMP N={N} instance {i}:')
|
|
|
|
| 1 |
"""Smoke test: rerun a few runs of the synthetic study and compare with the reference values
|
| 2 |
(reference/smoke_reference.json).
|
| 3 |
|
| 4 |
+
GlobMP on the 2-robot instance 0: the default (INIT class, one solve) and every route class (--routes all,
|
| 5 |
8 classes; with --full also the 4-robot instance 12, 16 classes); RRT-Connect plus post-processing on the
|
| 6 |
2-robot instance 0; the scaling run at 2 robots, seed 0.
|
| 7 |
|
|
|
|
| 48 |
cases = [(2, 0)] + ([(4, 12)] if a.full else [])
|
| 49 |
q = ref['N2_i0_init']; d = os.path.join(a.out, 'init')
|
| 50 |
RUN.main(['--N', '2', '--inst', '0', '--out', d, '--t-max', str(a.t_max)])
|
| 51 |
+
b = RUN.best_route(2, 0, d); print(f"GlobMP default N=2 instance 0: INIT class reference {q['cls']}, rerun {b[0] if b else None}")
|
| 52 |
+
good &= bool(b and b[0] == q['cls']) and _row('INIT class', q, b[1]['eval'], a.rtol)
|
| 53 |
for N, i in cases:
|
| 54 |
RUN.main(['--N', str(N), '--inst', str(i), '--routes', 'all', '--jobs', str(a.jobs), '--out', a.out, '--t-max', str(a.t_max)])
|
| 55 |
R = ref[f'N{N}_i{i}_ours_routes']; print(f'GlobMP N={N} instance {i}:')
|
|
@@ -70,6 +70,49 @@ def test_certificate_on_one_disc():
|
|
| 70 |
assert float(block_minima(p, Zf).min()) > -1e-9
|
| 71 |
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
if __name__ == '__main__':
|
| 74 |
for name, f in list(globals().items()):
|
| 75 |
if name.startswith('test_'):
|
|
|
|
| 70 |
assert float(block_minima(p, Zf).min()) > -1e-9
|
| 71 |
|
| 72 |
|
| 73 |
+
def test_init_against_enumeration():
|
| 74 |
+
"""INIT (globmp.init) on random state sets against listing every curve, per label, for both rules."""
|
| 75 |
+
import itertools
|
| 76 |
+
from globmp.init import init
|
| 77 |
+
|
| 78 |
+
class Toy:
|
| 79 |
+
n_pieces = 4; n_labels = 2
|
| 80 |
+
|
| 81 |
+
def __init__(self, seed):
|
| 82 |
+
r = np.random.default_rng(seed); self.ns = [1, 6, 5, 6, 1]
|
| 83 |
+
self.w = [r.integers(1, 9, size=(a, b)).astype(float) for a, b in zip(self.ns[:-1], self.ns[1:])]
|
| 84 |
+
self.ok = [r.random((a, b)) < 0.6 for a, b in zip(self.ns[:-1], self.ns[1:])]
|
| 85 |
+
self.flip = r.random((self.ns[2], self.ns[3])) < 0.5 # piece 2 sets the label
|
| 86 |
+
self.tg = [np.array([min(sum(self.w[i][c[k], c[k + 1]] for k, i in enumerate(range(j, 4)))
|
| 87 |
+
for c in itertools.product(*[[s] if q == j else range(n) for q, n in list(enumerate(self.ns))[j:]]))
|
| 88 |
+
for s in range(self.ns[j])]) for j in range(5)]
|
| 89 |
+
|
| 90 |
+
def n_states(self, j): return self.ns[j]
|
| 91 |
+
def cost(self, i, s, V): return V[:, None] + self.w[i][s]
|
| 92 |
+
def free(self, i, s): return self.ok[i][s]
|
| 93 |
+
def togo(self, j): return self.tg[j]
|
| 94 |
+
def label(self, i, s, lab): return lab[:, None] | self.flip[s] if i == 2 else lab[:, None]
|
| 95 |
+
|
| 96 |
+
class ToyPairs(Toy): # the piece test given pair by pair
|
| 97 |
+
def free_pairs(self, i, s, t): return self.ok[i][s, t]
|
| 98 |
+
|
| 99 |
+
for seed in range(20):
|
| 100 |
+
t = Toy(seed); tp = ToyPairs(seed); best = {}
|
| 101 |
+
for c in itertools.product(*[range(n) for n in t.ns]):
|
| 102 |
+
if all(t.ok[i][c[i], c[i + 1]] for i in range(4)):
|
| 103 |
+
f = sum(t.w[i][c[i], c[i + 1]] for i in range(4)); l = int(t.flip[c[2], c[3]])
|
| 104 |
+
if l not in best or f < best[l]: best[l] = f
|
| 105 |
+
U = min(best.values()) * 1.5 if best else 10.0
|
| 106 |
+
want = {l: f for l, f in best.items() if f <= U}
|
| 107 |
+
for rule in ('togo', 'budget'):
|
| 108 |
+
for ties, inst in (('first', t), ('state', t), ('first', tp), ('state', tp)):
|
| 109 |
+
r = init(inst, U, rule=rule, ties=ties, chunk=7)
|
| 110 |
+
assert {l: v[0] for l, v in r['curves'].items()} == want
|
| 111 |
+
for l, (f, path) in r['curves'].items():
|
| 112 |
+
assert all(t.ok[i][path[i], path[i + 1]] for i in range(4)) and int(t.flip[path[2], path[3]]) == l
|
| 113 |
+
assert sum(t.w[i][path[i], path[i + 1]] for i in range(4)) == f
|
| 114 |
+
|
| 115 |
+
|
| 116 |
if __name__ == '__main__':
|
| 117 |
for name, f in list(globals().items()):
|
| 118 |
if name.startswith('test_'):
|