Files
python-tdd/pyswiss/apps/charts/tests/unit/test_windows.py
Disco DeDisco 32db203543 Set the Game Clock — ephemeris narrowing (hard CSP): placements shrink the REAL date window; narrowed range prints below the prompt; unreachable signs dim + reject — TDD
- PySwiss gains its FIRST reverse lookup: GET /api/windows/?placements=
  Uranus:Aquarius,…&next=Saturn — sign_windows (per-planet stride scan +
  bisection edge refine to the hour) folds thru narrowed_windows
  (slowest planet first, each scan restricted to the prior intersection
  so the fast bodies only ever scan slivers) over the game window
  (settings GAME_WINDOW_START/END, default 1781-03-13 — Uranus's
  discovery — → 2100-12-31, the snapshot span); present_signs reports
  the next planet's reachable signs. Self-validating UTs (every window
  forward-checked at midpoint + edges) + 8 API ITs
- epic clock_windows endpoint (lazy, table_sky-shaped — room views stay
  HTTP-free) proxies the lookup, cached per room+placements (six felts
  polling one ritual state = ONE upstream call; failures cached 60s);
  fails OPEN {available:false} when PySwiss is unreachable
- place_clock_planet enforces the HARD constraint: a sign outside the
  narrowed windows' reach → 409 sign_unreachable; fail-open w.o PySwiss
  (the ritual never bricks on microservice downtime); PlaceClockPlanet
  ITs sever PySwiss in setUp so the turn walk stays deterministic
  against a live local service
- felt: #id_clock_windows readout below the prompt for ALL viewers —
  "1995-04-01 → 1998-04-17 · 2 windows" — fetched at parse + after own
  placement + on every clock_placement broadcast; drawRim opts gain
  allowedSigns → unreachable wedges .nw-sign--blocked (dimmed, inert,
  no handlers); SkyWheelSpec R10/R11
- SeedMapClockNarrowingTest FT stubs PySwiss in-process (real proxy,
  real gating): readout renders, blocked Aries won't place, allowed
  Pisces lands Saturn, readout re-narrows

[[project-voronoi-spec]]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:30:16 -04:00

114 lines
4.6 KiB
Python

"""
Unit tests for the reverse ephemeris lookup — sign_windows / narrowed_windows /
present_signs (Set the Game Clock narrowing, EarthmanRPG roadmap step 21).
Unlike test_calc.py these DO drive the Swiss Ephemeris (Moshier fallback when
data files are absent — 0.1° precision, far inside the ±1-day tolerances
here). The assertions are largely SELF-VALIDATING: rather than hardcoding
astrological history, each returned window is forward-checked — its midpoint
must actually carry the planet in the claimed sign, and its edges must be true
sign boundaries.
Run:
pyswiss/.venv/Scripts/python pyswiss/manage.py test apps.charts
"""
from datetime import datetime, timezone
from django.test import SimpleTestCase
import swisseph as swe
from apps.charts.calc import (
PLANET_CODES,
get_julian_day,
get_sign,
narrowed_windows,
present_signs,
set_ephe_path,
sign_windows,
)
def _jd(iso):
return get_julian_day(datetime.fromisoformat(iso).replace(tzinfo=timezone.utc))
def _sign_at(jd, planet):
pos, _ = swe.calc_ut(jd, PLANET_CODES[planet], swe.FLG_SWIEPH)
return get_sign(pos[0])
class SignWindowsTest(SimpleTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
set_ephe_path()
def test_sun_in_aries_2020(self):
wins = sign_windows('Sun', 'Aries', [(_jd('2020-01-01'), _jd('2020-12-31'))])
self.assertEqual(len(wins), 1)
a, b = wins[0]
# Ingress at the March equinox, egress ~30 days later.
self.assertAlmostEqual(a, _jd('2020-03-20'), delta=1.0)
self.assertAlmostEqual(b, _jd('2020-04-19'), delta=1.0)
def test_window_edges_are_true_sign_boundaries(self):
lo, hi = _jd('1990-01-01'), _jd('2000-01-01')
wins = sign_windows('Saturn', 'Pisces', [(lo, hi)])
# Saturn's mid-90s Pisces residence, split by the retrograde dip back
# into Aquarius — at least one window, every one self-consistent.
self.assertGreaterEqual(len(wins), 1)
for a, b in wins:
self.assertLess(a, b)
self.assertEqual(_sign_at((a + b) / 2, 'Saturn'), 'Pisces')
if a > lo + 1: # true boundary (not clipped by the scan bound)
self.assertNotEqual(_sign_at(a - 0.2, 'Saturn'), 'Pisces')
if b < hi - 1:
self.assertNotEqual(_sign_at(b + 0.2, 'Saturn'), 'Pisces')
def test_edges_refined_to_the_hour(self):
wins = sign_windows('Sun', 'Aries', [(_jd('2020-01-01'), _jd('2020-12-31'))])
a, b = wins[0]
# Within 1/24 day of the crossing on each side.
self.assertEqual(_sign_at(a + 1 / 24, 'Sun'), 'Aries')
self.assertNotEqual(_sign_at(a - 1 / 24, 'Sun'), 'Aries')
self.assertEqual(_sign_at(b - 1 / 24, 'Sun'), 'Aries')
self.assertNotEqual(_sign_at(b + 1 / 24, 'Sun'), 'Aries')
def test_narrowed_windows_intersect_all_placements(self):
base = [(_jd('1990-01-01'), _jd('2000-01-01'))]
wins = narrowed_windows({'Uranus': 'Aquarius', 'Saturn': 'Pisces'}, base)
self.assertGreaterEqual(len(wins), 1)
for a, b in wins:
mid = (a + b) / 2
self.assertEqual(_sign_at(mid, 'Uranus'), 'Aquarius')
self.assertEqual(_sign_at(mid, 'Saturn'), 'Pisces')
total = sum(b - a for a, b in wins)
# Vastly narrower than the 10-year base, but a real window.
self.assertGreater(total, 30)
self.assertLess(total, 500)
def test_narrowed_windows_empty_when_unreachable(self):
# Saturn resided in Leo in the late 70s and mid 00s — never in the 90s.
wins = narrowed_windows(
{'Saturn': 'Leo'}, [(_jd('1990-01-01'), _jd('2000-01-01'))])
self.assertEqual(wins, [])
def test_no_placements_keeps_the_base_window(self):
base = [(_jd('1990-01-01'), _jd('2000-01-01'))]
self.assertEqual(narrowed_windows({}, base), base)
def test_present_signs_lists_reachable_signs_only(self):
signs = present_signs(
'Saturn', [(_jd('1994-06-01'), _jd('1996-06-01'))])
# Pisces until the 1996 Aries ingress — exactly two reachable signs.
self.assertEqual(signs, {'Pisces', 'Aries'})
def test_moon_windows_inside_a_narrow_window(self):
# The end-game shape: by the Moon's turn the windows are days wide.
wins = sign_windows('Moon', 'Cancer', [(_jd('2020-06-01'), _jd('2020-07-01'))])
self.assertEqual(len(wins), 1)
a, b = wins[0]
self.assertAlmostEqual(b - a, 2.4, delta=0.5) # ~2.4-day residence
self.assertEqual(_sign_at((a + b) / 2, 'Moon'), 'Cancer')