4d1daab35a
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m34s
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) Successful in 18s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 10s
build / build (push) Successful in 26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m37s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 10s
Ostatnie trzy pozycje z treści LOG-05, których wcześniej nie było: - whole_sign_aries — znaki jako domy, ale dom I ZAWSZE na 0° Barana, niezależnie od Ascendentu (tradycja indyjska i część szkół hellenistycznych), - equal_mc — równe domy zakotwiczone na MC: dom X zaczyna się dokładnie na południku, a nie ma go gdzieś w środku, - obrót kosmogramu: Ascendent albo 0° Barana po lewej stronie koła. Zmienia WYŁĄCZNIE rysunek, żadna liczba nie jest przeliczana. Wariant „od Barana" unieruchamia koło względem zodiaku, więc dwa horoskopy da się porównać na oko. Oba nowe systemy zgodne z wyrocznią co do zera od pierwszego uruchomienia. Razem 13 systemów: 2 833 424 porównania w przemiale, zero przekroczeń. PRZY OKAZJI — DWA BŁĘDY, KTÓRE SAM WPROWADZIŁEM I KTÓRYCH TESTY NIE WIDZIAŁY: - linia zbierająca ostrzeżenia o fallbacku trafiła do handlera strony głównej zamiast do compile_pdf: odwoływała się do nieistniejącej zmiennej, czyli 500 na stronie głównej, a do PDF-a ostrzeżenia nie docierały wcale, - compile_build używał parametru formularza, którego nie miał w sygnaturze. Oba przeszły przez komplet zielonych testów, bo żaden nie wywoływał POST-a — testy prezentacji sprawdzały teksty w szablonach i w main.py. Doszły więc testy uderzające w prawdziwe trasy (POST / i POST /compile ze stubowaną logiką), które łapią tę klasę błędu. Z tego samego powodu przepisane dwa testy PDF-a: greppowały z main.py dokładny kształt wywołania render(chart, theme="print") i pękały przy dopisaniu argumentu, mimo poprawnego zachowania. Teraz wołają trasę i sprawdzają, że KAŻDY z czterech rysunków dostaje motyw druku. LOG-05 i PRE-05 → Zrobione w docs/astrololo_wymagania.xlsx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
179 lines
7.0 KiB
Python
179 lines
7.0 KiB
Python
"""Porównanie naszych domów z wyrocznią (Swiss Ephemeris) i raport zgodności.
|
|
|
|
ZASADA IZOLACJI: obu implementacjom podajemy TE SAME wejścia (RAMC, ε, φ) przez
|
|
`swe.houses_armc`. Gdybyśmy porównywali „nasz horoskop" z „horoskopem swissepha",
|
|
różnica w cuspie mogłaby wynikać z innego czasu gwiazdowego albo innego ε — czyli
|
|
z czegoś, co nie jest testowaną funkcją. Wtedy przy tysiącach przypadków utonęlibyśmy
|
|
w fałszywych alarmach. Czas gwiazdowy i ε testujemy OSOBNO (`compare_asc_mc`).
|
|
|
|
KRYTERIUM: nie procent zgodności, tylko liczba przypadków POWYŻEJ tolerancji plus
|
|
maksymalne odchylenie i MIEJSCE jego wystąpienia. Procent ukrywa kształt błędu:
|
|
„97%" nie odróżnia szumu zmiennoprzecinkowego od jednego rogu dziedziny, w którym
|
|
mylimy się o 30°.
|
|
|
|
LICENCJA: Swiss Ephemeris (AGPL) jest tu WYŁĄCZNIE wyrocznią testową. Nie wchodzi
|
|
do ścieżki produkcyjnej ani do zależności produktu — izolacja z LOG-27 zostaje.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
import swisseph as swe
|
|
|
|
from domain import Case, in_domain, obliquity
|
|
|
|
# Litery systemów w Swiss Ephemeris.
|
|
SWE_CODE = {
|
|
"whole_sign": b"W", "equal": b"E",
|
|
"equal_mc": b"D",
|
|
"whole_sign_aries": b"N", "porphyry": b"O",
|
|
"placidus": b"P", "koch": b"K", "regiomontanus": b"R", "campanus": b"C",
|
|
"morinus": b"M", "alcabitus": b"B", "vehlow": b"V", "topocentric": b"T",
|
|
}
|
|
|
|
TOLERANCE_DEG = 1.0 / 3600.0 # 1″ — uzgodniony próg
|
|
|
|
|
|
def angular_diff(a: float, b: float) -> float:
|
|
"""Najmniejsza różnica kątowa [0,180] — 359,9° i 0,1° dzieli 0,2°, nie 359,8°."""
|
|
d = abs(a - b) % 360.0
|
|
return min(d, 360.0 - d)
|
|
|
|
|
|
@dataclass
|
|
class Result:
|
|
system: str
|
|
checked: int = 0
|
|
over_tolerance: int = 0
|
|
skipped_out_of_domain: int = 0
|
|
max_dev: float = 0.0
|
|
max_where: str = ""
|
|
domain_mismatch: list[str] = field(default_factory=list)
|
|
deviations: list[float] = field(default_factory=list)
|
|
|
|
@property
|
|
def passed(self) -> bool:
|
|
return self.over_tolerance == 0 and not self.domain_mismatch
|
|
|
|
|
|
def oracle_cusps(case: Case, eps: float, system: str) -> list[float] | None:
|
|
"""Cuspy z wyroczni albo None, gdy wyrocznia odmawia (poza dziedziną).
|
|
|
|
pyswisseph zgłasza wyjątek dla Placidusa/Kocha powyżej koła podbiegunowego —
|
|
to sygnał, że zestaw jest poza dziedziną, i sam w sobie jest użyteczny: pozwala
|
|
sprawdzić, czy NASZA detekcja granicy pokrywa się z wyrocznią."""
|
|
try:
|
|
cusps, _ = swe.houses_armc(case.ramc % 360.0, case.lat, eps, SWE_CODE[system])
|
|
return list(cusps)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def compare_asc_mc(cases: list[Case]) -> Result:
|
|
"""Test funkcji WEJŚCIOWEJ: nasze Asc/MC vs wyrocznia, przy tym samym (RAMC, ε, φ).
|
|
|
|
Osobno, bo błąd tutaj kaskadowałby na wszystkie systemy i wyglądał jak błąd domów.
|
|
"""
|
|
from app.engine.houses import compute_asc, compute_mc
|
|
|
|
r = Result(system="asc/mc")
|
|
for c in cases:
|
|
eps = obliquity(c.jd)
|
|
try:
|
|
_, ascmc = swe.houses_armc(c.ramc % 360.0, c.lat, eps, b"O")
|
|
except Exception:
|
|
r.skipped_out_of_domain += 1
|
|
continue
|
|
for ours, theirs, what in (
|
|
(compute_asc(c.ramc, eps, c.lat), ascmc[0], "Asc"),
|
|
(compute_mc(c.ramc, eps), ascmc[1], "MC"),
|
|
):
|
|
d = angular_diff(ours, theirs)
|
|
r.checked += 1
|
|
r.deviations.append(d)
|
|
if d > r.max_dev:
|
|
r.max_dev, r.max_where = d, f"{what} @ {c.key()} [{c.label}]"
|
|
if d > TOLERANCE_DEG:
|
|
r.over_tolerance += 1
|
|
return r
|
|
|
|
|
|
def compare_system(system: str, cases: list[Case]) -> Result:
|
|
"""Porównanie jednego systemu domów z wyrocznią na zadanym zestawie przypadków."""
|
|
from app.engine.houses import cusps_for
|
|
|
|
r = Result(system=system)
|
|
for c in cases:
|
|
eps = obliquity(c.jd)
|
|
ours_in_domain = in_domain(system, c.lat, c.jd)
|
|
theirs = oracle_cusps(c, eps, system)
|
|
|
|
# Zgodność co do DZIEDZINY: jeśli wyrocznia odmawia, a my uważamy, że
|
|
# liczymy — nasza detekcja granicy jest zepsuta (albo odwrotnie).
|
|
if (theirs is None) != (not ours_in_domain):
|
|
r.domain_mismatch.append(
|
|
f"{c.key()} [{c.label}] — my:{'w dziedzinie' if ours_in_domain else 'poza'}, "
|
|
f"wyrocznia:{'odmawia' if theirs is None else 'liczy'}")
|
|
continue
|
|
if theirs is None:
|
|
r.skipped_out_of_domain += 1
|
|
continue
|
|
|
|
try:
|
|
ours = cusps_for(c.ramc, eps, c.lat, system)
|
|
except Exception as e: # nasz kod NIE MOŻE się wywalić
|
|
r.domain_mismatch.append(f"{c.key()} [{c.label}] — nasz kod rzucił: {e}")
|
|
continue
|
|
|
|
for i in range(12):
|
|
d = angular_diff(ours[i], theirs[i])
|
|
r.checked += 1
|
|
r.deviations.append(d)
|
|
if d > r.max_dev:
|
|
r.max_dev, r.max_where = d, f"dom {i + 1} @ {c.key()} [{c.label}]"
|
|
if d > TOLERANCE_DEG:
|
|
r.over_tolerance += 1
|
|
return r
|
|
|
|
|
|
def histogram(devs: list[float]) -> str:
|
|
"""Rozkład odchyleń — pokazuje KSZTAŁT zgodności, nie tylko sam maksimum."""
|
|
if not devs:
|
|
return " (brak danych)"
|
|
buckets = [
|
|
("= 0", lambda d: d == 0.0),
|
|
("< 1e-9°", lambda d: 0 < d < 1e-9),
|
|
("< 1e-6°", lambda d: 1e-9 <= d < 1e-6),
|
|
("< 1″", lambda d: 1e-6 <= d < TOLERANCE_DEG),
|
|
("≥ 1″ (BŁĄD)", lambda d: d >= TOLERANCE_DEG),
|
|
]
|
|
out = []
|
|
for name, pred in buckets:
|
|
n = sum(1 for d in devs if pred(d))
|
|
if n:
|
|
out.append(f" {name:14} {n:7} {'█' * min(40, 1 + n * 40 // len(devs))}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def format_report(results: list[Result], seed: int | None = None) -> str:
|
|
lines = ["", "=" * 78,
|
|
"ZGODNOŚĆ Z WYROCZNIĄ (Swiss Ephemeris) — domy astrologiczne",
|
|
f"tolerancja: {TOLERANCE_DEG:.8f}° (1″)" + (f" ziarno: {seed}" if seed is not None else ""),
|
|
"=" * 78,
|
|
f"{'SYSTEM':<18}{'SPRAWDZONYCH':>13}{'>TOL':>7}{'POZA DZIEDZ.':>14}{'MAX ODCH.':>14} WYNIK"]
|
|
for r in results:
|
|
lines.append(f"{r.system:<18}{r.checked:>13}{r.over_tolerance:>7}"
|
|
f"{r.skipped_out_of_domain:>14}{r.max_dev:>14.9f} "
|
|
f"{'OK' if r.passed else 'BŁĄD'}")
|
|
for r in results:
|
|
if r.max_dev > 0 or not r.passed:
|
|
lines += ["", f" [{r.system}] największe odchylenie: {r.max_dev:.9f}° — {r.max_where}"]
|
|
if r.domain_mismatch:
|
|
lines.append(f" [{r.system}] NIEZGODNOŚĆ DZIEDZINY ({len(r.domain_mismatch)}):")
|
|
lines += [f" {m}" for m in r.domain_mismatch[:5]]
|
|
if len(r.domain_mismatch) > 5:
|
|
lines.append(f" … i {len(r.domain_mismatch) - 5} więcej")
|
|
lines += ["", " Rozkład odchyleń (wszystkie systemy):", histogram(
|
|
[d for r in results for d in r.deviations]), "=" * 78]
|
|
return "\n".join(lines)
|