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:
+38
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user