File size: 13,845 Bytes
b9fe010
 
 
 
 
 
 
 
 
 
 
 
d49cd30
 
b9fe010
 
d49cd30
b9fe010
 
 
 
 
 
 
d49cd30
b9fe010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d49cd30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9fe010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""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