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>
252 lines
11 KiB
Python
252 lines
11 KiB
Python
"""Testy osi i domów — czysta matematyka (LOG-05, bez efemeryd)."""
|
||
import math
|
||
|
||
import pytest
|
||
|
||
from app.engine import houses as H
|
||
|
||
# RAMC i ε policzone Skyfieldem dla horoskopu referencyjnego (30.04.1984, Warszawa)
|
||
RAMC, EPS, LAT = 353.1968, 23.44133, 52.2333
|
||
|
||
|
||
def _near(a, b, tol=0.05):
|
||
return abs(((a - b + 180) % 360) - 180) < tol
|
||
|
||
|
||
def test_asc_mc_match_reference():
|
||
asc = H.compute_asc(RAMC, EPS, LAT)
|
||
mc = H.compute_mc(RAMC, EPS)
|
||
assert _near(asc, 112.18) # Cancer 22°10' (astro.com)
|
||
assert _near(mc, 352.59) # Pisces 22°35'
|
||
|
||
|
||
def test_whole_sign_starts_on_sign_boundary():
|
||
cusps = H.cusps(112.18, 352.59, H.WHOLE_SIGN)
|
||
assert cusps[0] == 90.0 # dom 1 = 0° Raka
|
||
assert cusps[1] == 120.0
|
||
|
||
|
||
def test_equal_cusps_are_30_apart_from_asc():
|
||
cusps = H.cusps(112.18, 352.59, H.EQUAL)
|
||
assert abs(cusps[0] - 112.18) < 1e-9
|
||
assert abs(cusps[1] - 142.18) < 1e-9
|
||
|
||
|
||
def test_porphyry_angles_on_cusps():
|
||
cusps = H.cusps(112.18, 352.59, H.PORPHYRY)
|
||
assert abs(cusps[0] - 112.18) < 1e-9 # Asc = dom 1
|
||
assert abs(cusps[9] - 352.59) < 1e-9 # MC = dom 10
|
||
assert abs(cusps[6] - (112.18 + 180) % 360) < 1e-9 # Dsc = dom 7
|
||
|
||
|
||
def test_assign_house_whole_sign():
|
||
cusps = H.cusps(112.18, 352.59, H.WHOLE_SIGN) # dom 1 = Rak (90–120°)
|
||
assert H.assign_house(100.0, cusps) == 1 # w Raku
|
||
assert H.assign_house(40.0, cusps) == 11 # Byk -> 11. dom
|
||
|
||
|
||
# ── regresje wykryte przez porównanie z wyrocznią (tests/oracle) ──────────
|
||
|
||
def test_ascendant_branch_beyond_polar_circle():
|
||
"""REGRESJA: powyżej koła podbiegunowego `atan2` wybierał NIEWŁAŚCIWY punkt
|
||
przecięcia ekliptyki z horyzontem — zwracaliśmy Descendent zamiast Ascendenta,
|
||
czyli błąd o 180°. Planety lądowały w PRZECIWNYCH domach dla całej północnej
|
||
Skandynawii. Rozstrzyga położenie względem MC: punkt wschodzący leży zawsze
|
||
w półkolu (0°,180°) na wschód od MC."""
|
||
eps = 23.4393
|
||
for lat in (67.0, 70.0, 85.0, -70.0):
|
||
for ramc in (60.0, 100.0, 200.0, 270.0, 300.0):
|
||
asc = H.compute_asc(ramc, eps, lat)
|
||
mc = H.compute_mc(ramc, eps)
|
||
offset = (asc - mc) % 360.0
|
||
assert 0.0 <= offset <= 180.0, (
|
||
f"Asc poza półkolem wschodzącym: lat={lat} ramc={ramc} offset={offset:.4f}")
|
||
|
||
|
||
def test_whole_sign_is_deterministic_at_sign_boundary():
|
||
"""REGRESJA: Ascendent o włos od granicy znaku (359,999999999976 vs 1e-10 —
|
||
ta sama wartość, różne strony) przerzucał cały dom I o 30°. Ten sam horoskop
|
||
na innej maszynie dawał inny wynik."""
|
||
below = H.cusps(359.999999999976, 270.0, H.WHOLE_SIGN)
|
||
above = H.cusps(1e-10, 270.0, H.WHOLE_SIGN)
|
||
assert below[0] == above[0] == 0.0, (below[0], above[0])
|
||
|
||
|
||
def test_polar_circle_moves_with_obliquity():
|
||
"""Granica dziedziny Placidusa/Kocha NIE jest stałą 66,56° — zależy od ε,
|
||
a ε zmienia się z datą. Testy brzegowe muszą ją liczyć per data."""
|
||
assert H.polar_circle(23.4393) == pytest.approx(66.5607, abs=1e-4) # dziś
|
||
assert H.polar_circle(23.747) == pytest.approx(66.253, abs=1e-4) # 370 p.n.e.
|
||
|
||
|
||
# ── systemy egzotyczne o zamkniętym wzorze (Etap 1) ──────────────────────
|
||
# Zgodność z wyrocznią sprawdza tests/oracle; tu pilnujemy niezmienników, które
|
||
# muszą zachodzić także bez swissepha (czyli w każdym środowisku).
|
||
|
||
EXOTIC = ("vehlow", "morinus", "regiomontanus", "campanus", "alcabitus",
|
||
"topocentric")
|
||
|
||
|
||
@pytest.mark.parametrize("system", EXOTIC)
|
||
def test_exotic_returns_twelve_cusps_in_range(system):
|
||
out = H.cusps_for(100.0, 23.4393, 50.0, system)
|
||
assert len(out) == 12
|
||
assert all(0.0 <= c < 360.0 for c in out)
|
||
|
||
|
||
@pytest.mark.parametrize("system", EXOTIC)
|
||
def test_opposite_houses_are_antipodal(system):
|
||
"""Domy przeciwległe leżą na tym samym kole wielkim, więc ich cuspy są
|
||
dokładnie antypodyczne. Naruszenie tego oznacza błąd w wyborze gałęzi."""
|
||
out = H.cusps_for(137.0, 23.4393, 42.0, system)
|
||
for i in range(6):
|
||
assert abs(((out[i + 6] - out[i]) % 360.0) - 180.0) < 1e-9, f"domy {i+1}/{i+7}"
|
||
|
||
|
||
@pytest.mark.parametrize("system", ("regiomontanus", "campanus", "alcabitus"))
|
||
def test_quadrant_systems_anchor_on_ascendant(system):
|
||
"""Systemy kwadrantowe zaczynają dom I na Ascendencie."""
|
||
ramc, eps, lat = 100.0, 23.4393, 50.0
|
||
assert H.cusps_for(ramc, eps, lat, system)[0] == pytest.approx(
|
||
H.compute_asc(ramc, eps, lat), abs=1e-9)
|
||
|
||
|
||
def test_vehlow_puts_ascendant_in_the_middle_of_house_one():
|
||
ramc, eps, lat = 100.0, 23.4393, 50.0
|
||
asc = H.compute_asc(ramc, eps, lat)
|
||
assert H.cusps_for(ramc, eps, lat, "vehlow")[0] == pytest.approx(
|
||
H.norm360(asc - 15.0), abs=1e-9)
|
||
|
||
|
||
def test_morinus_ignores_latitude():
|
||
"""Morinus rzutuje równik wprost na ekliptykę, bez horyzontu — jako jedyny
|
||
nie zależy od szerokości geograficznej."""
|
||
a = H.cusps_for(100.0, 23.4393, 20.0, "morinus")
|
||
b = H.cusps_for(100.0, 23.4393, 65.0, "morinus")
|
||
assert a == pytest.approx(b, abs=1e-12)
|
||
|
||
|
||
def test_unknown_system_is_rejected():
|
||
with pytest.raises(ValueError):
|
||
H.cusps_for(100.0, 23.4393, 50.0, "nie-ma-takiego")
|
||
|
||
|
||
# ── systemy łuku dobowego: Placidus i Koch (Etap 2) ──────────────────────
|
||
|
||
DIURNAL = ("placidus", "koch")
|
||
TROMSO_LAT = 69.65 # za kołem podbiegunowym — tam te systemy nie istnieją
|
||
KRAKOW_LAT = 50.06
|
||
|
||
|
||
@pytest.mark.parametrize("system", DIURNAL)
|
||
def test_diurnal_systems_refuse_beyond_the_polar_circle(system):
|
||
"""Odmowa, nie liczba. Cicha podmiana systemu jest niewykrywalna z wykresu."""
|
||
with pytest.raises(H.HouseSystemUndefined):
|
||
H.cusps_for(100.0, 23.4393, TROMSO_LAT, system)
|
||
|
||
|
||
@pytest.mark.parametrize("system", DIURNAL)
|
||
def test_fallback_says_what_it_did(system):
|
||
cs = H.cusps_detailed(100.0, 23.4393, TROMSO_LAT, system)
|
||
assert cs.is_fallback and cs.system == H.FALLBACK_SYSTEM and cs.requested == system
|
||
assert len(cs.cusps) == 12
|
||
assert cs.notice and system in cs.notice and H.FALLBACK_SYSTEM in cs.notice
|
||
|
||
|
||
@pytest.mark.parametrize("system", DIURNAL)
|
||
def test_no_fallback_and_no_notice_where_the_system_exists(system):
|
||
cs = H.cusps_detailed(100.0, 23.4393, KRAKOW_LAT, system)
|
||
assert not cs.is_fallback and cs.notice is None
|
||
assert cs.cusps == H.cusps_for(100.0, 23.4393, KRAKOW_LAT, system)
|
||
|
||
|
||
def test_placidus_cusps_satisfy_their_own_definition():
|
||
"""Sprawdzian NIEZALEŻNY od wyroczni: czy znaleziony punkt naprawdę przebył
|
||
zadany ułamek swojego półłuku. Wyrocznia mówi „zgadza się z swissephem",
|
||
to mówi „zgadza się z definicją Placidusa" — i działa bez swissepha."""
|
||
ramc, eps, lat = 212.9986, 23.4393, 66.3144 # tuż pod kołem, najtrudniej
|
||
eps_rad = math.radians(eps)
|
||
c = H.cusps_for(ramc, eps, lat, "placidus")
|
||
for house, (frac, nocturnal) in ((11, (1 / 3, False)), (12, (2 / 3, False)),
|
||
(2, (2 / 3, True)), (3, (1 / 3, True))):
|
||
lon = c[house - 1]
|
||
dec = H._declination_of_ecliptic_lon(lon, eps_rad)
|
||
ad = H._ascensional_difference(dec, lat)
|
||
ra = math.degrees(math.atan2(math.sin(math.radians(lon)) * math.cos(eps_rad),
|
||
math.cos(math.radians(lon))))
|
||
want = (ramc + 180.0 - frac * (90.0 - ad) if nocturnal
|
||
else ramc + frac * (90.0 + ad))
|
||
assert abs((ra - want + 180.0) % 360.0 - 180.0) < 1e-9, f"dom {house}"
|
||
|
||
|
||
def test_koch_reduces_to_equal_spacing_on_the_equator():
|
||
"""Na równiku półłuk każdego stopnia to dokładnie 90°, więc trójdzielenie
|
||
czasu daje Ascendenty co 30° czasu gwiazdowego — dobry test zdrowia wzoru."""
|
||
ramc, eps = 100.0, 23.4393
|
||
c = H.cusps_for(ramc, eps, 0.0, "koch")
|
||
for house, offset in ((11, -60.0), (12, -30.0), (2, 30.0), (3, 60.0)):
|
||
assert c[house - 1] == pytest.approx(
|
||
H.compute_asc(ramc + offset, eps, 0.0), abs=1e-9), f"dom {house}"
|
||
|
||
|
||
def test_polar_circle_boundary_moves_with_the_date():
|
||
"""Granica dziedziny NIE jest stałą 66,56° — zależy od ε, czyli od daty."""
|
||
lat = 66.60
|
||
assert lat > H.polar_circle(23.4393) # dziś: poza dziedziną
|
||
assert lat < H.polar_circle(23.3528) # rok 2270: już w dziedzinie
|
||
with pytest.raises(H.HouseSystemUndefined):
|
||
H.cusps_for(100.0, 23.4393, lat, "placidus")
|
||
assert len(H.cusps_for(100.0, 23.3528, lat, "placidus")) == 12
|
||
|
||
|
||
def test_topocentric_cusps_stay_in_order_between_mc_and_ascendant():
|
||
"""Regresja na to, co topocentrica psuło najdłużej — wybór gałęzi.
|
||
|
||
Cuspy 11 i 12 to punkty CIĄGŁEJ rodziny biegnącej od MC do Ascendentu, więc
|
||
czwórka MC → 11 → 12 → Asc leży na okręgu w kolejności, bez wyprzedzania się.
|
||
Zły wybór gałęzi przerzuca któryś punkt o 180° i kolejność pęka.
|
||
|
||
Kierunek celowo NIE jest ustalony: przy dużych szerokościach domy biegną
|
||
wstecz (przy φ=−79,55° MC=306,8°, a dom 11 to 291,4°) i to jest poprawne —
|
||
wyrocznia zwraca tam dokładnie to samo. Sprawdzamy więc, że kolejność trzyma
|
||
się w JEDNĄ ze stron, a nie że rośnie."""
|
||
def d(a, b):
|
||
return H.norm360(b - a)
|
||
|
||
for lat in (-89.9, -79.55, -67.18, 0.0, 50.06, 73.69, 80.66, 89.9):
|
||
c = H.cusps_for(129.2387, 23.4393, lat, "topocentric")
|
||
mc, c11, c12, asc = c[9], c[10], c[11], c[0]
|
||
forward = d(mc, c11) + d(c11, c12) + d(c12, asc)
|
||
backward = d(c11, mc) + d(c12, c11) + d(asc, c12)
|
||
assert (abs(forward - d(mc, asc)) < 1e-9
|
||
or abs(backward - d(asc, mc)) < 1e-9), \
|
||
f"φ={lat}: MC={mc:.3f} 11={c11:.3f} 12={c12:.3f} Asc={asc:.3f} nie po kolei"
|
||
|
||
|
||
def test_topocentric_needs_no_domain_limit():
|
||
"""W przeciwieństwie do Placidusa/Kocha topocentric liczy się WSZĘDZIE —
|
||
nie ma tu granicy dziedziny do wyprowadzenia, więc nie wolno jej udawać."""
|
||
for lat in (0.0, 66.5, 69.65, 89.9, -89.9):
|
||
assert len(H.cusps_for(100.0, 23.4393, lat, "topocentric")) == 12
|
||
cs = H.cusps_detailed(100.0, 23.4393, 89.9, "topocentric")
|
||
assert not cs.is_fallback and cs.notice is None
|
||
|
||
|
||
def test_whole_sign_aries_ignores_the_ascendant():
|
||
"""Wariant „od Barana": dom I zaczyna się na 0° Barana niezależnie od tego,
|
||
co wschodzi. To odróżnia go od zwykłego whole sign."""
|
||
for lat in (0.0, 50.06, -33.9):
|
||
c = H.cusps_for(100.0, 23.4393, lat, "whole_sign_aries")
|
||
assert c == [pytest.approx(30.0 * i) for i in range(12)]
|
||
|
||
|
||
def test_equal_mc_starts_the_tenth_house_exactly_on_the_midheaven():
|
||
"""W `equal` MC leży GDZIEŚ w domu X; w `equal_mc` zaczyna go dokładnie."""
|
||
ramc, eps, lat = 100.0, 23.4393, 50.0
|
||
mc = H.compute_mc(ramc, eps)
|
||
c = H.cusps_for(ramc, eps, lat, "equal_mc")
|
||
assert c[9] == pytest.approx(mc, abs=1e-9) # dom X rusza na MC
|
||
assert c[0] == pytest.approx(H.norm360(mc + 90.0), abs=1e-9)
|
||
for i in range(12): # nadal równe 30°
|
||
assert H.norm360(c[(i + 1) % 12] - c[i]) == pytest.approx(30.0, abs=1e-9)
|