Add fe-map and iss sources, The Shape of Distance as published, contribution guide

- femap/: planar best-effort solver, build, result and page (fe-map.aethercosmology.com)
- satellites/: The Sync Test build, ISS elements, SatNOGS timings and page (iss.aethercosmology.com)
- src/handoff/The_Shape_of_Distance_PRESENTATION.html: inputs for both
- CONTRIBUTING.md; README and LICENSE-DATA cover all four sites; GitHub mirror for issues and pull requests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Toby
2026-10-30 18:42:34 -04:00
parent 2bf0bdccb5
commit ad8b889437
27 changed files with 4474 additions and 43 deletions
+68
View File
@@ -0,0 +1,68 @@
"""SatNOGS facts quoted on the page, from the "Listening to the ISS" tab of
The Shape of Distance (src/handoff/The_Shape_of_Distance_PRESENTATION.html,
page 3; recordings of 3 October 2026 at 437.55 / 437.80 MHz).
python3 satnogs.py -> data/satnogs_20261003.json
"""
import base64, datetime as dt, json, os, re
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
PRES = os.path.join(HERE, "..", "src", "handoff", "The_Shape_of_Distance_PRESENTATION.html")
BURST_T = -10.0 # s from the Europe pass epoch: the instant compared across receivers
def page3():
s = open(PRES, encoding="utf-8").read()
blobs = re.findall(r"[A-Za-z0-9+/]{20000,}={0,2}", s)
html = base64.b64decode(blobs[3]).decode("utf-8")
assert "<title>Listening to the ISS</title>" in html
js = re.findall(r"<script[^>]*>(.*?)</script>", html, flags=re.S)[0]
return json.loads(js[js.index("{"): js.rindex("}") + 1])
def main():
D = page3()
out = {"source": "The Shape of Distance, Listening to the ISS (https://distance.aethercosmology.com/#doppler)", "passes": {}}
for key, p in D["passes"].items():
st = p["stations"]
dt0 = np.array([s["t0"] - s["pub"]["t_ca"] for s in st])
V = np.array([s["V"] for s in st]); E = np.array([s["V_err"] for s in st]); w = 1 / E**2
down = [s["t0"] for s in st]
out["passes"][key] = {
"utc": dt.datetime.fromtimestamp(p["t_epoch"], dt.timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
"stations": len(st),
"t0_rms_s": float(np.sqrt((dt0**2).mean())),
"v_kms": float((V * w).sum() / w.sum() / 1000),
# point beneath the station at each receiver's closest approach (published orbit)
"ca": [[s["pub"]["lat"], s["pub"]["lon"], s["t0"]] for s in st],
"offset_sd_median_hz": float(np.median([s["offset_sd"] for s in st])),
}
allV = np.array([s["V"] for p in D["passes"].values() for s in p["stations"]])
allE = np.array([s["V_err"] for p in D["passes"].values() for s in p["stations"]])
out["v_kms"] = float((allV / allE**2).sum() / (1 / allE**2).sum() / 1000)
# one instant of the Europe pass: carrier-corrected shift at every receiver with data within 3 s
p = D["passes"]["2026-10-03_Europe"]
shifts = []
for s in p["stations"]:
pts = np.array(s["pts"]); t = pts[:, 0]; i = np.searchsorted(t, BURST_T)
if 0 < i < len(t) and t[i] - t[i - 1] < 6:
shifts.append((s["name"], float(np.interp(BURST_T, t, pts[:, 1]) - (s["f"] - s["f_nom"])), s["lat"], s["lon"]))
v = np.array([x[1] for x in shifts])
lo, hi = min(shifts, key=lambda x: x[1]), max(shifts, key=lambda x: x[1])
out["burst"] = {
"utc": dt.datetime.fromtimestamp(p["t_epoch"] + BURST_T, dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
"receivers": len(shifts), "up": int((v > 0).sum()), "down": int((v < 0).sum()),
"min_hz": float(v.min()), "max_hz": float(v.max()), "min_name": lo[0], "max_name": hi[0],
"lat_range": [min(x[2] for x in shifts), max(x[2] for x in shifts)],
"lon_range": [min(x[3] for x in shifts), max(x[3] for x in shifts)],
}
with open(os.path.join(HERE, "data", "satnogs_20261003.json"), "w") as fh:
json.dump(out, fh, indent=1)
print(json.dumps({k: v for k, v in out.items() if k != "passes"}, indent=1))
for k, q in out["passes"].items():
print(k, q["utc"], q["stations"], "t0 rms %.2f s" % q["t0_rms_s"], "v %.3f" % q["v_kms"], "offset sd %.0f Hz" % q["offset_sd_median_hz"])
if __name__ == "__main__":
main()