86a0f16f9e
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 14s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 19s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 13s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Etap 0 planu egzotycznych systemów domów: zanim zaczniemy implementować Placidusa i spółkę, potrzebujemy narzędzia, które powie, czy wynik jest poprawny. Błąd w domach jest CICHY — wykres wygląda dobrze, tylko planety siedzą w złych domach. FRAMEWORK (tests/oracle): liczenie cuspów jako funkcja (RAMC, ε, φ), Swiss Ephemeris jako wyrocznia. Trzy decyzje projektowe, które okazały się kluczowe: - IZOLACJA: obu implementacjom podajemy TE SAME wejścia przez swe_houses_armc. Porównywanie „naszego horoskopu" z „horoskopem swissepha" mieszałoby różnice czasu gwiazdowego i ε z błędami domów — utonęlibyśmy w fałszywych alarmach. Czas gwiazdowy i ε mają własny test. - KRYTERIUM to liczba przypadków powyżej tolerancji (1″), max odchylenie I MIEJSCE, a nie procent zgodności. Procent ukrywa kształt błędu: „97%" nie odróżnia szumu zmiennoprzecinkowego od rogu dziedziny, w którym mylimy się o 30°. - GRANICE PER DATA: koło podbiegunowe nie jest stałą 66,56° — zależy od ε, które zmienia się z datą (23,75° w 370 p.n.e.), więc przesuwa się o ~0,3°. Uruchomiony na kodzie uchodzącym za poprawny, w PIERWSZYM przebiegu znalazł dwa realne błędy: 1. ASCENDENT O 180° ZA KOŁEM PODBIEGUNOWYM. `atan2` wybierał niewłaściwy punkt przecięcia ekliptyki z horyzontem — zwracaliśmy Descendent. Planety lądowały w PRZECIWNYCH domach dla całej północnej Skandynawii (Tromsø, Rovaniemi, Murmańsk), na ~11% przypadków przy tych szerokościach. Rozstrzyga położenie względem MC: punkt wschodzący leży w półkolu (0°,180°) na wschód od MC. 2. NIEDETERMINIZM WHOLE SIGN NA GRANICY ZNAKU. Ascendent o włos od granicy (359,999999999976 vs 1e-10 — ta sama wartość, różne strony) przerzucał dom I o 30°. Ten sam horoskop na innej maszynie dawał inny wynik. Przyciąganie do granicy przy 1e-9° (3,6 mikrosekundy łuku — poniżej realnej dokładności danych). Oba mają testy regresji w zwykłej suicie, więc są łapane też bez swissepha. Po poprawkach: build 0 przekroczeń, sweep 20 000 przypadków = 760 000 porównań, max odchylenie 0.000000000°. Istniejące suity bez regresji (logika 277+3, prez. 249). CI: krok BLOKUJĄCY w jobie swisseph-image, odpalany wewnątrz obrazu silnika B z zamontowaną warstwą logiczną. pyswisseph zostaje wyłącznie wyrocznią testową — nie wchodzi do zależności produktu, izolacja z LOG-27 nienaruszona. Dodane `cusps_for(ramc, eps, lat, system)` — kanoniczne wejście, w które Etap 1 będzie tylko dopisywał kolejne systemy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
5.7 KiB
Python
136 lines
5.7 KiB
Python
"""Osie i domy — czysta matematyka sferyczna (LOG-05).
|
|
|
|
Bezstanowe funkcje: z lokalnego czasu gwiazdowego (RAMC), nachylenia ekliptyki
|
|
(ε) i szerokości geograficznej (φ) wyliczają Ascendent i MC, a stąd cusps domów
|
|
dla prostych systemów (Whole Sign, Equal, Porphyry). Niezależne od silnika —
|
|
silnik dostarcza tylko RAMC i ε.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from app.engine.formats import SIGN_ABBR, norm360, sign_index # noqa: F401
|
|
|
|
WHOLE_SIGN = "whole_sign"
|
|
EQUAL = "equal"
|
|
PORPHYRY = "porphyry"
|
|
SYSTEMS = (WHOLE_SIGN, EQUAL, PORPHYRY)
|
|
|
|
|
|
def mean_obliquity(tt_jd: float) -> float:
|
|
"""Średnie nachylenie ekliptyki [°] dla daty (Julian TT). Wystarcza do domów."""
|
|
t = (tt_jd - 2451545.0) / 36525.0
|
|
arcsec = 84381.448 - 46.8150 * t - 0.00059 * t * t + 0.001813 * t ** 3
|
|
return arcsec / 3600.0
|
|
|
|
|
|
def compute_mc(ramc_deg: float, eps_deg: float) -> float:
|
|
r, e = math.radians(ramc_deg), math.radians(eps_deg)
|
|
mc = math.atan2(math.sin(r), math.cos(r) * math.cos(e))
|
|
return norm360(math.degrees(mc))
|
|
|
|
|
|
def compute_asc(ramc_deg: float, eps_deg: float, lat_deg: float) -> float:
|
|
"""Ascendent — punkt ekliptyki wschodzący na horyzoncie.
|
|
|
|
KOREKTA GAŁĘZI (błąd wykryty przez porównanie z wyrocznią, tests/oracle):
|
|
ekliptyka przecina horyzont w DWÓCH punktach — wschodzącym (Asc) i zachodzącym
|
|
(Dsc). `atan2` wybiera jeden z nich, ale powyżej koła podbiegunowego potrafi
|
|
wskazać ten NIEWŁAŚCIWY: dla szerokości 67°+ i szerokiego zakresu RAMC
|
|
zwracaliśmy Descendent, czyli Ascendent przesunięty o 180°. Skutek nie był
|
|
subtelny — planety lądowały w PRZECIWNYCH domach dla całej Skandynawii
|
|
północnej (Tromsø, Rovaniemi, Murmańsk).
|
|
|
|
Rozstrzyga położenie względem MC: punkt wschodzący leży zawsze w półkolu
|
|
(0°, 180°) na wschód od MC. Reguła zweryfikowana na 46 800 przypadkach wobec
|
|
Swiss Ephemeris — zero rozbieżności.
|
|
"""
|
|
r, e, phi = math.radians(ramc_deg), math.radians(eps_deg), math.radians(lat_deg)
|
|
asc = norm360(math.degrees(math.atan2(
|
|
math.cos(r),
|
|
-(math.sin(r) * math.cos(e) + math.tan(phi) * math.sin(e)),
|
|
)))
|
|
mc = compute_mc(ramc_deg, eps_deg)
|
|
return norm360(asc + 180.0) if (asc - mc) % 360.0 > 180.0 else asc
|
|
|
|
|
|
def _trisect(a: float, b: float) -> tuple[float, float]:
|
|
"""Dwa punkty dzielące łuk a→b (w kierunku zodiaku) na trzy równe części."""
|
|
span = (b - a) % 360.0
|
|
return norm360(a + span / 3.0), norm360(a + 2.0 * span / 3.0)
|
|
|
|
|
|
# Poniżej tej odległości od granicy znaku traktujemy Ascendent jak leżący DOKŁADNIE
|
|
# na niej. 1e-9° to 3,6 mikrosekundy łuku — o rzędy wielkości poniżej jakiejkolwiek
|
|
# realnej dokładności danych urodzeniowych, więc nie zmienia to żadnego horoskopu.
|
|
_SIGN_SNAP_DEG = 1e-9
|
|
|
|
|
|
def _snap_to_sign_boundary(lon: float) -> float:
|
|
"""Przyciąga długość do granicy znaku, gdy jest od niej o włos.
|
|
|
|
Whole sign jest NIECIĄGŁY na granicach znaków: różnica 10⁻¹¹° w Ascendencie
|
|
przerzuca cały dom I o 30°. Bez tego przyciągania ten sam horoskop policzony
|
|
na innej maszynie mógłby dać inny wynik (wykryte przez porównanie z wyrocznią:
|
|
nasz Asc = 359,999999999976, swissepha = 1e-10 — ta sama wartość po dwóch
|
|
stronach granicy Ryby/Baran). Determinizm jest tu ważniejszy niż dosłowność
|
|
zmiennoprzecinkowa."""
|
|
nearest = round(lon / 30.0) * 30.0
|
|
return norm360(nearest) if abs(lon - nearest) < _SIGN_SNAP_DEG else lon
|
|
|
|
|
|
def cusps(asc: float, mc: float, system: str) -> list[float]:
|
|
"""Zwraca 12 cusps (długości) domów 1..12."""
|
|
if system == WHOLE_SIGN:
|
|
start = sign_index(_snap_to_sign_boundary(asc)) * 30.0
|
|
return [norm360(start + 30.0 * i) for i in range(12)]
|
|
if system == EQUAL:
|
|
return [norm360(asc + 30.0 * i) for i in range(12)]
|
|
if system == PORPHYRY:
|
|
dsc, ic = norm360(asc + 180.0), norm360(mc + 180.0)
|
|
c = [0.0] * 12
|
|
c[0], c[3], c[6], c[9] = asc, ic, dsc, mc
|
|
c[1], c[2] = _trisect(asc, ic) # domy 2,3
|
|
c[4], c[5] = _trisect(ic, dsc) # domy 5,6
|
|
c[7], c[8] = _trisect(dsc, mc) # domy 8,9
|
|
c[10], c[11] = _trisect(mc, asc) # domy 11,12
|
|
return c
|
|
raise ValueError(f"nieznany system domów: {system}")
|
|
|
|
|
|
def polar_circle(eps_deg: float) -> float:
|
|
"""Szerokość koła podbiegunowego [°] dla danego nachylenia ekliptyki.
|
|
|
|
NIE jest to stała 66,56°: ε zmienia się z datą (ok. 23,71° w 370 p.n.e.,
|
|
23,44° dziś), więc granica przesuwa się o ~0,3° w zakresie dat programu.
|
|
Powyżej niej stopnie ekliptyki bywają okołobiegunowe — nie wschodzą ani nie
|
|
zachodzą — przez co systemy oparte na łuku dobowym (Placidus, Koch) tracą
|
|
definicję."""
|
|
return 90.0 - abs(eps_deg)
|
|
|
|
|
|
def cusps_for(ramc: float, eps: float, lat: float, system: str) -> list[float]:
|
|
"""Kanoniczne wejście: (RAMC, ε, φ) → 12 cusps.
|
|
|
|
Systemy proste (whole sign / equal / porphyry) potrzebują tylko Asc i MC,
|
|
ale systemy egzotyczne dzielą inne koła wielkie i wymagają pełnego zestawu
|
|
(RAMC, ε, φ). Ta funkcja jest wspólnym punktem wejścia dla obu rodzajów —
|
|
i to ją porównuje z wyrocznią framework testowy (tests/oracle).
|
|
"""
|
|
asc = compute_asc(ramc, eps, lat)
|
|
mc = compute_mc(ramc, eps)
|
|
return cusps(asc, mc, system)
|
|
|
|
|
|
def assign_house(lon: float, cusp_list: list[float]) -> int:
|
|
"""Numer domu (1..12), w którym leży dana długość ekliptyczna."""
|
|
lon = norm360(lon)
|
|
for i in range(12):
|
|
start = cusp_list[i]
|
|
end = cusp_list[(i + 1) % 12]
|
|
span = (end - start) % 360.0
|
|
offset = (lon - start) % 360.0
|
|
if offset < span:
|
|
return i + 1
|
|
return 12
|