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>
141 lines
5.9 KiB
Python
141 lines
5.9 KiB
Python
"""Aspekty pozazodiakalne (LOG-07): paralele deklinacji i antyscja.
|
||
|
||
Aspekty głowne (LOG-06) mierzą kąt wzdłuż EKLIPTYKI. Ale dwa ciała mogą być
|
||
powiązane też inaczej — a te powiązania klasyczna astrologia liczy naprawdę,
|
||
nie na oko:
|
||
|
||
* **Paralela / kontrparalela deklinacji.** Deklinacja to „szerokość" na równiku
|
||
niebieskim — jak daleko na północ/południe od równika stoi ciało. Dwa ciała na
|
||
tej samej deklinacji (parallel) działają jak koniunkcja, na przeciwnej
|
||
(kontrparalela) — jak opozycja, mimo że wzdłuż ekliptyki mogą być gdziekolwiek.
|
||
Baza interpretacyjna zna to zjawisko pod skrótem „P. Dec.".
|
||
|
||
* **Antyscja / kontrantyscja.** Odbicie punktu względem osi przesileń
|
||
(0° Raka – 0° Koziorożca). Dwa punkty w antyscji są równo odległe od tej osi —
|
||
„dzielą" tę samą długość dnia. Kontrantyscja to odbicie względem osi
|
||
równonocy (0° Barana – 0° Wagi).
|
||
|
||
Wszystko liczymy na współrzędnych TROPIKALNYCH of-date, bo:
|
||
- deklinacja jest wielkością fizyczną (równikową), niezależną od wyboru zodiaku;
|
||
- antyscja jest z definicji tropikalna — jej oś to punkty przesileń, czyli
|
||
kardynalne punkty zodiaku tropikalnego.
|
||
Dlatego moduł bierze surowe długości/szerokości z silnika, a nie etykiety po
|
||
przesunięciu na zodiak syderyczny/draconiczny.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from app.engine.aspects import LUMINARIES, RIGID_PAIRS, separation
|
||
from app.engine.formats import norm360
|
||
from app.engine.zodiac import to_equatorial
|
||
|
||
# Orby — konfigurowalne (wymóg LOG-07). Paralele i antyscja są klasycznie CIASNE:
|
||
# to kontakty „punktowe", więc szeroki orb produkowałby fałszywe trafienia.
|
||
DECL_ORB = 1.0 # ° deklinacji dla paraleli/kontrparaleli
|
||
ANTISCIA_ORB = 1.0 # ° długości dla antyscji/kontrantyscji
|
||
LUMINARY_BONUS = 0.0 # świadomie 0 — te kontakty trzymamy ciasno; do podniesienia w API
|
||
|
||
|
||
def _is_rigid(name_a: str, name_b: str) -> bool:
|
||
return frozenset({name_a, name_b}) in RIGID_PAIRS
|
||
|
||
|
||
def declination(lon: float, lat: float, eps: float) -> float:
|
||
"""Deklinacja [−90, 90]° z długości i szerokości ekliptycznej (pełny wzór,
|
||
z szerokością — istotne dla Księżyca i planet, które schodzą z ekliptyki)."""
|
||
_, dec = to_equatorial(lon, lat, eps)
|
||
return dec
|
||
|
||
|
||
def is_out_of_bounds(dec: float, eps: float) -> bool:
|
||
"""Deklinacja poza zakresem Słońca (|dec| > nachylenie ekliptyki).
|
||
|
||
„Out of bounds" — ciało zaszło dalej na północ/południe, niż Słońce kiedykolwiek
|
||
potrafi. Astrologicznie czytane jako działanie „poza normą", stąd wart odnotowania."""
|
||
return abs(dec) > eps
|
||
|
||
|
||
def antiscion(lon: float) -> float:
|
||
"""Odbicie długości względem osi przesileń (0° Raka – 0° Koziorożca)."""
|
||
return norm360(180.0 - lon)
|
||
|
||
|
||
def contra_antiscion(lon: float) -> float:
|
||
"""Odbicie długości względem osi równonocy (0° Barana – 0° Wagi)."""
|
||
return norm360(-lon)
|
||
|
||
|
||
def _allowed(orb: float, name_a: str, name_b: str, bonus: float) -> float:
|
||
if bonus and (name_a in LUMINARIES or name_b in LUMINARIES):
|
||
return orb + bonus
|
||
return orb
|
||
|
||
|
||
def find_declination_aspects(
|
||
bodies: list[dict], orb: float = DECL_ORB, luminary_bonus: float = LUMINARY_BONUS
|
||
) -> list[dict]:
|
||
"""bodies: dicty z 'name' i 'declination' (°).
|
||
|
||
Zwraca paralele (ta sama deklinacja) i kontrparalele (przeciwna). Pary z
|
||
RIGID_PAIRS pomijane — np. węzły są z definicji zawsze w kontrparaleli
|
||
(SN = NN+180 na ekliptyce → deklinacja przeciwna), co nie niesie informacji.
|
||
"""
|
||
out: list[dict] = []
|
||
n = len(bodies)
|
||
for i in range(n):
|
||
for j in range(i + 1, n):
|
||
a, b = bodies[i], bodies[j]
|
||
if _is_rigid(a["name"], b["name"]):
|
||
continue
|
||
da, db = a.get("declination"), b.get("declination")
|
||
if da is None or db is None:
|
||
continue
|
||
da, db = float(da), float(db)
|
||
allowed = _allowed(orb, a["name"], b["name"], luminary_bonus)
|
||
parallel_dev = abs(da - db)
|
||
contra_dev = abs(da + db)
|
||
# ciało może wpaść tylko w jeden z dwóch — bierzemy ciaśniejszy
|
||
if parallel_dev <= allowed and parallel_dev <= contra_dev:
|
||
out.append(_row("parallel", a, b, parallel_dev, allowed, da, db))
|
||
elif contra_dev <= allowed:
|
||
out.append(_row("contraparallel", a, b, contra_dev, allowed, da, db))
|
||
return out
|
||
|
||
|
||
def find_antiscia(
|
||
bodies: list[dict], orb: float = ANTISCIA_ORB, luminary_bonus: float = LUMINARY_BONUS
|
||
) -> list[dict]:
|
||
"""bodies: dicty z 'name' i 'decimal' (długość tropikalna of-date, °).
|
||
|
||
Zwraca antyscje (odbicie względem osi przesileń) i kontrantyscje (osi równonocy).
|
||
"""
|
||
out: list[dict] = []
|
||
n = len(bodies)
|
||
for i in range(n):
|
||
for j in range(i + 1, n):
|
||
a, b = bodies[i], bodies[j]
|
||
if _is_rigid(a["name"], b["name"]):
|
||
continue
|
||
la, lb = a.get("decimal"), b.get("decimal")
|
||
if la is None or lb is None:
|
||
continue
|
||
la, lb = float(la), float(lb)
|
||
allowed = _allowed(orb, a["name"], b["name"], luminary_bonus)
|
||
anti_dev = separation(la, antiscion(lb))
|
||
contra_dev = separation(la, contra_antiscion(lb))
|
||
if anti_dev <= allowed and anti_dev <= contra_dev:
|
||
out.append(_row("antiscion", a, b, anti_dev, allowed))
|
||
elif contra_dev <= allowed:
|
||
out.append(_row("contra_antiscion", a, b, contra_dev, allowed))
|
||
return out
|
||
|
||
|
||
def _row(kind: str, a: dict, b: dict, dev: float, allowed: float,
|
||
dec_a: float | None = None, dec_b: float | None = None) -> dict:
|
||
row = {
|
||
"obj1": a["name"], "obj2": b["name"],
|
||
"type": kind, "orb": round(dev, 3), "allowed": round(allowed, 3),
|
||
}
|
||
if dec_a is not None:
|
||
row["dec1"], row["dec2"] = round(dec_a, 3), round(dec_b, 3)
|
||
return row
|