fe-map: optional measurement along a sphere (shape conceded, radius free, haversine distance); land dots in the 3D view

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Toby
2026-10-31 13:44:40 -04:00
parent a5fe852429
commit 01ffa67de6
6 changed files with 482 additions and 64 deletions
+38 -3
View File
@@ -27,7 +27,7 @@ _spec = importlib.util.spec_from_file_location("pingstats_build", os.path.join(S
_pb = importlib.util.module_from_spec(_spec); _spec.loader.exec_module(_pb)
live, land = _pb.live, _pb.land # src/build.py
from models import R, C_FIBER, globe as gc, ae_xy, dualpole_xy, mercator_xy
from fit import Problem, sphere_fit
from fit import Problem, SphereProblem, sphere_fit, true_offsets
PRES = os.path.join(SRC, "handoff", "The_Shape_of_Distance_PRESENTATION.html")
KM_PER_MS = C_FIBER / 2 / 1000 # one-way km per ms of round trip, light in glass
@@ -55,6 +55,32 @@ def page_const(html, name):
return json.JSONDecoder().raw_decode(js[i:])[0]
def land_dots(polys, n=20000):
"""Points spread evenly over the sphere (Fibonacci lattice) that fall on land: the 3D view draws land
as these dots."""
def contains(ring, x, y): # even-odd ray casting in longitude-latitude
r = np.asarray(ring, float); inside = np.zeros(len(x), bool)
box = (x >= r[:, 0].min()) & (x <= r[:, 0].max()) & (y >= r[:, 1].min()) & (y <= r[:, 1].max())
if not box.any():
return inside
xs, ys = x[box], y[box]; hit = np.zeros(len(xs), bool)
for (x1, y1), (x2, y2) in zip(r, np.roll(r, -1, axis=0)):
if y1 == y2:
continue
c = (y1 > ys) != (y2 > ys)
hit ^= c & (xs < (x2 - x1) * (ys - y1) / (y2 - y1) + x1)
inside[box] = hit
return inside
k = np.arange(n) + 0.5; lat = np.degrees(np.arcsin(1 - 2 * k / n)); lon = (np.degrees(np.pi * (1 + 5 ** 0.5) * k) + 180) % 360 - 180
on = np.zeros(n, bool)
for poly in polys:
inside = contains(poly[0], lon, lat)
for hole in poly[1:]:
inside &= ~contains(hole, lon, lat)
on |= inside
return [[round(float(a), 1), round(float(b), 1)] for a, b in zip(lat[on], lon[on])]
def aeq(lat, lon, lat0, lon0):
"""Azimuthal equidistant map centred on (lat0, lon0), km. North-pole AE is (90, 0); dual-pole is (0, 0)."""
p1, l1, p0, l0 = map(np.radians, (np.asarray(lat), np.asarray(lon), lat0, lon0))
@@ -243,6 +269,13 @@ def main():
sph = sphere_fit(X3)
print(f"3D: {k3} broken, radius {sph['radius']:.0f} km, wrap {sph['wrap']:.0f} deg, offset {sph['offset']:.3f}", flush=True)
# Measured along a sphere instead (the shape conceded, its size left free), from the best flat layout.
sp = SphereProblem(len(C), prob.fa, prob.fb, prob.fl, prob.la, prob.lb, prob.lu)
Ra, Ua, bka = sp.arc_best_effort(best["X"], LO, HI)
offa = true_offsets(Ua, C[:, 0], C[:, 1], Ra)
arc = dict(radius=Ra, **bka, off_median=float(np.median(offa)), off_p90=float(np.percentile(offa, 90)))
print(f"along a sphere: radius {Ra:.0f} km, broken {bka}, cities from true place: median {arc['off_median']:.0f} km", flush=True)
bands = []
for j, (lo, hi) in enumerate(BANDS[:1] if FAST else BANDS):
cand = out_jobs[seeds + NB * j: seeds + NB * (j + 1)]
@@ -266,7 +299,7 @@ def main():
below80=int((gr < 0.8).sum()), below85=int((gr < 0.85).sum()), meanlog=float(np.abs(np.log(gr)).mean())),
starts_broken=[[x["label"], x["broken"]] for x in res if "refined" not in x],
n_best=sum(x["broken"] == best["broken"] for x in res),
three=dict(broken=k3, **counts(prob, X3, LO, HI), **sph),
three=dict(broken=k3, **counts(prob, X3, LO, HI), **sph), arc=arc,
globe_long=[[int(prob.fa[i]), int(prob.fb[i]), round(float(gr[i]), 3)] for i in np.where(gr > 1 + HI)[0]],
globe_short=[[int(prob.fa[i]), int(prob.fb[i]), round(float(gr[i]), 3)] for i in np.where(gr < 1 - LO)[0]],
tri=D["tri"], lo=LO, hi=HI, km_per_ms=KM_PER_MS, end_slack=END_SLACK, match_km=MATCH_KM,
@@ -280,7 +313,7 @@ def main():
f["ab"][:5] + [strip(f["ab"][5])], f["ba"][:5] + [strip(f["ba"][5])]] for f in fl],
lim=[[l["a"], l["b"], l["U"], l["kind"], l["note"], l["label"], l["url"]] for l in lim],
gf=[round(float(x), 1) for x in GF], gl=[round(float(x), 1) for x in GL],
land=land(),
land=land(), dots=land_dots(land()),
starts={k: dict(label=v[0], centre=v[1], X=np.round(v[2], 1).tolist()) for k, v in starts.items()},
best=dict(label=best["label"], centre=best["centre"], X=np.round(best["X"], 1).tolist()),
best3=dict(X=np.round(X3, 1).tolist()),
@@ -336,6 +369,8 @@ def copy_values(out):
FIT_B75=next((fmt(x["flat"]["below75"]) for x in out["bands"] if x["flat"]["short"] + x["flat"]["long"] + x["flat"]["limits"] == 0), "–"),
FIT_B60=next((fmt(x["flat"]["below60"]) for x in out["bands"] if x["flat"]["short"] + x["flat"]["long"] + x["flat"]["limits"] == 0), "–"),
GLOBE_B75=fmt(out["bands"][0]["globe"]["below75"]), GLOBE_B60=fmt(out["bands"][0]["globe"]["below60"]),
ARC_R=fmt(round(s["arc"]["radius"])), ARC_BROKEN=fmt(s["arc"]["long"] + s["arc"]["short"] + s["arc"]["limits"]),
ARC_OFF=fmt(round(s["arc"]["off_median"])),
GLOBE_SHORT_LIST=", ".join(f"{names[a]}–{names[bb]}" for a, bb, x in sorted(s["globe_short"], key=lambda t: t[2])[:4]),
)
return v