From b36b3bee19888f1a03042cb93a82f625db720df8 Mon Sep 17 00:00:00 2001 From: migatu Date: Mon, 3 Aug 2026 13:16:32 +0200 Subject: [PATCH] feat: konfigurowalne aspekty i orby (PRE-06) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- services/logic/app/engine/aspects.py | 21 ++++++-- services/logic/app/engine/chart.py | 8 ++- services/logic/app/main.py | 8 ++- services/logic/tests/test_aspects.py | 25 ++++++++++ .../presentation/app/clients/logic_client.py | 6 +++ services/presentation/app/main.py | 23 +++++++-- services/presentation/app/static/compile.js | 2 + services/presentation/app/static/formsync.js | 5 +- .../presentation/app/templates/chart.html | 5 ++ .../presentation/app/templates/compile.html | 5 ++ .../tests/test_aspect_settings.py | 50 +++++++++++++++++++ 11 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 services/presentation/tests/test_aspect_settings.py diff --git a/services/logic/app/engine/aspects.py b/services/logic/app/engine/aspects.py index af9f7dc..1b8d389 100644 --- a/services/logic/app/engine/aspects.py +++ b/services/logic/app/engine/aspects.py @@ -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( diff --git a/services/logic/app/engine/chart.py b/services/logic/app/engine/chart.py index 6efbab7..b78656f 100644 --- a/services/logic/app/engine/chart.py +++ b/services/logic/app/engine/chart.py @@ -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), diff --git a/services/logic/app/main.py b/services/logic/app/main.py index 3d096a8..46b44c7 100644 --- a/services/logic/app/main.py +++ b/services/logic/app/main.py @@ -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: diff --git a/services/logic/tests/test_aspects.py b/services/logic/tests/test_aspects.py index 5db74c4..0b79d21 100644 --- a/services/logic/tests/test_aspects.py +++ b/services/logic/tests/test_aspects.py @@ -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 diff --git a/services/presentation/app/clients/logic_client.py b/services/presentation/app/clients/logic_client.py index b104b60..e3108de 100644 --- a/services/presentation/app/clients/logic_client.py +++ b/services/presentation/app/clients/logic_client.py @@ -55,6 +55,9 @@ class LogicClient: zodiac: str = "tropical", tables: bool = False, house_systems: list[str] | None = None, + aspect_orb: float = 8.0, + aspect_luminary_bonus: float = 2.0, + aspect_minor: bool = False, ) -> dict[str, Any]: """Pełny horoskop dla danego momentu — woła logic /chart/positions.""" payload = { @@ -64,6 +67,9 @@ class LogicClient: "objects": objects, "house_system": house_system, "house_systems": house_systems, + "aspect_orb": aspect_orb, + "aspect_luminary_bonus": aspect_luminary_bonus, + "aspect_minor": aspect_minor, "stations": stations, "zodiac": zodiac, "tables": tables, diff --git a/services/presentation/app/main.py b/services/presentation/app/main.py index 64807e0..2a85e88 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -106,14 +106,18 @@ def chart_compute( lon: float = Form(0.0), house_system: str = Form("whole_sign"), house_systems: list[str] = Form([]), # PRE-05: dodatkowe systemy do porównania + aspect_orb: float = Form(8.0), # PRE-06: konfigurowalny orb aspektów + aspect_luminary_bonus: float = Form(2.0), + aspect_minor: bool = Form(False), stations: bool = Form(False), zodiac: str = Form("tropical"), tables: bool = Form(False), ): form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon, "house_system": house_system, - "house_systems": house_systems, "stations": stations, - "zodiac": zodiac, "tables": tables} + "house_systems": house_systems, "aspect_orb": aspect_orb, + "aspect_luminary_bonus": aspect_luminary_bonus, "aspect_minor": aspect_minor, + "stations": stations, "zodiac": zodiac, "tables": tables} ctx: dict = {"form": form, "result": None, "error": None, "moment": None} try: iso_utc, label = _build_utc(date, time, tz_offset) @@ -121,6 +125,8 @@ def chart_compute( ctx["result"] = logic.positions( when_utc_iso=iso_utc, lat=lat, lon=lon, house_system=house_system, house_systems=house_systems, + aspect_orb=aspect_orb, aspect_luminary_bonus=aspect_luminary_bonus, + aspect_minor=aspect_minor, stations=stations, zodiac=zodiac, tables=tables, ) from app import chartwheel # kosmogram (PRE-12), SVG po stronie serwera @@ -155,6 +161,9 @@ def compile_build( lon: float = Form(0.0), house_system: str = Form("whole_sign"), house_systems: list[str] = Form([]), # PRE-05 — porównanie domów w podsumowaniu + aspect_orb: float = Form(8.0), # PRE-06 — te same ustawienia aspektów + aspect_luminary_bonus: float = Form(2.0), + aspect_minor: bool = Form(False), stations: bool = Form(False), # LOG-03 — stacje też w podsumowaniu zodiac: str = Form("tropical"), tables: bool = Form(False), # LOG-23 — żywioły/faza/godziny w podsumowaniu @@ -170,8 +179,9 @@ def compile_build( """ form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon, "house_system": house_system, - "house_systems": house_systems, "stations": stations, - "zodiac": zodiac, "tables": tables} + "house_systems": house_systems, "aspect_orb": aspect_orb, + "aspect_luminary_bonus": aspect_luminary_bonus, "aspect_minor": aspect_minor, + "stations": stations, "zodiac": zodiac, "tables": tables} ctx: dict = {"form": form, "result": None, "error": None, "moment": None} try: iso_utc, label = _build_utc(date, time, tz_offset) @@ -179,6 +189,8 @@ def compile_build( ctx["result"] = logic.positions( when_utc_iso=iso_utc, lat=lat, lon=lon, house_system=house_system, house_systems=house_systems, + aspect_orb=aspect_orb, aspect_luminary_bonus=aspect_luminary_bonus, + aspect_minor=aspect_minor, stations=stations, zodiac=zodiac, tables=tables, ) from app import chartwheel @@ -224,6 +236,9 @@ def compile_pdf(payload: dict): lat=float(data.get("lat") or 0.0), lon=float(data.get("lon") or 0.0), house_system=str(data.get("house_system") or "whole_sign"), house_systems=[s for s in (data.get("house_systems") or []) if s], + aspect_orb=float(data.get("aspect_orb") or 8.0), + aspect_luminary_bonus=float(data.get("aspect_luminary_bonus") or 2.0), + aspect_minor=bool(data.get("aspect_minor")), stations=bool(data.get("stations")), tables=bool(data.get("tables")), zodiac=str(data.get("zodiac") or "tropical"), ) diff --git a/services/presentation/app/static/compile.js b/services/presentation/app/static/compile.js index 7f89797..4395aa2 100644 --- a/services/presentation/app/static/compile.js +++ b/services/presentation/app/static/compile.js @@ -109,6 +109,8 @@ lat: v('lat'), lon: v('lon'), house_system: v('house_system'), zodiac: v('zodiac'), house_systems: multi('house_systems'), + aspect_orb: v('aspect_orb'), aspect_luminary_bonus: v('aspect_luminary_bonus'), + aspect_minor: checked('aspect_minor'), stations: checked('stations'), tables: checked('tables'), place: (document.querySelector('#geoSearch') || {}).value || '' }; diff --git a/services/presentation/app/static/formsync.js b/services/presentation/app/static/formsync.js index c1bdf51..68e28df 100644 --- a/services/presentation/app/static/formsync.js +++ b/services/presentation/app/static/formsync.js @@ -36,7 +36,10 @@ { key: 'place', sel: '#geoSearch' }, { key: 'stations', sel: 'input[name=stations]', type: 'check' }, { key: 'tables', sel: 'input[name=tables]', type: 'check' }, - { key: 'house_systems', name: 'house_systems', type: 'multi' } + { key: 'house_systems', name: 'house_systems', type: 'multi' }, + { key: 'aspect_orb', sel: 'input[name=aspect_orb]' }, + { key: 'aspect_luminary_bonus', sel: 'input[name=aspect_luminary_bonus]' }, + { key: 'aspect_minor', sel: 'input[name=aspect_minor]', type: 'check' } ]; function elementsOf(f) { diff --git a/services/presentation/app/templates/chart.html b/services/presentation/app/templates/chart.html index c97f738..599d926 100644 --- a/services/presentation/app/templates/chart.html +++ b/services/presentation/app/templates/chart.html @@ -64,6 +64,11 @@ +
+ + + +
{% if location_label %}

Wstępnie wpisano lokalizację: {{ location_label }} ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".

{% endif %}
diff --git a/services/presentation/app/templates/compile.html b/services/presentation/app/templates/compile.html index aade5de..f41945b 100644 --- a/services/presentation/app/templates/compile.html +++ b/services/presentation/app/templates/compile.html @@ -70,6 +70,11 @@ zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie t
+
+ + + +