Files
cable-latency-survey/femap/build.py
T

381 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The Best Flat Map: every measured length and limit laid out on one plane, then released into 3D.
Data (all published earlier on distance.aethercosmology.com and pingstats.aethercosmology.com):
flights The Measured Sphere, "each flight's own speed": 219 cities, 1,344 routes flown both ways.
Length = aircraft cruise speed x mean airborne time, less 15 min for climb and descent.
limits The Measured Sphere's cable, rail, road and satellite-ranging limits, every WonderNetwork
2020 round trip and every live round trip (RIPE Atlas, Globalping, Vultr) between servers
in these cities.
triangles The Triangle Test's 935 surveyed triangles (scored, not solved: a plane gives every
triangle exactly 180 degrees).
Coordinates are used to label cities, draw the start maps, match servers to cities and score the
globe for comparison. The solver (fit.py) sees only lengths and limits.
../.venv/bin/python build.py -> data/femap.json, site/index.html
"""
import base64, datetime as dt, importlib.util, json, math, os, pickle, re, sys, time
for _v in ("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(_v, "1")
from multiprocessing import Pool
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "src")
sys.path.insert(0, SRC)
_spec = importlib.util.spec_from_file_location("pingstats_build", os.path.join(SRC, "build.py"))
_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, 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
END_SLACK = 25.0 # km allowed at each end of a round trip
MATCH_KM = 80.0 # a server belongs to a city if it is this close to its airport
LO, HI = 0.25, 0.10 # default flight band: [0.75 L, 1.10 L]
BANDS = [(0.25, 0.10), (0.25, 0.25), (0.40, 0.10), (0.50, 0.10)]
N_RANDOM = 300
FAST = os.environ.get("FAST") == "1" # quick preview: one start, one 3D seed, one band -> data/preview.json, site/preview.html
SEED = 20261030
FA_PREFIX = "https://www.flightaware.com/live/flight/"
def pages():
s = open(PRES, encoding="utf-8").read()
out = []
for b in re.findall(r"[A-Za-z0-9+/]{20000,}={0,2}", s):
out.append(base64.b64decode(b).decode("utf-8", "ignore"))
return out
def page_const(html, name):
js = next(x for x in re.findall(r"<script[^>]*>(.*?)</script>", html, flags=re.S) if f"const {name}=" in x)
i = js.index(f"const {name}=") + len(name) + 7
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))
cc = np.sin(p0) * np.sin(p1) + np.cos(p0) * np.cos(p1) * np.cos(l1 - l0)
c = np.arccos(np.clip(cc, -1, 1)); k = np.where(c < 1e-9, 1.0, c / np.sin(np.maximum(c, 1e-9)))
x = R * k * np.cos(p1) * np.sin(l1 - l0)
y = R * k * (np.cos(p0) * np.sin(p1) - np.sin(p0) * np.cos(p1) * np.cos(l1 - l0))
return np.c_[x, y]
def plane_d(f, a, b):
x1, y1 = f(*a); x2, y2 = f(*b)
return math.hypot(x2 - x1, y2 - y1)
def shortest_on_any(a, b):
"""Shortest distance between two places on the globe or any of the three flat maps used elsewhere."""
return min(gc(*a, *b), plane_d(ae_xy, a, b), plane_d(dualpole_xy, a, b), plane_d(mercator_xy, a, b))
def load():
P = pages()
ms = page_const(P[6], "DATA")["own"]
tri = page_const(P[7], "FIN")["tri"]
cities = ms["cities"]
C = [(c[1], c[2]) for c in cities]
n = len(C)
fl = []
for f in ms["flights"]:
a, b, x = f[0], f[1], f[8]
ab, ba = x["ab"], x["ba"]
fl.append(dict(a=a, b=b, L=x["km"], airline=f[4], fa=f[5], fb=f[6], page=f[7],
ab=[ab["ident"], ab["ac"], ab["air"], ab["n"], round(ab["v"]), ab["src"]],
ba=[ba["ident"], ba["ac"], ba["air"], ba["n"], round(ba["v"]), ba["src"]]))
lim = {} # (a, b, kind) -> record; round trips keep the tightest per pair
def add(a, b, U, kind, note, label="", url=""):
if a == b:
return
a, b = min(a, b), max(a, b)
k = (a, b, kind)
if k not in lim or U < lim[k]["U"]:
lim[k] = dict(a=a, b=b, U=round(U, 1), kind=kind, note=note, label=label, url=url)
kinds = {"cable": "rt", "rail": "rail", "road": "road", "satellite": "sat"}
for l in ms["limits"]:
add(l[0], l[1], l[2], kinds[l[3]], l[4].replace("round trip ", ""), l[5], l[6] if len(l) > 6 else "")
def nearest(p):
d = [gc(*p, *q) for q in C]; i = int(np.argmin(d)); return i, d[i]
dropped = []
wn = json.load(open(os.path.join(SRC, "data", "wn_clean.json")))
srv = wn["servers"]; wm = {k: nearest((s["lat"], s["lon"])) for k, s in srv.items()}
n_wn = 0
# A round trip shorter than every surface allows means a server is not where it is listed. A server
# caught in two or more such pairs is treated as mislocated and all its pairs are left out.
reach_of = lambda p: KM_PER_MS * p["rtt"] + 2 * END_SLACK
bad = [p for p in wn["pairs"] if reach_of(p) < min(p["globe"], p["ae"], p["dualpole"], p["mercator"])]
hits = {}
for p in bad:
for s in (p["a"], p["b"]):
hits[s] = hits.get(s, 0) + 1
mislocated = {s for s, k in hits.items() if k >= 2}
for p in bad:
dropped.append(f'{srv[p["a"]]["name"]} - {srv[p["b"]]["name"]} ({p["rtt"]} ms)')
for p in wn["pairs"]:
a, b = wm[p["a"]], wm[p["b"]]
if a[1] > MATCH_KM or b[1] > MATCH_KM or p in bad or p["a"] in mislocated or p["b"] in mislocated:
continue
reach = reach_of(p)
n_wn += 1
add(a[0], b[0], reach + a[1] + b[1], "rt",
f'{p["rtt"]:.2f} ms, WonderNetwork 2020 archive ({p["n"]} samples)',
f'WonderNetwork servers {srv[p["a"]]["name"]} and {srv[p["b"]]["name"]}', "https://wondernetwork.com/pings")
lv = live(); sites = lv["sites"]; sm = [nearest((s[2], s[3])) for s in sites]
n_live = 0
for c in lv["cells"]:
a, b = sm[c[0]], sm[c[1]]
if a[1] > MATCH_KM or b[1] > MATCH_KM:
continue
sa, sb = sites[c[0]], sites[c[1]]
reach = KM_PER_MS * c[2] + 2 * END_SLACK
if reach < shortest_on_any((sa[2], sa[3]), (sb[2], sb[3])):
dropped.append(f"{sa[0]} - {sb[0]} ({c[2]} ms)")
continue
n_live += 1
add(a[0], b[0], reach + a[1] + b[1], "rt", f"{c[2]:.1f} ms, live measurement 2026 ({c[5]})",
f"{sa[0]} probe to {sb[0]} anchor", "https://pingstats.aethercosmology.com/")
lim = sorted(lim.values(), key=lambda r: (r["kind"], r["a"], r["b"]))
tri_fail = dict(n=len(tri), flat=sum(abs(t[3]) > 4 for t in tri), globe=sum(abs(t[2]) > 4 for t in tri))
return dict(cities=cities, C=np.array(C), fl=fl, lim=lim, tri=tri_fail, dropped=dropped, n_wn=n_wn, n_live=n_live,
mislocated=sorted(f'{srv[s]["name"]} ({srv[s]["country"]})' for s in mislocated))
def mds_start(prob, C):
"""Classical multidimensional scaling of the shortest flight-path lengths, turned and mirrored
(no stretching) to sit as close as possible to the North-pole AE layout for drawing."""
n = prob.n; D = np.full((n, n), np.inf); np.fill_diagonal(D, 0)
for a, b, L in zip(prob.fa, prob.fb, prob.fl):
D[a, b] = D[b, a] = min(D[a, b], L)
for k in range(n):
D = np.minimum(D, D[:, k:k + 1] + D[k:k + 1, :])
J = np.eye(n) - 1 / n; B = -0.5 * J @ (D ** 2) @ J
w, V = np.linalg.eigh(B); X = V[:, -2:] * np.sqrt(np.maximum(w[-2:], 0))
T = aeq(C[:, 0], C[:, 1], 90, 0)
Xc, Tc = X - X.mean(0), T - T.mean(0)
U, _, Vt = np.linalg.svd(Xc.T @ Tc)
return Xc @ (U @ Vt) + T.mean(0)
_PROB = None
def _init(prob):
global _PROB
_PROB = prob
def _run(job):
label, centre, X0, lo, hi = job[:5]
k, X = _PROB.best_effort(X0, lo, hi, **(job[5] if len(job) > 5 else {}))
r = _PROB.dists(X)[0] / _PROB.fl
return dict(label=label, centre=centre, broken=k, meanlog=float(np.abs(np.log(r)).mean()), X=X)
def counts(prob, X, lo, hi):
dd, ld = prob.dists(X); r = dd / prob.fl
return dict(short=int((r < 1 - lo).sum()), long=int((r > 1 + hi).sum()), limits=int((ld > prob.lu * 1.0005).sum()))
def main():
t0 = time.time()
D = load(); C = D["C"]; fl, lim = D["fl"], D["lim"]
prob = Problem(len(C), [f["a"] for f in fl], [f["b"] for f in fl], [f["L"] for f in fl],
[l["a"] for l in lim], [l["b"] for l in lim], [l["U"] for l in lim])
GF = np.array([gc(*C[f["a"]], *C[f["b"]]) for f in fl]); GL = np.array([gc(*C[l["a"]], *C[l["b"]]) for l in lim])
print(f"{len(C)} cities, {len(fl)} flights, {len(lim)} limits ({D['n_wn']} WonderNetwork pairs, {D['n_live']} live cells, "
f"{len(D['dropped'])} round trips dropped as mislocated)", flush=True)
starts = {"ae": ("North-pole AE", [90, 0], aeq(C[:, 0], C[:, 1], 90, 0)),
"dp": ("Dual-pole AE", [0, 0], aeq(C[:, 0], C[:, 1], 0, 0)),
"mds": ("Flight times only", [90, 0], mds_start(prob, C))}
rng = np.random.default_rng(SEED)
jobs = [(v[0], v[1], v[2], LO, HI) for v in starts.values()]
named = [(0, 90), (0, -90), (0, 180), (-90, 0), (45, 100), (60, -30), (30, 60), (-30, 150)]
rand = [(round(math.degrees(math.asin(rng.uniform(-1, 1))), 1), round(rng.uniform(-180, 180), 1)) for _ in range(N_RANDOM)]
jobs += [(f"AE centred on {la}, {lo}", [la, lo], aeq(C[:, 0], C[:, 1], la, lo), LO, HI) for la, lo in named + rand]
if FAST:
jobs = jobs[:1]
cache = os.path.join(HERE, "data", "search_fast.pkl" if FAST else "search.pkl")
sig = (len(C), len(fl), len(lim), LO, HI, len(jobs))
pool = Pool(7, initializer=_init, initargs=(prob,))
if os.path.exists(cache) and pickle.load(open(cache, "rb"))["sig"] == sig:
res = pickle.load(open(cache, "rb"))["res"]
print("search: cached", flush=True)
else:
res = pool.map(_run, jobs, chunksize=1)
pickle.dump(dict(sig=sig, res=res), open(cache, "wb"))
res.sort(key=lambda r: (r["broken"], r["meanlog"]))
# Refine the 20 best layouts: twelve rounds, shorter solves, two reweighting floors.
if not FAST:
ref = pool.map(_run, [(r["label"], r["centre"], r["X"], LO, HI, dict(rounds=12, maxiter=3000, eps=e))
for r in res[:20] for e in (0.02, 0.05)], chunksize=1)
for r in ref:
r["refined"] = True
print("refined:", sorted(r["broken"] for r in ref)[:10], flush=True)
res = sorted(res + ref, key=lambda r: (r["broken"], r["meanlog"]))
best = res[0]
print(f"search: {len(res)} starts in {time.time() - t0:.0f}s; best {best['broken']} broken from {best['label']}; "
f"worst start {res[-1]['broken']}", flush=True)
for r in res[:5]:
print(" ", r["broken"], round(r["meanlog"], 4), r["label"])
# Release the best flat layout into free 3D space: same data, same solver, the plane rule removed.
# In the same pool: the band table (each band solved from the best layout and from North-pole AE).
seeds = 1 if FAST else 6
rel = [("3D", None, np.c_[best["X"], np.random.default_rng(SEED + s).normal(0, 50, len(C))], LO, HI) for s in range(seeds)]
seeds_b = [r["X"] for r in res[:5]] + [starts["ae"][2]]
NB = len(seeds_b)
bjobs = [(f"band {lo} {hi}", None, X0, lo, hi) for lo, hi in (BANDS[:1] if FAST else BANDS) for X0 in seeds_b]
out_jobs = pool.map(_run, rel + bjobs, chunksize=1)
pool.close()
X3s = out_jobs[:seeds]
b3 = min(X3s, key=lambda t: t["broken"]); k3, X3 = b3["broken"], b3["X"]
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)]
cand = cand + [dict(broken=prob.broken(best["X"], lo, hi), X=best["X"])]
X = min(cand, key=lambda t: t["broken"])["X"]; r = prob.dists(X)[0] / prob.fl; gr = GF / prob.fl
bands.append(dict(lo=lo, hi=hi, flat=dict(**counts(prob, X, lo, hi), below75=int((r < 0.75).sum()), below60=int((r < 0.6).sum())),
globe=dict(short=int((gr < 1 - lo).sum()), long=int((gr > 1 + hi).sum()), limits=int((GL > prob.lu * 1.0005).sum()),
below75=int((gr < 0.75).sum()), below60=int((gr < 0.6).sum())),
flat_long=[[int(prob.fa[i]), int(prob.fb[i]), round(float(r[i]), 3)] for i in np.where(r > 1 + hi)[0]]))
print("band", lo, hi, bands[-1]["flat"], bands[-1]["globe"], flush=True)
r = prob.dists(best["X"])[0] / prob.fl; gr = GF / prob.fl
stats = dict(
n_cities=len(C), n_flights=len(fl), n_limits=len(lim),
n_kind={k: sum(l["kind"] == k for l in lim) for k in ("rt", "rail", "road", "sat")},
n_wn=D["n_wn"], n_live=D["n_live"], dropped=D["dropped"], mislocated=D["mislocated"], n_starts=len(jobs),
best=dict(label=best["label"], centre=best["centre"], broken=best["broken"], **counts(prob, best["X"], LO, HI),
below80=int((r < 0.8).sum()), below85=int((r < 0.85).sum()), meanlog=best["meanlog"]),
globe=dict(**{k: int(v) for k, v in dict(short=(gr < 1 - LO).sum(), long=(gr > 1 + HI).sum(),
limits=(GL > prob.lu * 1.0005).sum()).items()},
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), 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,
built=dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M UTC"))
def strip(u):
return u[len(FA_PREFIX):] if u.startswith(FA_PREFIX) else u
out = dict(
cities=D["cities"],
fl=[[f["a"], f["b"], round(f["L"], 1), f["airline"], f["fa"], f["fb"], f["page"],
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(), 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()),
bands=bands, stats=stats, fa_prefix=FA_PREFIX)
os.makedirs(os.path.join(HERE, "data"), exist_ok=True)
with open(os.path.join(HERE, "data", "preview.json" if FAST else "femap.json"), "w") as fh:
json.dump(out, fh, separators=(",", ":"))
print(json.dumps(stats, indent=1)[:4000])
render(out)
print(f"done in {time.time() - t0:.0f}s")
def render(out):
tpl = os.path.join(HERE, "template.html")
if not os.path.exists(tpl):
return
html = open(tpl, encoding="utf-8").read()
s = out["stats"]
vals = copy_values(out)
for k, v in vals.items():
html = html.replace("{{" + k + "}}", v)
left = sorted(set(re.findall(r"\{\{(\w+)\}\}", html)))
assert not left, left
html = html.replace("/*DATA*/null", json.dumps(out, separators=(",", ":")))
os.makedirs(os.path.join(HERE, "site"), exist_ok=True)
name = "preview.html" if FAST else "index.html"
open(os.path.join(HERE, "site", name), "w", encoding="utf-8").write(html)
print("site/" + name, round(len(html) / 1e6, 2), "MB")
def fmt(n):
return f"{n:,}"
def copy_values(out):
s = out["stats"]; b, g, t3 = s["best"], s["globe"], s["three"]
names = [c[0] for c in out["cities"]]
long_ = sorted(out["bands"][0]["flat_long"], key=lambda x: -x[2])
v = dict(
N_CITIES=fmt(s["n_cities"]), N_FLIGHTS=fmt(s["n_flights"]), N_LIMITS=fmt(s["n_limits"]),
N_RT=fmt(s["n_kind"]["rt"]), N_RAIL=fmt(s["n_kind"]["rail"] + s["n_kind"]["road"]), N_SAT=fmt(s["n_kind"]["sat"]),
N_STARTS=fmt(s["n_starts"]), N_TRI=fmt(s["tri"]["n"]), TRI_FLAT=fmt(s["tri"]["flat"]), TRI_GLOBE=fmt(s["tri"]["globe"]),
BEST_LONG=fmt(b["long"]), BEST_SHORT=fmt(b["short"]), BEST_LIM=fmt(b["limits"]), BEST_BROKEN=fmt(b["broken"]),
BEST_B80=fmt(b["below80"]), GLOBE_B80=fmt(g["below80"]), GLOBE_LONG=fmt(g["long"]), GLOBE_SHORT=fmt(g["short"]),
GLOBE_LIM=fmt(g["limits"]),
WORST_PAIR=f"{names[long_[0][0]]}–{names[long_[0][1]]}" if long_ else "–",
WORST_X=f"{long_[0][2]:.2f}" if long_ else "–",
LONG_LIST=", ".join(f"{names[a]}–{names[bb]} ×{x:.2f}" for a, bb, x in long_[:8]),
R3=fmt(round(t3["radius"])), WRAP3=f"{t3['wrap']:.0f}", OFF3=f"{t3['offset'] * 100:.1f}", BROKEN3=fmt(t3["broken"]),
LO_PCT=f"{round((1 - s['lo']) * 100)}", HI_PCT=f"{round((1 + s['hi']) * 100)}",
BUILT=s["built"], N_DROPPED=fmt(len(s["dropped"])), N_STARTS_AE=fmt(s["n_starts"] - 3), KM_PER_MS=f"{s['km_per_ms']:.1f}",
FIT_LO=next((f"{round((1 - x['lo']) * 100)}" for x in out["bands"] if x["flat"]["short"] + x["flat"]["long"] + x["flat"]["limits"] == 0), "–"),
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
if __name__ == "__main__":
main()