f1956a08ff
build / build (push) Successful in 1m1s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m57s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 28s
Testy / Kontrola składni wszystkich warstw (push) Successful in 24s
Aspekty głowne (LOG-06) mierzą kąt wzdłuż ekliptyki. LOG-07 dokłada dwa rodzaje powiązań, które klasyczna astrologia liczy naprawdę, nie na oko: - PARALELA/KONTRPARALELA DEKLINACJI — dwa ciała na tej samej („parallel") lub przeciwnej („contraparallel") deklinacji działają jak koniunkcja/opozycja, mimo że wzdłuż ekliptyki mogą stać gdziekolwiek. Deklinacja liczona PEŁNYM wzorem (z szerokością ekliptyczną — istotne dla Księżyca i planet schodzących z ekliptyki), przez to_equatorial z LOG-04. Orb konfigurowalny (domyślnie 1°, ciasno — to kontakty punktowe). - ANTYSCJA/KONTRANTYSCJA — odbicie długości względem osi przesileń (0° Raka – 0° Koziorożca) albo równonocy (0° Barana – 0° Wagi). Liczone na współrzędnych TROPIKALNYCH of-date, bo deklinacja jest fizyczna (równikowa, niezależna od zodiaku), a antyscja z definicji tropikalna — jej oś to kardynalne punkty zodiaku tropikalnego. Dlatego PRZED przesunięciem na zodiak syderyczny/draconiczny. Dodatkowo: flaga „out of bounds" (|deklinacja| > nachylenie ekliptyki — ciało poza zakresem Słońca) i kolumna deklinacji przy każdej pozycji. Integracja z bazą: baza interpretacyjna ZNA paralele pod frazą „P. Dec.", więc generujemy fasetkę paraleli (trafia też do promptu LLM). PUŁAPKA: samo „Dec." w bazie to często DEKANAT („3rd Dec. of [Gem"), więc szukamy dokładnie „P. Dec." — inaczej sypnęłoby fałszywymi trafieniami. Kontrparaleli i antyscji baza nie opisuje osobnym znacznikiem, więc zostają obliczeniem display-only. Pary z RIGID_PAIRS (węzły) odsiane: SN = NN+180° na ekliptyce → deklinacja ZAWSZE przeciwna, czyli definicyjna kontrparalela bez informacji. Walidacja wobec faktów NIEZALEŻNYCH od kodu: - deklinacja Słońca 30.04.1984 = +14.88° wobec ~+14.9° z almanachu, - antyscja to arytmetyka odbicia: inwolucja i pary znaków (Rak↔Bliźnięta, Baran↔Panna) sprawdzone na piechotę, - węzły faktycznie mają przeciwną deklinację i są odsiane, - deklinacja niezależna od zodiaku (tropikalny == syderyczny co do 1e-6°), - realna baza: mechanizm paraleli znajduje wpisy „P. Dec." i odsiewa dekanaty (z 33 wpisów „P. Dec." 5 ma niepusty efekt — głównie „[conj or P. Dec."). UI: kolumna deklinacji (+znacznik OOB), tabela paraleli/kontrparaleli i tabela antyscji na ekranie Horoskop. Testy: 15 nowych (out_of_zodiac) + 2 (fasetka paraleli vs pułapka dekanatu). Całość: logika 251 passed / 1 skipped, prezentacja 25 passed. Render szablonu sprawdzony osobno (bez błędu Jinja, wszystkie sekcje obecne). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
158 lines
6.5 KiB
Python
158 lines
6.5 KiB
Python
"""Aspekty pozazodiakalne — paralele deklinacji i antyscja (LOG-07).
|
|
|
|
Walidacja wobec faktów NIEZALEŻNYCH od naszego kodu:
|
|
* deklinacja Słońca 30.04.1984 ≈ +14.9° (almanach; Słońce w ~10° Byka, koniec
|
|
kwietnia, deklinacja rośnie ku przesileniu letniemu),
|
|
* antyscja to czysta arytmetyka odbicia względem osi przesileń — pary znaków
|
|
(Rak↔Bliźnięta, Baran↔Panna) i inwolucja dają się sprawdzić na piechotę,
|
|
* węzły są z definicji w kontrparaleli (SN = NN+180° na ekliptyce → deklinacja
|
|
przeciwna), więc muszą być odsiane jako kontakt bez informacji.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from app.engine.chart import build_chart
|
|
from app.engine.models import ChartMoment
|
|
from app.engine.out_of_zodiac import (
|
|
antiscion,
|
|
contra_antiscion,
|
|
declination,
|
|
find_antiscia,
|
|
find_declination_aspects,
|
|
is_out_of_bounds,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def krakow():
|
|
return ChartMoment(when_utc=datetime(1984, 4, 30, 9, 20, tzinfo=timezone.utc),
|
|
lat=50.0647, lon=19.9450)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def chart(own_engine, krakow):
|
|
return build_chart(own_engine, krakow)
|
|
|
|
|
|
# ----------------------------------------------------------------- deklinacja
|
|
|
|
def test_sun_declination_matches_almanac(chart):
|
|
"""Deklinacja Słońca 30.04.1984 ≈ +14.9° (wartość z almanachu)."""
|
|
sun = next(p for p in chart["positions"] if p["name"] == "Sun")
|
|
assert 14.6 < sun["declination"] < 15.1
|
|
|
|
|
|
def test_declination_uses_latitude_not_only_longitude():
|
|
"""Pełny wzór: przy tej samej długości różna szerokość → różna deklinacja.
|
|
Gdyby liczyć z samej długości (β=0), obie wyszłyby identyczne."""
|
|
eps = 23.44
|
|
on_ecliptic = declination(60.0, 0.0, eps) # β = 0
|
|
above = declination(60.0, 5.0, eps) # 5° na północ od ekliptyki
|
|
assert abs(above - on_ecliptic) > 3.0 # szerokość realnie zmienia deklinację
|
|
|
|
|
|
def test_obliquity_is_reasonable_for_1984(chart):
|
|
assert 23.43 < chart["obliquity"] < 23.45
|
|
|
|
|
|
def test_out_of_bounds_flag():
|
|
eps = 23.44
|
|
assert is_out_of_bounds(24.0, eps) is True # poza zakresem Słońca
|
|
assert is_out_of_bounds(20.0, eps) is False
|
|
assert is_out_of_bounds(-24.5, eps) is True # także na południe
|
|
|
|
|
|
# --------------------------------------------------------------- antyscja (wzór)
|
|
|
|
def test_antiscion_is_reflection_across_solstitial_axis():
|
|
assert antiscion(0.0) == pytest.approx(180.0) # 0° Barana → 0° Wagi
|
|
assert antiscion(105.0) == pytest.approx(75.0) # 15° Raka → 15° Bliźniąt
|
|
assert antiscion(90.0) == pytest.approx(90.0) # 0° Raka leży NA osi
|
|
|
|
|
|
def test_antiscion_is_an_involution():
|
|
for lon in (12.3, 88.0, 200.5, 359.9):
|
|
assert antiscion(antiscion(lon)) == pytest.approx(lon)
|
|
|
|
|
|
def test_contra_antiscion_across_equinoctial_axis():
|
|
assert contra_antiscion(0.0) == pytest.approx(0.0) # 0° Barana leży NA osi
|
|
assert contra_antiscion(30.0) == pytest.approx(330.0) # 0° Byka → 0° Ryb (odbicie)
|
|
|
|
|
|
def test_sign_pairs_are_classic():
|
|
"""Antyscja parami znaków: Baran↔Panna, Byk↔Lew, Bliźnięta↔Rak."""
|
|
# 5° Barana (5°) → 175° = 25° Panny
|
|
assert antiscion(5.0) == pytest.approx(175.0)
|
|
# 10° Byka (40°) → 140° = 20° Lwa
|
|
assert antiscion(40.0) == pytest.approx(140.0)
|
|
|
|
|
|
# --------------------------------------------------- wykrywanie paraleli/antyscji
|
|
|
|
def test_parallel_and_contraparallel_detection():
|
|
bodies = [
|
|
{"name": "A", "declination": 12.0},
|
|
{"name": "B", "declination": 12.4}, # paralela z A (0.4°)
|
|
{"name": "C", "declination": -11.6}, # kontrparalela z A (|12-11.6|=0.4)
|
|
{"name": "D", "declination": 5.0}, # nic (daleko)
|
|
]
|
|
res = {(r["obj1"], r["obj2"]): r for r in find_declination_aspects(bodies, orb=1.0)}
|
|
assert res[("A", "B")]["type"] == "parallel"
|
|
assert res[("A", "C")]["type"] == "contraparallel"
|
|
assert ("A", "D") not in res
|
|
|
|
|
|
def test_orb_is_configurable():
|
|
bodies = [{"name": "A", "declination": 10.0}, {"name": "B", "declination": 11.5}]
|
|
assert find_declination_aspects(bodies, orb=1.0) == [] # 1.5° > 1.0°
|
|
assert len(find_declination_aspects(bodies, orb=2.0)) == 1 # mieści się
|
|
|
|
|
|
def test_nodes_definitional_contraparallel_is_filtered(chart):
|
|
"""SN = NN+180° na ekliptyce → deklinacja przeciwna ZAWSZE. To kontakt bez
|
|
informacji, więc pary NN/SN nie może być w wynikach."""
|
|
decl = {p["name"]: p["declination"] for p in chart["positions"]}
|
|
# najpierw potwierdzamy, że deklinacje faktycznie są przeciwne
|
|
assert decl["North Node"] == pytest.approx(-decl["South Node"], abs=0.05)
|
|
# a mimo to para NN/SN nie pojawia się na liście paraleli
|
|
pairs = {frozenset((r["obj1"], r["obj2"])) for r in chart["parallels"]}
|
|
assert frozenset(("North Node", "South Node")) not in pairs
|
|
|
|
|
|
def test_antiscia_detection_with_constructed_longitudes():
|
|
# A na 40°, B na 138° → antyscja A leży na 140°, więc B jest 2° od niej
|
|
bodies = [{"name": "A", "decimal": 40.0}, {"name": "B", "decimal": 138.0}]
|
|
assert find_antiscia(bodies, orb=1.0) == [] # 2° > 1°
|
|
hit = find_antiscia(bodies, orb=3.0)
|
|
assert len(hit) == 1 and hit[0]["type"] == "antiscion"
|
|
|
|
|
|
def test_contra_antiscion_detection():
|
|
# A na 40°, kontrantyscja A = 320°; B na 320.5° → kontrantyscja w orbie
|
|
bodies = [{"name": "A", "decimal": 40.0}, {"name": "B", "decimal": 320.5}]
|
|
hit = find_antiscia(bodies, orb=1.0)
|
|
assert len(hit) == 1 and hit[0]["type"] == "contra_antiscion"
|
|
|
|
|
|
# ------------------------------------------------------ spójność z build_chart
|
|
|
|
def test_chart_exposes_parallels_and_antiscia(chart):
|
|
assert "parallels" in chart and "antiscia" in chart
|
|
assert "obliquity" in chart
|
|
# znany, ciasny kontakt z tego horoskopu: Uran ~paralela~ Neptun (mid-80s)
|
|
un = next((r for r in chart["parallels"]
|
|
if {r["obj1"], r["obj2"]} == {"Uranus", "Neptune"}), None)
|
|
assert un is not None and un["type"] == "parallel"
|
|
|
|
|
|
def test_declination_is_zodiac_independent(own_engine, krakow):
|
|
"""Deklinacja jest fizyczna — nie zmienia się z wyborem zodiaku."""
|
|
tropical = build_chart(own_engine, krakow, zodiac="tropical")
|
|
sidereal = build_chart(own_engine, krakow, zodiac="sidereal_lahiri")
|
|
dt = {p["name"]: p["declination"] for p in tropical["positions"]}
|
|
ds = {p["name"]: p["declination"] for p in sidereal["positions"]}
|
|
for name in dt:
|
|
assert dt[name] == pytest.approx(ds[name], abs=1e-6)
|