"""Dziedzina wejścia funkcji liczącej domy — klasy równoważności i wartości brzegowe. Liczenie cuspów traktujemy jako FUNKCJĘ trzech parametrów: (RAMC, ε, φ). ε nie jest niezależne — wynika z daty, więc realnymi parametrami są (data, φ) plus RAMC, który zależy od czasu gwiazdowego i długości geograficznej. Zakres dat programu: 370 p.n.e. – 2270 n.e. (rok astronomiczny -369 … 2270). PUŁAPKA, o którą łatwo się potknąć: granica dziedziny dla Placidusa i Kocha — koło podbiegunowe — NIE jest stałą 66,56°. Zależy od ε, a ε zmienia się z datą. W zakresie programu przesuwa się o ~0,3°. Wartości brzegowe MUSZĄ więc być liczone per data, inaczej testy brzegowe celują obok granicy. """ from __future__ import annotations import random from dataclasses import dataclass # Zakres dat programu jako dni juliańskie (rok astronomiczny: 370 p.n.e. = -369). # Wyznacza go jądro efemeryd de421, którego używa silnik. Większe jądro (de441, # ~13 000 lat) jest DECYZJĄ ODŁOŻONĄ — świadomie „nice to have", nie „must": # rozszerza wyłącznie zakres dat, a nie poprawność czegokolwiek w obecnym # zakresie. Gdy kiedyś dojdzie, tu jest jedno miejsce do zmiany — i wtedy # wartości brzegowe (ε, koło podbiegunowe) trzeba przeliczyć dla nowych krańców. JD_MIN = 1_566_839.5 # -369-01-01 JD_MAX = 2_695_000.5 # ~2270-12-31 JD_J2000 = 2_451_545.0 # punkt odniesienia — środek dziedziny # Systemy oparte na łuku dobowym: tracą definicję za kołem podbiegunowym. DIURNAL_ARC_SYSTEMS = frozenset({"placidus", "koch"}) # Systemy, w których NASZA dziedzina jest WĘŻSZA niż wyroczni — świadomie, nie przez # rozjazd. Topocentric: swisseph zwraca powyżej koła podbiegunowego cuspy, które nie # dzielą okręgu (domy nachodzą na siebie), i sam sobie tam przeczy — swe_house_pos # nie zgadza się z jego własnymi cuspami. Wolimy odmówić niż podać liczbę bez # znaczenia, więc harness ma tego NIE zgłaszać jako niezgodności dziedziny. NARROWER_THAN_ORACLE = frozenset({"topocentric"}) # Jak blisko granicy stawiamy wartości brzegowe (po obu stronach). EPSILON_NUDGE = 0.01 @dataclass(frozen=True) class Case: """Pojedynczy zestaw danych wejściowych + skąd się wziął (do raportu).""" ramc: float lat: float jd: float label: str # "brzeg: równik" / "wnętrze" / "losowy" — grupowanie w raporcie def key(self) -> str: return f"ramc={self.ramc:.4f} lat={self.lat:.4f} jd={self.jd:.1f}" def obliquity(jd: float) -> float: """ε dla daty — z NASZEJ implementacji, żeby granice liczyć tak samo jak silnik.""" from app.engine.houses import mean_obliquity return mean_obliquity(jd) def polar_circle(jd: float) -> float: from app.engine.houses import polar_circle as pc return pc(obliquity(jd)) def in_domain(system: str, lat: float, jd: float) -> bool: """Czy zestaw należy do dziedziny danego systemu. Poza dziedziną są wyłącznie systemy łuku dobowego powyżej koła podbiegunowego (i same bieguny, gdzie degeneruje się cała geometria).""" if abs(lat) >= 90.0: return False if system in DIURNAL_ARC_SYSTEMS or system in NARROWER_THAN_ORACLE: return abs(lat) < polar_circle(jd) return True # ── wartości brzegowe ──────────────────────────────────────────────────── def _boundary_lats(jd: float) -> list[tuple[float, str]]: """Szerokości brzegowe DLA DANEJ DATY (koło podbiegunowe zależy od ε).""" pc = polar_circle(jd) out: list[tuple[float, str]] = [ (0.0, "brzeg: równik"), (pc - EPSILON_NUDGE, "brzeg: tuż POD kołem podbieg."), (pc + EPSILON_NUDGE, "brzeg: tuż ZA kołem podbieg."), (-(pc - EPSILON_NUDGE), "brzeg: tuż pod kołem (płd.)"), (-(pc + EPSILON_NUDGE), "brzeg: tuż za kołem (płd.)"), (89.9, "brzeg: prawie biegun N"), (-89.9, "brzeg: prawie biegun S"), ] return out def _boundary_ramcs() -> list[tuple[float, str]]: """RAMC na krańcach i w punktach kardynalnych — tam wzory mają osobliwości.""" return [ (0.0, "brzeg: RAMC 0°"), (90.0, "brzeg: RAMC 90°"), (180.0, "brzeg: RAMC 180°"), (270.0, "brzeg: RAMC 270°"), (359.99, "brzeg: RAMC ~360°"), ] def _boundary_jds() -> list[tuple[float, str]]: return [ (JD_MIN, "brzeg: początek zakresu dat"), (JD_J2000, "środek zakresu (J2000)"), (JD_MAX, "brzeg: koniec zakresu dat"), ] def boundary_cases() -> list[Case]: """Iloczyn wartości brzegowych — pełne pokrycie kombinacji granicznych.""" out: list[Case] = [] for jd, jl in _boundary_jds(): for lat, ll in _boundary_lats(jd): for ramc, rl in _boundary_ramcs(): out.append(Case(ramc, lat, jd, f"{ll} | {rl} | {jl}")) return out def interior_cases(count: int, seed: int = 0) -> list[Case]: """Tyle samo przypadków z WNĘTRZA klas równoważności, ile brzegowych. Deterministyczne przy danym ziarnie — ten sam build daje ten sam zestaw. """ rnd = random.Random(seed) out: list[Case] = [] for _ in range(count): jd = rnd.uniform(JD_MIN, JD_MAX) pc = polar_circle(jd) # wnętrze = z dala od granic, żeby nie dublować przypadków brzegowych lat = rnd.uniform(-pc + 1.0, pc - 1.0) out.append(Case(rnd.uniform(0.0, 360.0), lat, jd, "wnętrze")) return out def random_cases(count: int, seed: int) -> list[Case]: """Wielki losowy przemiał — CAŁA dziedzina, także za kołem podbiegunowym. Ziarno jest wymagane i raportowane: losowy test, którego nie da się odtworzyć, jest bezużyteczny, bo czerwień raz na tydzień nie daje się zdiagnozować. """ rnd = random.Random(seed) return [ Case(rnd.uniform(0.0, 360.0), rnd.uniform(-89.9, 89.9), rnd.uniform(JD_MIN, JD_MAX), "losowy") for _ in range(count) ]