GlobMP / globmp /gridstart /instance.py
tarzanagh's picture
One INIT: globmp/init.py serves the grid start and the synthetic study
d49cd30 verified
Raw History Blame Contribute Delete
13.8 kB
"""The grid-start instance: one robot in the plane passes two discs; start (0,0), goal (10,0); acceleration cost
(k = 2), endpoint positions fixed (ell = 0), inflation margin 0.005; N cubic pieces on equal intervals joined C^1
(the class S_N of Theorem 1).
Free control points (the paper's rational chart), 4N numbers:
x = [ c0 | (a_1, Q_1) | ... | (a_{N-1}, Q_{N-1}) | a_N ]
c0 = second control point of piece 0, Q_j = position at joint j, a_j = control point just before joint j.
Piece i has control points (Q_i, o_i, a_{i+1}, Q_{i+1}) with Q_0 = START, Q_N = GOAL, o_0 = c0, o_i = 2 Q_i - a_i
(C^1 with equal piece durations; an integer map, so the grid h Z^{4N} is a product over the joints).
Time scale: the package gives every piece unit duration, so cost(total duration 1) = N^3 * cost(package).
Contents: the program (`build`), the chart, the cost as a quadratic (`Quad`), the exact test of one piece
(`pieces_free`), what INIT needs (`GridStates`: the grid values per joint, the piece cost, the piece test; `grid_start`
calls globmp.init), route classes, and the exact rational check of a whole curve.
"""
import math
import time
import numpy as np
import torch
from .. import Problem, DT
from .. import spline as bern
from ..check import block_minima
from ..init import init
torch.set_num_threads(1)
START = np.array([0.0, 0.0]); GOAL = np.array([10.0, 0.0])
DISCS = [((3.3, 0.25), 1.0), ((6.7, -0.35), 1.0)]
DELTA = 0.005
D = 3
K4 = bern.cost_gram(D, 2).numpy() # 4x4 Gram of the acceleration cost of one piece
CD = np.array([math.comb(D, j) for j in range(D + 1)], float)
C2 = np.array([math.comb(2 * D, j) for j in range(2 * D + 1)], float)
def build(N, whiten=False, bb=False, stop_rel=1e-10):
p = Problem(2, D, 2, 0, N, START, GOAL, ell_j=1, delta=DELTA, whiten=whiten, bb=bb, stop_rel=stop_rel)
for c, r in DISCS: p.add_sphere(c, r)
return p
# ------------------------------------------------------------------ chart
def idx_joint(j, N):
"""x indices of the state at joint j (0: c0, N: a_N, else (a_j, Q_j))."""
if j == 0: return [0, 1]
if j == N: return [4 * N - 2, 4 * N - 1]
return list(range(2 + 4 * (j - 1), 6 + 4 * (j - 1)))
def ctrl(x, N):
"""x (..., 4N) -> control points (..., N, 4, 2)."""
x = np.asarray(x, float); lead = x.shape[:-1]
C = np.zeros(lead + (N, 4, 2))
Q = [np.broadcast_to(START, lead + (2,))]; a = [None]
for j in range(1, N):
ij = idx_joint(j, N); a.append(x[..., ij[0]:ij[0] + 2]); Q.append(x[..., ij[0] + 2:ij[0] + 4])
a.append(x[..., 4 * N - 2:4 * N]); Q.append(np.broadcast_to(GOAL, lead + (2,)))
for i in range(N):
o = x[..., 0:2] if i == 0 else 2 * Q[i] - a[i]
C[..., i, 0, :] = Q[i]; C[..., i, 1, :] = o; C[..., i, 2, :] = a[i + 1]; C[..., i, 3, :] = Q[i + 1]
return C
def x_to_G(x, N):
C = ctrl(x, N) # (N,4,2)
return np.transpose(C, (2, 0, 1)).reshape(2, N * 4)
def G_to_Z(p, G):
"""chart point of a coefficient matrix that satisfies the linear constraints (any chart, also whitened)."""
R = (torch.as_tensor(G, dtype=DT) - p.Gamma0)
Z = torch.linalg.lstsq(p.N, R.T).solution.T
assert float((p.Gamma(Z) - torch.as_tensor(G, dtype=DT)).abs().max()) < 1e-8, 'G violates the linear constraints'
return Z
def Z_to_x(p, Z, N):
S = p.segs(torch.as_tensor(Z, dtype=DT)).detach().numpy() # (N, 2, 4)
x = np.zeros(4 * N); x[0:2] = S[0, :, 1]
for j in range(1, N):
ij = idx_joint(j, N); x[ij[0]:ij[0] + 2] = S[j - 1, :, 2]; x[ij[0] + 2:ij[0] + 4] = S[j - 1, :, 3]
x[4 * N - 2:] = S[N - 1, :, 2]
return x
class Quad:
"""f(x) = c + 2 g.x + x^T H x for the whole curve and for every suffix of pieces (cost-to-go bounds)."""
def __init__(self, N):
self.N = N; m = 4 * N
C0 = ctrl(np.zeros(m), N); J = np.stack([ctrl(e, N) - C0 for e in np.eye(m)], -1) # (N,4,2,m)
self.C0, self.J = C0, J
self.c, self.g, self.H = self._quad(range(N))
Hi = np.linalg.inv(self.H); self.xs = -Hi @ self.g; self.fmin = self.c + self.g @ self.xs; self.Hi = Hi
self.togo_data = {}
for j in range(N):
c, g, H = self._quad(range(j, N)); s = idx_joint(j, N); r = list(range(s[-1] + 1, m))
Hrr_i = np.linalg.inv(H[np.ix_(r, r)])
self.togo_data[j] = (c, g[s], H[np.ix_(s, s)], g[r], H[np.ix_(r, s)], Hrr_i)
def _quad(self, pieces):
pieces = list(pieces); C0 = self.C0[pieces]; J = self.J[pieces]
H = np.einsum('pq,ipcm,iqcn->mn', K4, J, J); g = np.einsum('pq,ipc,iqcn->n', K4, C0, J)
return float(np.einsum('pq,ipc,iqc->', K4, C0, C0)), g, H
def f(self, x):
x = np.asarray(x, float); return self.c + 2 * x @ self.g + np.einsum('...m,mn,...n->...', x, self.H, x)
def togo(self, j, S):
"""lower bound on the cost of pieces j..N-1 from state S (K, dim) at joint j (unconstrained minimum)."""
if j == self.N: return np.zeros(len(S))
c, gs, Hss, gr, Hrs, Hrr_i = self.togo_data[j]
v = gr[None, :] + S @ Hrs.T
return c + 2 * S @ gs + np.einsum('km,mn,kn->k', S, Hss, S) - np.einsum('km,mn,kn->k', v, Hrr_i, v)
def sublevel_count(self, U, h):
"""volume of the ellipsoid {f <= U} divided by h^m: estimate of the number of lattice points with f <= U."""
m = 4 * self.N; ev = np.linalg.eigvalsh(self.H)
logv = 0.5 * m * math.log(math.pi) - math.lgamma(0.5 * m + 1) + 0.5 * m * math.log(max(U - self.fmin, 1e-300)) - 0.5 * np.log(ev).sum()
return math.exp(logv - m * math.log(h))
def box(self, U, h):
"""integer ranges (lo, hi) per coordinate of the lattice h Z^m inside the bounding box of {f <= U}."""
half = np.sqrt(max(U - self.fmin, 0.0) * np.diag(self.Hi))
lo = np.ceil((self.xs - half) / h - 1e-12).astype(int); hi = np.floor((self.xs + half) / h + 1e-12).astype(int)
return lo, hi
# ------------------------------------------------------------------ exact piece test
def piece_coefs(B):
"""control points (K,4,2) -> Bernstein-6 coefficients (K, n_discs, 7) of |x(s)-c|^2 - (r+DELTA)^2."""
K = B.shape[0]; out = []
for c, r in DISCS:
A = (B - np.array(c)[None, None, :]) * CD[None, :, None]
prod = np.zeros((K, 2 * D + 1))
for a in range(D + 1):
prod[:, a:a + D + 1] += (A[:, a:a + 1, :] * A).sum(-1)
out.append(prod / C2 - (r + DELTA) ** 2)
return np.stack(out, 1)
def _split(c):
"""de Casteljau at 1/2: (m,7) -> left (m,7), right (m,7)."""
m, n = c.shape; L = np.empty_like(c); R = np.empty_like(c); cur = c.copy()
for j in range(n):
L[:, j] = cur[:, 0]; R[:, n - 1 - j] = cur[:, -1]
cur = 0.5 * (cur[:, :-1] + cur[:, 1:])
return L, R
def _exact_min(c):
"""minimum over [0,1] of a polynomial given by Bernstein coefficients (roots of the derivative)."""
n = len(c) - 1
pw = np.zeros(n + 1) # power basis, increasing degree
for j in range(n + 1):
for i in range(j, n + 1):
pw[i] += c[j] * math.comb(n, j) * math.comb(n - j, i - j) * (-1) ** (i - j)
P = np.polynomial.Polynomial(pw); cand = [0.0, 1.0]
for r in P.deriv().roots():
if abs(r.imag) < 1e-9 and 0.0 <= r.real <= 1.0: cand.append(r.real)
return min(float(P(t)) for t in cand)
def nonneg(Bc, depth=40):
"""Bc (K,7): True where the polynomial is >= 0 on [0,1]. Nonnegative Bernstein coefficients certify, a
negative end value refutes, otherwise bisect; what is still undecided at the depth limit (tangent cases)
goes to the derivative-root minimisation. Returns (bool (K,), number of root fallbacks)."""
K = len(Bc); res = np.ones(K, bool); idx = np.arange(K); cur = Bc
for _ in range(depth):
neg = (cur[:, 0] < 0) | (cur[:, -1] < 0)
res[idx[neg]] = False
und = ~neg & ~(cur >= 0).all(1) & res[idx]
if not und.any(): return res, 0
cur = cur[und]; idx = idx[und]
L, R = _split(cur); cur = np.concatenate([L, R]); idx = np.concatenate([idx, idx])
left = np.unique(idx[res[idx]])
for j in left: res[j] = _exact_min(Bc[j]) >= 0
return res, len(left)
def pieces_free(B):
"""control points (K,4,2) -> True where the piece clears every inflated disc on its whole interval."""
Bc = piece_coefs(B); ok = np.ones(len(B), bool); nfb = 0
for b in range(Bc.shape[1]):
sel = np.where(ok)[0]
if len(sel) == 0: break
r, f = nonneg(Bc[sel, b]); ok[sel] = r; nfb += f
return ok, nfb
# ------------------------------------------------------------------ INIT on the grid h Z^{4N}
def lattice(lo, hi, h):
ax = [np.arange(l, u + 1) * h for l, u in zip(lo, hi)]
return np.stack(np.meshgrid(*ax, indexing='ij'), -1).reshape(-1, len(ax))
class GridStates:
"""The grid h Z^{4N} inside the bounding box of {f <= U}, as INIT's input (globmp.init): the state at joint j
is the grid value of (a_j, Q_j) (c0 at joint 0, a_N at joint N); piece i has control points
(Q_i, o_i, a_{i+1}, Q_{i+1}); its cost is the acceleration cost, its test is `pieces_free`."""
def __init__(self, N, h, U):
self.q = q = Quad(N); self.N = self.n_pieces = N; self.lo, self.hi = lo, hi = q.box(U, h)
self.T = [lattice(lo[idx_joint(j, N)], hi[idx_joint(j, N)], h) for j in range(N + 1)]
self.root_fallback = 0; self._piece = (None,)
def n_states(self, j): return len(self.T[j])
def _source(self, i, s):
"""(Q_i, o_i) of the states s at joint i."""
S = self.T[i][s]
return (np.broadcast_to(START, S.shape), S) if i == 0 else (S[:, 2:], 2 * S[:, 2:] - S[:, :2])
def _target(self, i):
"""(a_{i+1}, Q_{i+1}) of every state at joint i+1, the target part of the piece cost and the target factor
of its cross term."""
if self._piece[0] != i:
T = self.T[i + 1]; Ta, Tq = (T[:, :2], T[:, 2:]) if i + 1 < self.N else (T, np.broadcast_to(GOAL, T.shape))
v = np.stack([Ta, Tq], 1)
self._piece = (i, Ta, Tq, np.einsum('pq,kpc,kqc->k', K4[2:, 2:], v, v), v.reshape(len(v), 4))
return self._piece[1:]
def cost(self, i, s, V):
Ta, Tq, wt, vf = self._target(i); u = np.stack(self._source(i, s), 1)
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)
tot = 2 * uK @ vf.T; tot += (V + ws)[:, None] + wt[None, :]
return tot
def free_pairs(self, i, s, t):
Sq, So = self._source(i, s); Ta, Tq = self._target(i)[:2]
ok, nfb = pieces_free(np.stack([Sq, So, Ta[t], Tq[t]], 1)); self.root_fallback += nfb
return ok
def togo(self, j): return self.q.togo(j, self.T[j])
def grid_start(N, h, U, rule='togo', improve_only=True):
"""z_h by INIT (globmp.init) on the grid of spacing h with budget U. Returns the cheapest feasible grid point x
(or None = FAIL: no feasible grid point with cost <= U), its cost, and the counts."""
t0 = time.time(); g = GridStates(N, h, U)
r = init(g, U * (1 + 1e-12), rule=rule, improve_only=improve_only)
st = dict(layers=r['layers'], fail=not r['curves'], box=[(int(a), int(b)) for a, b in zip(g.lo, g.hi)],
root_fallback=g.root_fallback)
if not r['curves']: return None, None, dict(st, time=time.time() - t0)
cost, path = r['curves'][0]; x = np.zeros(4 * N)
for j in range(N + 1): x[idx_joint(j, N)] = g.T[j][path[j]]
npts = 1
for a, b in zip(g.lo, g.hi): npts *= int(b - a + 1)
st.update(time=time.time() - t0, grid_points_box=npts, tests=sum(l['tested'] for l in r['layers']),
pairs=sum(l['pairs'] for l in r['layers']), budget_pass=sum(l['budget_pass'] for l in r['layers']))
return x, cost, st
# ------------------------------------------------------------------ route classes and checks
def side_of(p, Z):
"""route class: side of each disc at the curve's closest approach ('p' above the centre)."""
S = p.segs(torch.as_tensor(Z, dtype=DT)).detach().numpy() # (N,2,4)
s = np.linspace(0, 1, 801); i = np.arange(D + 1)
Bm = CD[:, None] * s[None, :] ** i[:, None] * (1 - s[None, :]) ** (D - i[:, None])
pts = np.concatenate([S[k] @ Bm for k in range(S.shape[0])], 1) # (2, N*801)
out = []
for c, r in DISCS:
dd = (pts[0] - c[0]) ** 2 + (pts[1] - c[1]) ** 2; t = int(np.argmin(dd)); out.append('p' if pts[1, t] > c[1] else 'm')
return ''.join(out)
def certify_true(p, Z):
"""exact rational checker (globmp.synthetic.exact) at the true radii, no margin."""
from ..synthetic.exact.certify_curve import Plan, check_plan
from fractions import Fraction
spec = dict(spheres=[([0, 1], (Fraction(c[0]).limit_denominator(1000), Fraction(c[1]).limit_denominator(1000)),
Fraction(r).limit_denominator(1000)) for c, r in DISCS])
segs = p.segs(torch.as_tensor(Z, dtype=DT)).detach().numpy()
return bool(check_plan(Plan(segs), spec, clearance=False)['certified'])
def subdivide_cubic(P4, N):
"""one cubic with control points P4 (4,2) -> coefficient matrix (2, 4N) of the same curve cut into N equal pieces."""
from numpy.polynomial import polynomial as Pl
out = np.zeros((2, 4 * N)); s = np.array([0.0, 1 / 3, 2 / 3, 1.0])
i = np.arange(4); Bm = lambda t: CD[:, None] * t[None, :] ** i[:, None] * (1 - t[None, :]) ** (3 - i[:, None])
Minv = np.linalg.inv(Bm(s)) # values at 4 nodes -> control points
for k in range(N):
t = (k + s) / N; vals = P4.T @ Bm(t) # (2,4) values of the big cubic at the piece's nodes
out[:, 4 * k:4 * k + 4] = vals @ Minv
return out