"""Best-effort layout of cities on a plane (or in free 3D space) from measured lengths and limits. Lengths (flights) must fall inside a band [L(1-lo), L(1+hi)]; limits (round trips, rail, road, satellite passes) cap a distance from above. Nothing else is imposed: no latitude, longitude, radius or projection enters the objective. The search runs in two stages, mirrored by the page's live solver: 1. least squares on every flight's relative error (smooth, finds the overall shape); 2. reweighted pseudo-L1 on the amount by which each measurement falls outside its band, so the solver gives up on as few measurements as it can rather than straining many (an approximation to minimising the number broken). """ import numpy as np from scipy.optimize import minimize LIMIT_W = 3.0 # weight of a limit relative to a flight in stage 2 MARGIN = 0.005 # stage 2 aims this far inside each band DELTA = 0.003 # pseudo-L1 smoothing EPS = 0.02 # reweighting floor ROUNDS = 8 class Problem: def __init__(self, n, fa, fb, fl, la, lb, lu): self.n = n self.fa, self.fb, self.fl = np.asarray(fa), np.asarray(fb), np.asarray(fl, float) self.la, self.lb, self.lu = np.asarray(la, int), np.asarray(lb, int), np.asarray(lu, float) def _grad(self, dim, X, ga, gl): G = np.empty((self.n, dim)); n = self.n gv = ga[:, None] * (X[self.fa] - X[self.fb]) hv = gl[:, None] * (X[self.la] - X[self.lb]) if len(self.lu) else None for q in range(dim): G[:, q] = np.bincount(self.fa, gv[:, q], n) - np.bincount(self.fb, gv[:, q], n) if hv is not None: G[:, q] += np.bincount(self.la, hv[:, q], n) - np.bincount(self.lb, hv[:, q], n) return G.ravel() def dists(self, X): dd = np.sqrt(((X[self.fa] - X[self.fb]) ** 2).sum(1)) + 1e-9 ld = np.sqrt(((X[self.la] - X[self.lb]) ** 2).sum(1)) + 1e-9 if len(self.lu) else np.zeros(0) return dd, ld def l2(self, dim, wl=100.0): def f(x): X = x.reshape(self.n, dim); dd, ld = self.dists(X) e = dd / self.fl - 1; ex = np.maximum(0, ld / self.lu - 1) return (e ** 2).sum() + wl * (ex ** 2).sum(), self._grad(dim, X, 2 * e / self.fl / dd, wl * 2 * ex / self.lu / ld) return f def sparse(self, dim, lo, hi, w, wl): a = self.fl * (1 - lo + MARGIN); b = self.fl * (1 + hi - MARGIN); ub = self.lu * (1 - MARGIN) def f(x): X = x.reshape(self.n, dim); dd, ld = self.dists(X) eh = np.maximum(0, dd / b - 1); el = np.maximum(0, 1 - dd / a) sh = np.sqrt(eh ** 2 + DELTA ** 2); sl = np.sqrt(el ** 2 + DELTA ** 2) ex = np.maximum(0, ld / ub - 1); sx = np.sqrt(ex ** 2 + DELTA ** 2) F = (w * (sh + sl - 2 * DELTA)).sum() + (wl * (sx - DELTA)).sum() ga = w * (eh / sh / b - el / sl / a) / dd gl = wl * ex / sx / ub / ld return F, self._grad(dim, X, ga, gl) return f def excess(self, X, lo, hi): dd, ld = self.dists(X); r = dd / self.fl return np.maximum(0, r / (1 + hi) - 1) + np.maximum(0, 1 - r / (1 - lo)), np.maximum(0, ld / self.lu - 1) def broken(self, X, lo, hi): e, l = self.excess(X, lo, hi) return int((e > 0).sum() + (l > 5e-4).sum()) def best_effort(self, X0, lo, hi, rounds=ROUNDS, maxiter=30000, eps=EPS, limit_w=LIMIT_W): dim = X0.shape[1] run = lambda f, X: minimize(f, X.ravel(), jac=True, method="L-BFGS-B", options={"maxiter": maxiter, "maxcor": 30}).x.reshape(self.n, dim) X = run(self.l2(dim), X0) best = (self.broken(X, lo, hi), X.copy()) w = np.ones(len(self.fl)); wl = np.full(len(self.lu), limit_w) for _ in range(rounds): X = run(self.sparse(dim, lo, hi, w, wl), X) k = self.broken(X, lo, hi) if k < best[0]: best = (k, X.copy()) e, l = self.excess(X, lo, hi) w = eps / (e + eps); wl = limit_w * eps / (l + eps) return best def sphere_fit(X): """Least-squares sphere through 3D points: radius, median offset (share of radius), and how far round it the points reach (angle from their mean direction to the farthest point).""" A = np.c_[2 * X, np.ones(len(X))]; s = np.linalg.lstsq(A, (X ** 2).sum(1), rcond=None)[0] c = s[:3]; r = float(np.sqrt(s[3] + c @ c)); d = np.linalg.norm(X - c, axis=1) u = (X - c) / d[:, None]; m = u.mean(0); m /= np.linalg.norm(m) return dict(radius=r, offset=float(np.median(np.abs(d - r) / r)), wrap=float(np.degrees(np.arccos(np.clip(u @ m, -1, 1))).max()), centre=c.tolist()) # ---- measured along a sphere: concedes the shape, leaves its size free ---- def _arcs(P, R, a, b): nrm = np.linalg.norm(P, axis=1); U = P / nrm[:, None] u, v = U[a], U[b]; c = np.clip((u * v).sum(1), -1, 1); s = np.linalg.norm(np.cross(u, v), axis=1) th = np.arctan2(s, c); s = np.maximum(s, 1e-9) gu = -(v - c[:, None] * u) / (s * nrm[a])[:, None]; gv = -(u - c[:, None] * v) / (s * nrm[b])[:, None] return R * th, th, gu, gv class SphereProblem(Problem): """Cities held on a sphere of free radius; each distance measured along its surface (the haversine arc, R x central angle). Stage 1 fits the radius with the cities; later stages hold it.""" def _pairs(self, P, R): return _arcs(P, R, self.fa, self.fb), _arcs(P, R, self.la, self.lb) def _apply(self, ga, gl, fa_, la_, R): (dd, th, gu, gv), (ld, lth, lgu, lgv) = fa_, la_; n = self.n; G = np.zeros((n, 3)) for q in range(3): G[:, q] = (np.bincount(self.fa, ga * R * gu[:, q], n) + np.bincount(self.fb, ga * R * gv[:, q], n) + np.bincount(self.la, gl * R * lgu[:, q], n) + np.bincount(self.lb, gl * R * lgv[:, q], n)) return G.ravel(), float((ga * th).sum() + (gl * lth).sum()) def arc_l2(self, wl=100.0): def f(x): P = x[:-1].reshape(self.n, 3); R = x[-1]; F_, L_ = self._pairs(P, R); dd, ld = F_[0], L_[0] e = dd / self.fl - 1; ex = np.maximum(0, ld / self.lu - 1) G, gR = self._apply(2 * e / self.fl, wl * 2 * ex / self.lu, F_, L_, R) return (e ** 2).sum() + wl * (ex ** 2).sum(), np.r_[G, gR] return f def arc_sparse(self, R, lo, hi, w, wl): a = self.fl * (1 - lo + MARGIN); b = self.fl * (1 + hi - MARGIN); ub = self.lu * (1 - MARGIN) def f(x): P = x.reshape(self.n, 3); F_, L_ = self._pairs(P, R); dd, ld = F_[0], L_[0] eh = np.maximum(0, dd / b - 1); el = np.maximum(0, 1 - dd / a) sh = np.sqrt(eh ** 2 + DELTA ** 2); sl = np.sqrt(el ** 2 + DELTA ** 2) ex = np.maximum(0, ld / ub - 1); sx = np.sqrt(ex ** 2 + DELTA ** 2) G, _ = self._apply(w * (eh / sh / b - el / sl / a), wl * ex / sx / ub, F_, L_, R) return (w * (sh + sl - 2 * DELTA)).sum() + (wl * (sx - DELTA)).sum(), G return f def arc_broken(self, P, R, lo, hi): F_, L_ = self._pairs(P, R); r = F_[0] / self.fl return dict(long=int((r > 1 + hi).sum()), short=int((r < 1 - lo).sum()), limits=int((L_[0] > self.lu * 1.0005).sum())) def arc_best_effort(self, X2, lo, hi, rounds=ROUNDS, maxiter=30000, eps=EPS, limit_w=LIMIT_W): """From a flat layout laid on a spherical cap whose radius is a third of the layout's width.""" X2 = X2 - X2.mean(0); R0 = float(np.ptp(X2, axis=0).max()) / 3 x = np.r_[np.c_[X2, np.full(self.n, R0)].ravel(), R0] x = minimize(self.arc_l2(), x, jac=True, method="L-BFGS-B", options={"maxiter": maxiter, "maxcor": 30}).x P, R = x[:-1].reshape(self.n, 3), float(x[-1]) k = sum(self.arc_broken(P, R, lo, hi).values()); best = (k, P.copy()) w = np.ones(len(self.fl)); wl = np.full(len(self.lu), limit_w) for _ in range(rounds): P = minimize(self.arc_sparse(R, lo, hi, w, wl), P.ravel(), jac=True, method="L-BFGS-B", options={"maxiter": maxiter, "maxcor": 30}).x.reshape(self.n, 3) k = sum(self.arc_broken(P, R, lo, hi).values()) if k < best[0]: best = (k, P.copy()) F_, L_ = self._pairs(P, R); r = F_[0] / self.fl e = np.maximum(0, r / (1 + hi) - 1) + np.maximum(0, 1 - r / (1 - lo)); l = np.maximum(0, L_[0] / self.lu - 1) w = eps / (e + eps); wl = limit_w * eps / (l + eps) P = best[1]; U = P / np.linalg.norm(P, axis=1)[:, None] return R, U, self.arc_broken(P, R, lo, hi) def true_offsets(U, lat, lon, R): """Distance of each solved city from its true place, after the orthogonal transformation that best lines the true positions up with the solved ones (used to place coastlines and to check the fit).""" p, l = np.radians(lat), np.radians(lon) T = np.c_[np.cos(p) * np.cos(l), np.cos(p) * np.sin(l), np.sin(p)] Us, _, Vt = np.linalg.svd(U.T @ T); Q = Us @ Vt return R * np.arccos(np.clip(((T @ Q.T) * U).sum(1), -1, 1))