test: framework porównania domów z wyrocznią + DWA błędy, które od razu wykrył
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>
This commit was merged in pull request #60.
This commit is contained in:
2026-08-04 18:03:08 +02:00
parent e3114f3e7c
commit 86a0f16f9e
7 changed files with 571 additions and 4 deletions
+62 -4
View File
@@ -31,12 +31,27 @@ def compute_mc(ramc_deg: float, eps_deg: float) -> float:
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 = math.atan2(
asc = norm360(math.degrees(math.atan2(
math.cos(r),
-(math.sin(r) * math.cos(e) + math.tan(phi) * math.sin(e)),
)
return norm360(math.degrees(asc))
)))
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]:
@@ -45,10 +60,29 @@ def _trisect(a: float, b: float) -> tuple[float, float]:
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(asc) * 30.0
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)]
@@ -64,6 +98,30 @@ def cusps(asc: float, mc: float, system: str) -> list[float]:
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)