""" world_ephemeris.py — as above, so below. The sky itself, as control signals. Computes real planetary positions with the Swiss Ephemeris (Moshier mode — NO data files, no downloads, good from 3000 BC to 3000 AD). Nothing is stored or fetched; the whole sky is generated on demand, like world_sky.py does for the sun. Astrology data is ideal for an instrument because it's inherently cyclic at wildly different rates — the Moon wheels monthly, the Sun yearly, Saturn every 29 years — so you get a bank of natural LFOs spanning seconds to a lifetime. Emits (all normalized-friendly): /gs/astro/moon moon phase, 0 (new) .. 1 (full) /gs/astro/moon_lon Moon ecliptic longitude 0..360 (fast wheel ~13deg/day) /gs/astro/sun_lon Sun longitude 0..360 (the seasons) /gs/astro/ ecliptic longitude 0..360 for mercury..saturn (slow LFOs) /gs/astro/tension closeness to HARD aspects (square/opposition) — dissonance /gs/astro/harmony closeness to SOFT aspects (trine/sextile) — consonance /gs/astro/retro fraction of planets retrograde 0..1 /gs/astro/mercury_retro 1 if Mercury is retrograde else 0 """ from __future__ import annotations import argparse import datetime import itertools import math import time import swisseph as swe from pythonosc.udp_client import SimpleUDPClient FLG = swe.FLG_MOSEPH | swe.FLG_SPEED BODIES = { "sun": swe.SUN, "moon": swe.MOON, "mercury": swe.MERCURY, "venus": swe.VENUS, "mars": swe.MARS, "jupiter": swe.JUPITER, "saturn": swe.SATURN, "uranus": swe.URANUS, "neptune": swe.NEPTUNE, "pluto": swe.PLUTO, } # which longitudes are worth emitting as sources (the rest still feed aspects) EMIT_LON = ["moon", "sun", "mercury", "venus", "mars", "jupiter", "saturn"] HARD = [(0, 8), (180, 8), (90, 7)] # conjunction/opposition/square + orbs SOFT = [(120, 7), (60, 5)] # trine/sextile def sky(jd): lon, speed = {}, {} for name, body in BODIES.items(): xx, _ = swe.calc_ut(jd, body, FLG) lon[name], speed[name] = xx[0], xx[3] return lon, speed def aspects(lon): hard = soft = 0.0 for a, b in itertools.combinations(lon, 2): d = abs(lon[a] - lon[b]) % 360 d = min(d, 360 - d) for ang, orb in HARD: if abs(d - ang) < orb: hard += 1 - abs(d - ang) / orb for ang, orb in SOFT: if abs(d - ang) < orb: soft += 1 - abs(d - ang) / orb return hard, soft 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("--interval", type=float, default=20.0) args = ap.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[ephemeris] emitting /gs/astro/* (Swiss Ephemeris, no files) " f"every {args.interval:g}s") while True: try: now = datetime.datetime.now(datetime.timezone.utc) jd = swe.julday(now.year, now.month, now.day, now.hour + now.minute / 60 + now.second / 3600) lon, speed = sky(jd) phase_angle = (lon["moon"] - lon["sun"]) % 360 illum = (1 - math.cos(math.radians(phase_angle))) / 2 client.send_message("/gs/astro/moon", float(illum)) for name in EMIT_LON: addr = "moon_lon" if name == "moon" else \ "sun_lon" if name == "sun" else name client.send_message(f"/gs/astro/{addr}", float(lon[name])) hard, soft = aspects(lon) client.send_message("/gs/astro/tension", float(hard)) client.send_message("/gs/astro/harmony", float(soft)) retro = [n for n, s in speed.items() if s < 0 and n != "moon"] client.send_message("/gs/astro/retro", len(retro) / len(BODIES)) client.send_message("/gs/astro/mercury_retro", 1.0 if speed["mercury"] < 0 else 0.0) except Exception as e: # noqa print(f"[ephemeris] warn: {e}") time.sleep(args.interval) if __name__ == "__main__": main()