feat: konfigurowalne aspekty i orby (PRE-06)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m47s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 12s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 9s
build / build (push) Successful in 26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m30s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 12s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m47s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 12s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 9s
build / build (push) Successful in 26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m30s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 12s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Dotąd orb (8°) i aspekty były zaszyte w silniku. Astrolodzy pracują na różnych orbach i lubią dokładać aspekty poboczne — teraz da się to ustawić w UI. Silnik (aspects.py): `find_aspects` przyjmuje orb, luminary_bonus i `minor`. Aspekty poboczne = tylko trzy (półsekstyl 30° / półkwadratura 45° / kwinkunks 150°) — bo mają już glify i barwy w prezentacji i w engine/glyphs.py, więc dokładają się bez ruszania czegokolwiek poza silnikiem. Bazy zwykle ich nie opisują (brak tokenu), więc trafiają na kosmogram i do tabeli, ale NIE tworzą faset — most sygnifikatorów po cichu je pomija (skip przy braku tokenu). Plumbing przez wszystkie warstwy: request logiki, build_chart, klient prezentacji, oba handlery (/, /compile) ORAZ PDF (/compile/pdf). Ustawienia wędrują między zakładkami (formsync) i lecą do PDF-a (compile.js) — żeby podsumowanie liczyło aspekty tym samym orbem co horoskop (ta sama zasada co fix podsumowania #46). UI: orb, bonus dla świateł, checkbox „aspekty poboczne" na obu formularzach. Weryfikacja na żywym /chart/positions: domyślnie 24 aspekty (główne); minor → 52 (dochodzą quincunx/semisextile/semisquare); orb 3 → 13, orb 12 → 32. Testy: logika +3 (minor/orb/bonus), prezentacja +6 (obecność pól, przekazanie przez warstwy, sync, PDF). Logika 273, prezentacja 220. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #49.
This commit is contained in:
@@ -15,6 +15,16 @@ MAJOR = {
|
||||
"trine": 120.0,
|
||||
"opposition": 180.0,
|
||||
}
|
||||
# Aspekty POBOCZNE (opcjonalne, PRE-06). Tylko te trzy — bo mają już glify i barwy
|
||||
# w warstwie prezentacji (chartwheel) oraz w engine/glyphs.py, więc dokładają się
|
||||
# bez ruszania czegokolwiek poza silnikiem. Bazy interpretacyjne zwykle ich nie
|
||||
# opisują (brak tokenu w DB_TOKEN), więc trafiają na kosmogram i do tabeli, ale
|
||||
# NIE generują faset sygnifikatorów — most po cichu je pomija (significators.py).
|
||||
MINOR = {
|
||||
"semisextile": 30.0,
|
||||
"semisquare": 45.0,
|
||||
"quincunx": 150.0,
|
||||
}
|
||||
DB_TOKEN = {
|
||||
"conjunction": "[conj", "sextile": "[sex", "square": "[sq",
|
||||
"trine": "[tri", "opposition": "[opp",
|
||||
@@ -62,15 +72,18 @@ def _is_applying(la: float, lb: float, sa: float, sb: float, angle: float, dt: f
|
||||
|
||||
|
||||
def find_aspects(
|
||||
positions: list[dict], orb: float = DEFAULT_ORB, luminary_bonus: float = LUMINARY_BONUS
|
||||
positions: list[dict], orb: float = DEFAULT_ORB, luminary_bonus: float = LUMINARY_BONUS,
|
||||
minor: bool = False,
|
||||
) -> list[dict]:
|
||||
"""positions: dicty z 'name', 'decimal' (długość) i opcjonalnie 'speed' (°/dobę).
|
||||
|
||||
Zwraca listę aspektów głównych; gdy znane są prędkości, każdy aspekt ma
|
||||
applying (bool) i skrót 'as': 'A'/'S' (aplikacyjny/separacyjny).
|
||||
Zwraca listę aspektów; gdy znane są prędkości, każdy aspekt ma applying (bool)
|
||||
i skrót 'as': 'A'/'S' (aplikacyjny/separacyjny). Orb i bonus dla świateł są
|
||||
KONFIGUROWALNE (PRE-06); `minor=True` dokłada aspekty poboczne (30/45/150°).
|
||||
|
||||
Pary z RIGID_PAIRS (np. NN/SN) są pomijane — ich kąt jest definicyjny.
|
||||
"""
|
||||
checks = {**MAJOR, **MINOR} if minor else MAJOR
|
||||
out: list[dict] = []
|
||||
n = len(positions)
|
||||
for i in range(n):
|
||||
@@ -83,7 +96,7 @@ def find_aspects(
|
||||
continue
|
||||
sep = separation(float(la), float(lb))
|
||||
allowed = orb + (luminary_bonus if (a["name"] in LUMINARIES or b["name"] in LUMINARIES) else 0.0)
|
||||
for asp, angle in MAJOR.items():
|
||||
for asp, angle in checks.items():
|
||||
dev = abs(sep - angle)
|
||||
if dev <= allowed:
|
||||
applying = _is_applying(
|
||||
|
||||
@@ -36,7 +36,9 @@ def _shift_pos(pdict: dict, off: float) -> None:
|
||||
|
||||
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN,
|
||||
lots_method: str = "degree", zodiac: str = Z.TROPICAL,
|
||||
house_systems: list[str] | None = None) -> dict:
|
||||
house_systems: list[str] | None = None,
|
||||
aspect_orb: float = 8.0, aspect_luminary_bonus: float = 2.0,
|
||||
aspect_minor: bool = False) -> dict:
|
||||
from app.engine import glyphs as GL
|
||||
from app.engine.aspects import find_aspects
|
||||
from app.engine.houses import mean_obliquity
|
||||
@@ -50,7 +52,9 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
||||
positions = engine.positions(moment)
|
||||
result: dict = {"engine": engine.name, "positions": [p.as_dict() for p in positions]}
|
||||
# aspekty liczymy PRZED zmianą zodiaku — kąty między obiektami są niezmiennicze
|
||||
result["aspects"] = find_aspects(result["positions"]) # aspekty (LOG-06)
|
||||
result["aspects"] = find_aspects( # aspekty (LOG-06), konfigurowalne (PRE-06)
|
||||
result["positions"], orb=aspect_orb,
|
||||
luminary_bonus=aspect_luminary_bonus, minor=aspect_minor)
|
||||
|
||||
# Aspekty pozazodiakalne (LOG-07): deklinacja i antyscja liczone na
|
||||
# współrzędnych TROPIKALNYCH of-date — deklinacja jest fizyczna (równikowa),
|
||||
|
||||
@@ -45,6 +45,9 @@ class PositionsRequest(BaseModel):
|
||||
objects: list[str] | None = None
|
||||
house_system: str = "whole_sign" # whole_sign | equal | porphyry (PRYMARNY — pod kosmogram)
|
||||
house_systems: list[str] | None = None # PRE-05: dodatkowe systemy do porównania obok
|
||||
aspect_orb: float = 8.0 # PRE-06: orb aspektów (konfigurowalny)
|
||||
aspect_luminary_bonus: float = 2.0 # PRE-06: dodatkowy orb dla Słońca/Księżyca
|
||||
aspect_minor: bool = False # PRE-06: dołóż aspekty poboczne (30/45/150°)
|
||||
stations: bool = False # licz stacje (LOG-03; wolniejsze — root-findy)
|
||||
tables: bool = False # tabele dodatkowe (LOG-23; szuka wschodu/zachodu)
|
||||
zodiac: str = "tropical" # LOG-04: tropical | sidereal_{lahiri,fagan_bradley,krishnamurti} | draconic
|
||||
@@ -69,7 +72,10 @@ def chart_positions(req: PositionsRequest) -> dict:
|
||||
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||
try:
|
||||
chart = build_chart(engine, moment, req.house_system, zodiac=req.zodiac,
|
||||
house_systems=req.house_systems)
|
||||
house_systems=req.house_systems,
|
||||
aspect_orb=req.aspect_orb,
|
||||
aspect_luminary_bonus=req.aspect_luminary_bonus,
|
||||
aspect_minor=req.aspect_minor)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
if req.stations:
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
from app.engine.aspects import RIGID_PAIRS, find_aspects, separation
|
||||
|
||||
|
||||
# ── konfigurowalne aspekty i orby (PRE-06) ──
|
||||
|
||||
def test_minor_aspects_only_with_flag():
|
||||
"""Domyślnie tylko główne; `minor=True` dokłada poboczne (30/45/150°)."""
|
||||
pos = [{"name": "A", "decimal": 10.0}, {"name": "B", "decimal": 40.0}] # 30° = półsekstyl
|
||||
assert find_aspects(pos) == [] # główne: brak
|
||||
assert [a["aspect"] for a in find_aspects(pos, minor=True)] == ["semisextile"]
|
||||
|
||||
|
||||
def test_orb_is_configurable():
|
||||
"""Węższy orb odsiewa aspekt, który szerszy łapie."""
|
||||
pos = [{"name": "A", "decimal": 10.0}, {"name": "B", "decimal": 78.0}] # 68° = sextile dev 8°
|
||||
assert find_aspects(pos, orb=5.0) == []
|
||||
assert [a["aspect"] for a in find_aspects(pos, orb=10.0)] == ["sextile"]
|
||||
|
||||
|
||||
def test_luminary_bonus_widens_orb_for_lights():
|
||||
"""Bonus dolicza orb tylko gdy w parze jest Słońce/Księżyc."""
|
||||
lights = [{"name": "Sun", "decimal": 10.0}, {"name": "Mars", "decimal": 79.0}] # dev 9° od sextile
|
||||
assert find_aspects(lights, orb=8.0, luminary_bonus=0.0) == [] # 9 > 8
|
||||
assert [a["aspect"] for a in find_aspects(lights, orb=8.0, luminary_bonus=2.0)] == ["sextile"] # 9 < 10
|
||||
plain = [{"name": "Venus", "decimal": 10.0}, {"name": "Mars", "decimal": 79.0}]
|
||||
assert find_aspects(plain, orb=8.0, luminary_bonus=2.0) == [] # brak światła → bez bonusu
|
||||
|
||||
|
||||
def test_separation_wraparound():
|
||||
assert separation(10, 350) == 20
|
||||
assert separation(0, 180) == 180
|
||||
|
||||
Reference in New Issue
Block a user