""" world_almanac.py — the planting calendar. Sowing by the moon. For millennia people planted by an almanac: the moon's phase and the zodiac sign it's passing through told you what to sow. It's not fetched data — it's computed from the same sky world_ephemeris uses. The zodiac's four elements cycle in a fixed order (fire, earth, air, water), and biodynamic tradition maps them to a crop type — a slow four-step sequencer the moon walks through, changing every ~2.25 days: earth signs -> ROOT days water signs -> LEAF days air signs -> FLOWER days fire signs -> FRUIT/SEED days Musically it's a modal calendar: the instrument's character can follow the moon through the garden. Emits: /gs/almanac/element 0..1 stepped — the day's element (root/leaf/flower/fruit) /gs/almanac/waxing 1 waxing (sow above-ground), 0 waning (sow roots) /gs/almanac/dec 0..1 — Moon's declination (ascending/descending, ~27.3d) /gs/almanac/fertile 0..1 — a simple biodynamic favourability score """ from __future__ import annotations import argparse import datetime import math import time import swisseph as swe from pythonosc.udp_client import SimpleUDPClient FLG = swe.FLG_MOSEPH | swe.FLG_SPEED # element index by sign (Aries..Pisces): fire,earth,air,water repeating. # biodynamic crop: 0 fire=fruit, 1 earth=root, 2 air=flower, 3 water=leaf ELEMENTS = ["fruit", "root", "flower", "leaf"] 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=30.0) args = ap.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[almanac] emitting /gs/almanac/* (root/leaf/flower/fruit by the moon) " f"every {args.interval:g}s") last_label = None 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) moon, _ = swe.calc_ut(jd, swe.MOON, FLG) sun, _ = swe.calc_ut(jd, swe.SUN, FLG) eq, _ = swe.calc_ut(jd, swe.MOON, swe.FLG_MOSEPH | swe.FLG_EQUATORIAL | swe.FLG_SPEED) sign = int(moon[0] // 30) % 12 elem = sign % 4 # 0 fire,1 earth,2 air,3 water phase_angle = (moon[0] - sun[0]) % 360 waxing = 1.0 if phase_angle < 180 else 0.0 illum = (1 - math.cos(math.radians(phase_angle))) / 2 dec = eq[1] # Moon declination (deg) dec01 = max(0.0, min(1.0, (dec + 28.5) / 57.0)) ascending = 1.0 if eq[4] > 0 else 0.0 fertile = 0.45 * illum + 0.35 * ascending + 0.20 * (1.0 if elem in (1, 3) else 0.0) client.send_message("/gs/almanac/element", elem / 3.0) client.send_message("/gs/almanac/waxing", waxing) client.send_message("/gs/almanac/dec", dec01) client.send_message("/gs/almanac/fertile", fertile) label = ELEMENTS[elem] if label != last_label: print(f" 🌱 {label} day (moon in sign {sign + 1}, " f"{'waxing' if waxing else 'waning'}, " f"{'ascending' if ascending else 'descending'})") last_label = label except Exception as e: # noqa print(f"[almanac] warn: {e}") time.sleep(args.interval) if __name__ == "__main__": main()