tarzanagh commited on
Commit
d49cd30
·
verified ·
1 Parent(s): 4535cef

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 CHANGED
@@ -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, Algorithm 3 (INIT): the grid start of Theorem 1 by a shortest path over the joints
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-P 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) |
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 |
globmp/gridstart/README.md CHANGED
@@ -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 (`init`; comments give the line numbers of Algorithm 3) and the plain grid (`plain_grid`) |
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. It is not run on the team
64
- instances of `globmp/synthetic/`, where a joint has 16 to 64 coordinates.
 
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`. At N = 2 they need more than the default
67
- 6000 iterations to come within 1e-5 of `E*_2` (`--T 100000`).
 
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.
globmp/gridstart/instance.py CHANGED
@@ -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`), route classes, and the exact rational check of a whole curve.
 
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)."""
globmp/gridstart/{init.py → plain_grid.py} RENAMED
@@ -1,88 +1,9 @@
1
- """INIT (Algorithm 3 of the paper): the grid start z_h of Theorem 1 by a shortest path over the joints, and the
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 GOAL, START, K4, Quad, ctrl, idx_joint, pieces_free
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 = _lattice(lo[inner], hi[inner], h)
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
globmp/gridstart/run.py CHANGED
@@ -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 budget rule, INIT with the budget + cost-to-go rule, the plain grid
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, TOL_SCALE
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() * TOL_SCALE, T=T, t_max=t_max)
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 = init(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,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 = init(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 = init(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)
 
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)
globmp/init.py ADDED
@@ -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)
globmp/synthetic/README.md CHANGED
@@ -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-P (a reduced INIT grid search, `init.py`) picks one route class, then one first-order run of
8
- Algorithms 1-2 from the INIT-P curve. No Newton step, no SQP, no polish. With `--routes all`, GlobMP solves every
9
- route class from a waypoint path and keeps the cheapest; that run gives `reference/` and Fig. `fig_synth1`.
 
 
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
- | `init.py` | INIT-P: route-class ranking by a dynamic program over the joints (grid h = 0.05 m) |
19
- | `run.py` | GlobMP on a main-comparison instance: INIT-P class (default), or every route class (`--routes all`) |
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-P class, one solve per instance (main way to run)
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-P classes; `--routes ppmp,pppp` solves the listed classes. Use separate output
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-P (`init.py`): joint y-coordinates on a grid of spacing h = 0.05 m within 2 m of the rows, joint x-coordinates
71
- from the obstacle-free optimum, zero jets, one DP per exchanging pair with exact piece tests and budget pruning
72
- (1.3 x the coarse-grid cost). It drops INIT's guarantee (the reduced family stops at every joint); its curve is
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-P + one solve): instances with cost equal to the all-class best (within 1e-5) | 20/20 | 19/20 | 18/20 | 20/20 |
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-P takes 1 to 3 s of the instance time.
 
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-P class, cost, ratio to the all-class best, time (INIT-P + solve); the same for `--top 2` |
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>_initp.json` (INIT-P ranking, default mode), `<S>_rrt.json` (+ `_path.npy`, `.npy`)
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.
globmp/synthetic/__init__.py CHANGED
@@ -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-P picks a route class, then first-order Algorithm 2
7
  (--routes all: every route class)
8
- init INIT-P, the route-class 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)
 
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)
globmp/synthetic/{init.py → joints.py} RENAMED
@@ -1,23 +1,22 @@
1
- """INIT-P: route-class ranking for the main-comparison instances (2..8 planar robots, minimum snap, M = 7 pieces).
2
-
3
- A reduced form of the paper's INIT grid search:
4
- * dynamic program over the M+1 joints, one layer per piece, with back-pointers; exact piece test on the whole
5
- piece interval; budget pruning (a transition is dropped when V(s) + w(s,s') + togo(s') > U, togo = the exact
6
- obstacle-free lower bound on the remaining pieces, U = 1.3 x the cost of the coarse-grid (2h) solution).
7
- * joint state: the robots' y-coordinates on the grid y_row + h Z, |y - y_row| <= 2 m; the x-coordinates at the
8
- joints are those of the obstacle-free optimum, and the jets (derivatives 1..4) at every joint are 0. Every
9
- piece is then a + (b - a) H(s), H the degree-9 smoothstep (Bernstein coefficients 0,0,0,0,0,1,1,1,1,1): its
10
- configuration path is a straight segment, the piece test is an exact segment/disc distance, and its cost is
11
- c_H |b - a|^2, so the DP is exact on this family.
12
- * one DP per exchanging pair (its two robots, every sphere test and the pair test; the route bits of the pair and
13
- of its robots' sphere crossings are carried in the state). Team classes are ranked by summed DP cost; the
14
- separations between pairs are checked exactly on the assembled curve.
15
- The reduced family (curves that stop at every joint) is a strict subset of the curve space, so INIT's guarantee
16
- does not carry over: the best-ranked class need not hold the global optimum. Every INIT-P curve is feasible and
17
- lies in the class it reports, so its cost is an upper bound.
18
-
19
- run_init writes <out>/N<N>_i<i>_initp.json (ranking, time) and returns the ranked classes with their joint
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
- def dp_cluster(ins, X, yrow, rob, bits, h, Yr, U, delta=I.DELTA):
69
- """INIT-P on one cluster (two robots rob = (ri, rj)). bits: list of ('pair', None) / ('cross', (robot, c)).
70
- Returns {mask: (cost_y, ypath (M+1, 2))} for every route mask reachable within the budget, and stats."""
71
- M = X.shape[0] - 1; K = int(round(Yr / h)); n = 2 * K + 1; g = h * np.arange(-K, K + 1)
72
- ri, rj = rob; balls = ins['balls']; R = I.RS + delta; w = CH * h * h
73
- W = w * (np.arange(n)[:, None] - np.arange(n)[None, :]) ** 2 # (a, b) y-cost
74
- # per piece tables
75
- tabs = []
76
- for m in range(M):
77
- T = {}
78
- for key, r in (('i', ri), ('j', rj)):
79
- ya = yrow[r] + g[:, None]; yb = yrow[r] + g[None, :]; ok = np.ones((n, n), bool)
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
- rxa = X[m, ri] - X[m, rj]; rxb = X[m + 1, ri] - X[m + 1, rj]
85
- T['okp'] = seg_disc_dist(rxa, e0 + dg[:, None], rxb, e0 + dg[None, :], 0.0, 0.0) >= R
86
- T['bi'] = np.zeros((n, n), np.int64); T['bj'] = np.zeros((n, n), np.int64); T['bp'] = np.zeros((4 * K + 1, 4 * K + 1), np.int64)
87
- tabs.append(T)
88
- for q, (kind, arg) in enumerate(bits):
89
- if kind == 'pair':
90
- rx = X[:, ri] - X[:, rj]; m, lam = cross_piece(rx, 0.0)
91
- e0 = yrow[ri] - yrow[rj]; dg = h * np.arange(-2 * K, 2 * K + 1)
92
- ry = e0 + dg[:, None] + (dg[None, :] - dg[:, None]) * lam
93
- tabs[m]['bp'] |= (ry < 0).astype(np.int64) << q
94
- else:
95
- r, (cx, cy) = arg; m, lam = cross_piece(X[:, r], cx)
96
- y = yrow[r] + g[:, None] + (g[None, :] - g[:, None]) * lam
97
- tabs[m]['bi' if r == ri else 'bj'] |= (y > cy).astype(np.int64) << q
98
- Bm = 1 << len(bits)
99
- V = np.full((Bm, n, n), np.inf); V[0, K, K] = 0.0
100
- preds = []; stats = dict(transitions=0, budget_pass=0, feasible=0)
101
- for m in range(M):
102
- T = tabs[m]; last = m == M - 1
103
- tg = np.zeros((n, n)) if last else w * (np.arange(-K, K + 1)[:, None] ** 2 + np.arange(-K, K + 1)[None, :] ** 2) / (M - m - 1)
104
- bS, a0S, a1S = np.nonzero(np.isfinite(V)); VS = V[bS, a0S, a1S]
105
- dS = a0S - a1S + 2 * K
106
- Vn = np.full((Bm, n, n), np.inf); Pn = np.full((Bm, n, n), -1, np.int64)
107
- b0s = [K] if last else range(n); b1s = np.array([K]) if last else np.arange(n)
108
- db = b1s.copy() # placeholder
109
- for b0 in b0s:
110
- dbi = b0 - b1s + 2 * K
111
- c = VS[:, None] + W[a0S, b0][:, None] + W[a1S][:, b1s]
112
- stats['transitions'] += c.size
113
- bud = c + tg[b0, b1s][None, :] <= U
114
- stats['budget_pass'] += int(bud.sum())
115
- f = bud & T['oki'][a0S, b0][:, None] & T['okj'][a1S][:, b1s] & T['okp'][dS][:, dbi]
116
- stats['feasible'] += int(f.sum())
117
- if not f.any(): continue
118
- c = np.where(f, c, np.inf)
119
- nb = bS[:, None] | T['bi'][a0S, b0][:, None] | T['bj'][a1S][:, b1s] | T['bp'][dS][:, dbi]
120
- for nm in np.unique(nb[f]):
121
- cm = np.where(nb == nm, c, np.inf); j = np.argmin(cm, 0); v = cm[j, np.arange(len(b1s))]
122
- cur = Vn[nm, b0, b1s]; better = v < cur
123
- Vn[nm, b0, b1s[better]] = v[better]; Pn[nm, b0, b1s[better]] = j[better]
124
- preds.append((bS, a0S, a1S, Pn)); V = Vn
125
- out = {}
126
- for mask in range(Bm):
127
- if not np.isfinite(V[mask, K, K]): continue
128
- path = np.zeros((M + 1, 2), int); path[M] = (K, K); st = (mask, K, K)
129
- for m in range(M - 1, -1, -1):
130
- bS, a0S, a1S, Pn = preds[m]; j = Pn[st]; st = (bS[j], a0S[j], a1S[j]); path[m] = (a0S[j], a1S[j])
131
- assert path[0, 0] == K and path[0, 1] == K and st[0] == 0
132
- ypath = np.stack([yrow[ri] + g[path[:, 0]], yrow[rj] + g[path[:, 1]]], 1)
133
- touch = bool((np.abs(path - K) == K).any())
134
- out[mask] = (float(V[mask, K, K]), ypath, touch)
135
- return out, stats
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
 
138
  def clusters(ins):
@@ -154,9 +164,10 @@ def team_poly(X, ypaths, cls, N):
154
  return Q
155
 
156
 
157
- def init_p(N, i, h=0.1, Yr=2.0, slack=0.3, topk=3):
158
- """INIT-P for instance (N, i): coarse pass (2h, no budget) -> U; fine pass (h, budget U) -> per-class costs per
159
- cluster; team classes ranked by summed cost; inter-cluster tests on the assembled curve; top-k feasible."""
 
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 = dp_cluster(ins, X, yrow, c['rob'], c['bits'], hh, Yr, Uc); R.append((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), dp_cost=cost, Q=Q, sep_margin=sep, ball_margin=ball,
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 run_init(N, i, out, h=0.05, top=1):
204
- """INIT-P on instance (N, i): writes <out>/N<N>_i<i>_initp.json and returns dict(i, time, ranked), where each
205
- ranked entry has the class tag, its DP cost and its joint configurations Q (M+1, N, 2)."""
206
- r = init_p(N, i, h=h, topk=top)
207
- ranked = [dict(tag=d['tag'], dp_cost=d['dp_cost'], Q=d['Q'].tolist()) for d in r['ranked']]
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'], dp_cost=d['dp_cost'], sep_margin=d['sep_margin'], ball_margin=d['ball_margin'])
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}_initp.json'), 'w') as fh: json.dump(rec, fh, indent=0, default=float)
213
- print(f"[INIT-P N{N} i{i}] {r['time']:.1f}s " + ' '.join(f"{d['tag']}:{d['dp_cost']:.3f}" for d in r['ranked']), flush=True)
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
 
globmp/synthetic/reference/init_summary.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "_about": "INIT-P (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-P + solve(s) per instance, one core per solve (AMD EPYC 7502).",
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": {
globmp/synthetic/reference/smoke_reference.json CHANGED
@@ -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-P 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,
 
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,
globmp/synthetic/report.py CHANGED
@@ -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-P 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,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}_initp.json'); t0 = ini['time'] if ini else 0.0 # INIT-P 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'):
 
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'):
globmp/synthetic/run.py CHANGED
@@ -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-P (init.py, grid h = 0.05 m) picks one route class from a reduced grid search, and one
5
- Algorithm-2 solve runs from its INIT-P curve (--top K solves the K best-ranked classes instead).
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>_initp.json
33
- (the INIT-P 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-P, 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,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.n_it), t_alg1=float(p.t_it))
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-P
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 .init import poly_to_Z
227
- Z0 = poly_to_Z(p, np.asarray(start[1])); how = f'initp_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,8 +252,8 @@ def best_route(N, i, res):
252
 
253
  def _init_job(args):
254
  torch.set_num_threads(1)
255
- from .init import run_init
256
- return run_init(*args)
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-P 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-P 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,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-P {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
  """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
 
globmp/synthetic/smoke.py CHANGED
@@ -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-P 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,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-P class reference {q['cls']}, rerun {b[0] if b else None}")
52
- good &= bool(b and b[0] == q['cls']) and _row('INIT-P 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}:')
 
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}:')
globmp/tests/test_core.py CHANGED
@@ -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_'):