Files
astrololo/tests/oracle/domain.py
T
gitea 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
test: framework porównania domów z wyrocznią + DWA błędy, które od razu wykrył
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>
2026-08-04 18:03:08 +02:00

142 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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).
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"})
# 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:
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)
]