Godstrument/workers/world_sky.py
jing d2c7938321 👁 god's eye: GODSIGH's cosmology as a Spirit on the shelf
A new map-layout skin — the world's own sensor picture read AS the cosmology,
ported from the GODSIGH view. A minimal equirectangular Earth (a sparse hand-drawn
continent set kept inline — no heavy geo data in the one-file console), a 30°
graticule refit to a 2:1 rect each frame (dynamicStations), and nine geographic
"posts" the sources are seated at by godseyeOf. The overlay seats every feed at its
LITERAL coordinates: earthquakes flash at quake.lat/lon (venue-antipode fallback if
absent), the ISS and recon birds sweep as a moving point + trail from sats.iss_lat/
lon, aircraft are a shimmer band, the markets pulse at NY/London/Tokyo, the solar
wind washes the poles as an aurora, the word sparks at civilisational coordinates,
and your own hands are the ring at the venue. Cold watch-floor palette (near-black,
cyan, warning amber) and a Bb minor-pentatonic retune. Registered in SKINS +
SKIN_HOME beside the bagua; grimoire + manual entry ("the eye that hears").

To give the console the venue's location (it had none), world_sky now emits
/gs/sky/lat,lon; config.json declares sky.lat/lon and quake.lat/lon so the eye can
place "here" and the quakes. (sats.* signals + routes already landed with world_sats.)

Verified: full viz parses/runs with the groove self-check passing and zero console
errors; an isolated harness renders the REAL overlay code into a coherent world-map
with every layer; and an end-to-end run (feeds -> hub -> WS) delivers all the eye's
input signals. Adversarial review found one wrap bug — the ISS trail streaking
across the map at the ±180° antimeridian — now fixed by breaking the polyline at the
seam (verified: a seam-crossing sine trail renders split, no full-width streak).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:18:18 +10:00

91 lines
3.7 KiB
Python

"""
world_sky.py — the local sky over the venue, as a slow LFO.
Computes the sun's elevation above the horizon at a given latitude/longitude and
the current instant, using the NOAA solar-position algorithm. No network, no API
key, never rate-limited — just astronomy. This is the most place-specific signal
in the whole instrument: playing Brisbane at 2am sounds nothing like London at
sunset, because the sun is literally somewhere else in the sky.
Emits:
/gs/sky/elev solar elevation in degrees (-90 night .. +90 overhead)
/gs/sky/day 0..1 smooth day/night (0 deep night, 1 broad daylight)
/gs/sky/az solar azimuth in degrees (0=N, 90=E, 180=S, 270=W)
"""
from __future__ import annotations
import argparse
import datetime
import math
import time
from pythonosc.udp_client import SimpleUDPClient
def solar_position(lat: float, lon: float, when: datetime.datetime):
"""Return (elevation_deg, azimuth_deg) for lat/lon at UTC time `when`."""
n = when.timetuple().tm_yday
hour = when.hour + when.minute / 60 + when.second / 3600
g = 2 * math.pi / 365 * (n - 1 + (hour - 12) / 24) # fractional year
eqtime = 229.18 * (0.000075 + 0.001868 * math.cos(g)
- 0.032077 * math.sin(g) - 0.014615 * math.cos(2 * g)
- 0.040849 * math.sin(2 * g)) # minutes
decl = (0.006918 - 0.399912 * math.cos(g) + 0.070257 * math.sin(g)
- 0.006758 * math.cos(2 * g) + 0.000907 * math.sin(2 * g)
- 0.002697 * math.cos(3 * g) + 0.00148 * math.sin(3 * g)) # radians
time_offset = eqtime + 4 * lon # minutes (E +)
tst = hour * 60 + time_offset # true solar time
ha = math.radians(tst / 4 - 180) # hour angle
lat_r = math.radians(lat)
cos_zen = (math.sin(lat_r) * math.sin(decl)
+ math.cos(lat_r) * math.cos(decl) * math.cos(ha))
cos_zen = max(-1.0, min(1.0, cos_zen))
zen = math.acos(cos_zen)
elev = 90 - math.degrees(zen)
# azimuth
sin_az = -math.sin(ha) * math.cos(decl)
cos_az = (math.sin(decl) - math.sin(lat_r) * math.cos(zen)) / \
(math.cos(lat_r) * math.sin(zen) + 1e-9)
az = (math.degrees(math.atan2(sin_az, cos_az)) + 360) % 360
return elev, az
def day_fraction(elev: float) -> float:
"""Smooth 0..1 across the twilight band (civil dusk .. full day)."""
return max(0.0, min(1.0, (elev + 6.0) / 18.0))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--lat", type=float, default=-27.47, help="latitude (default Brisbane)")
ap.add_argument("--lon", type=float, default=153.02, help="longitude (default Brisbane)")
ap.add_argument("--interval", type=float, default=10.0)
args = ap.parse_args()
client = SimpleUDPClient(args.host, args.port)
print(f"[sky] emitting /gs/sky/elev|day|az for ({args.lat},{args.lon}) "
f"every {args.interval:g}s")
while True:
try:
elev, az = solar_position(args.lat, args.lon,
datetime.datetime.now(datetime.timezone.utc))
client.send_message("/gs/sky/elev", float(elev))
client.send_message("/gs/sky/day", day_fraction(elev))
client.send_message("/gs/sky/az", float(az))
client.send_message("/gs/sky/lat", float(args.lat)) # the venue's own coordinates,
client.send_message("/gs/sky/lon", float(args.lon)) # so the console can place "here" on a map
except Exception as e: # noqa
print(f"[sky] warn: {e}")
time.sleep(args.interval)
if __name__ == "__main__":
main()