diff --git a/services/logic/app/engine/chart.py b/services/logic/app/engine/chart.py index a5658a8..6efbab7 100644 --- a/services/logic/app/engine/chart.py +++ b/services/logic/app/engine/chart.py @@ -35,7 +35,8 @@ 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) -> dict: + lots_method: str = "degree", zodiac: str = Z.TROPICAL, + house_systems: list[str] | None = None) -> dict: from app.engine import glyphs as GL from app.engine.aspects import find_aspects from app.engine.houses import mean_obliquity @@ -94,6 +95,16 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str system = house_system if house_system in H.SYSTEMS else H.WHOLE_SIGN cusp_list = H.cusps(asc, mc, system) # tropikalne — geometria domów jest niezmiennicza + def _cusps_out(cl: list[float]) -> list[dict]: + """Cuspy → wiersze pod UI/kosmogram: znak, stopień w znaku, długość, glif.""" + return [ + {"house": i + 1, "sign": SIGNS[sign_index(norm360(c - off))], + "in_sign": in_sign(norm360(c - off)), + "decimal": round(norm360(c - off), 6), # długość cuspu — pod kosmogram (PRE-12) + "sign_glyph": GL.sign_glyph(SIGNS[sign_index(norm360(c - off))])} + for i, c in enumerate(cl) + ] + result["house_system"] = system result["angles"] = { "Asc": _fmt("Asc", asc, off), @@ -103,16 +114,25 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str } for a in result["angles"].values(): # glif znaku osi (LOG-22) a["sign_glyph"] = GL.sign_glyph(a["sign"]) - result["cusps"] = [ - {"house": i + 1, "sign": SIGNS[sign_index(norm360(c - off))], - "in_sign": in_sign(norm360(c - off)), - "decimal": round(norm360(c - off), 6), # długość cuspu — pod kosmogram (PRE-12) - "sign_glyph": GL.sign_glyph(SIGNS[sign_index(norm360(c - off))])} - for i, c in enumerate(cusp_list) - ] + result["cusps"] = _cusps_out(cusp_list) # PRYMARNY system — pod kosmogram i wstecz for pdict, obj in zip(result["positions"], positions): pdict["house"] = H.assign_house(obj.longitude, cusp_list) # dom po długości tropikalnej + # Wiele systemów domów NARAZ (PRE-05/LOG-05) — do porównania obok siebie. + # Osie (Asc/MC) są wspólne; różni się tylko PODZIAŁ na domy. Prymarny zostaje + # w `cusps`/`house_system` (kosmogram i wstecz), a `house_systems` niesie pełen + # zestaw; `positions[].houses[system]` mówi, w którym domu obiekt siedzi wg + # danego systemu. Dokładamy tylko gdy poproszono o więcej niż jeden. + requested = [system] + [s for s in (house_systems or []) if s in H.SYSTEMS] + ordered = list(dict.fromkeys(requested)) # prymarny pierwszy, bez duplikatów + if len(ordered) > 1: + result["house_systems"] = [] + for s in ordered: + cl = cusp_list if s == system else H.cusps(asc, mc, s) + result["house_systems"].append({"system": s, "cusps": _cusps_out(cl)}) + for pdict, obj in zip(result["positions"], positions): + pdict.setdefault("houses", {})[s] = H.assign_house(obj.longitude, cl) + # Lots (LOG-08) — wymagają Asc i sekty (dzień/noc) from app.engine.firdaria import is_day_birth from app.engine.lots import compute_lots diff --git a/services/logic/app/main.py b/services/logic/app/main.py index f11a641..3d096a8 100644 --- a/services/logic/app/main.py +++ b/services/logic/app/main.py @@ -43,7 +43,8 @@ class PositionsRequest(BaseModel): lat: float = 0.0 lon: float = 0.0 objects: list[str] | None = None - house_system: str = "whole_sign" # whole_sign | equal | porphyry + 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 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 @@ -67,7 +68,8 @@ def chart_positions(req: PositionsRequest) -> dict: engine = get_engine() 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) + chart = build_chart(engine, moment, req.house_system, zodiac=req.zodiac, + house_systems=req.house_systems) except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) if req.stations: diff --git a/services/logic/tests/test_chart.py b/services/logic/tests/test_chart.py index bbc9f39..5cd4180 100644 --- a/services/logic/tests/test_chart.py +++ b/services/logic/tests/test_chart.py @@ -25,3 +25,44 @@ def test_house_systems_available(own_engine, reference_moment): chart = build_chart(own_engine, reference_moment, system) assert chart["house_system"] == system assert len(chart["cusps"]) == 12 + + +# ── wiele systemów domów naraz (PRE-05/LOG-05) ── + +def test_multiple_house_systems_side_by_side(own_engine, reference_moment): + """Prymarny zostaje w cusps/house_system; `house_systems` niesie pełen zestaw + do porównania, z prymarnym NA POCZĄTKU i bez duplikatów.""" + chart = build_chart(own_engine, reference_moment, "whole_sign", + house_systems=["equal", "porphyry", "whole_sign"]) + assert chart["house_system"] == "whole_sign" # prymarny bez zmian + assert [h["system"] for h in chart["house_systems"]] == ["whole_sign", "equal", "porphyry"] + for h in chart["house_systems"]: + assert len(h["cusps"]) == 12 + + +def test_single_system_has_no_comparison_block(own_engine, reference_moment): + """Bez dodatkowych systemów nie zaśmiecamy wyniku.""" + assert "house_systems" not in build_chart(own_engine, reference_moment, "whole_sign") + + +def test_object_gets_a_house_in_every_system(own_engine, reference_moment): + """Sedno porównania: każdy obiekt ma dom w KAŻDYM systemie, a prymarny jest + spójny z polem `.house`.""" + chart = build_chart(own_engine, reference_moment, "whole_sign", house_systems=["porphyry"]) + sun = next(p for p in chart["positions"] if p["name"] == "Sun") + assert set(sun["houses"]) == {"whole_sign", "porphyry"} + assert sun["houses"]["whole_sign"] == sun["house"] + + +def test_primary_always_first_even_if_not_listed(own_engine, reference_moment): + chart = build_chart(own_engine, reference_moment, "equal", house_systems=["porphyry"]) + systems = [h["system"] for h in chart["house_systems"]] + assert systems[0] == "equal" and "porphyry" in systems + + +def test_unknown_extra_system_is_ignored(own_engine, reference_moment): + """Nieobsługiwany system (np. placidus — dojdzie przez swisseph osobno) jest + po prostu pomijany, nie wywala horoskopu.""" + chart = build_chart(own_engine, reference_moment, "whole_sign", + house_systems=["placidus", "equal"]) + assert [h["system"] for h in chart["house_systems"]] == ["whole_sign", "equal"] diff --git a/services/presentation/app/clients/logic_client.py b/services/presentation/app/clients/logic_client.py index 17ad5c8..b104b60 100644 --- a/services/presentation/app/clients/logic_client.py +++ b/services/presentation/app/clients/logic_client.py @@ -54,6 +54,7 @@ class LogicClient: stations: bool = False, zodiac: str = "tropical", tables: bool = False, + house_systems: list[str] | None = None, ) -> dict[str, Any]: """Pełny horoskop dla danego momentu — woła logic /chart/positions.""" payload = { @@ -62,6 +63,7 @@ class LogicClient: "lon": lon, "objects": objects, "house_system": house_system, + "house_systems": house_systems, "stations": stations, "zodiac": zodiac, "tables": tables, diff --git a/services/presentation/app/main.py b/services/presentation/app/main.py index 6d6b524..5421fb8 100644 --- a/services/presentation/app/main.py +++ b/services/presentation/app/main.py @@ -79,12 +79,14 @@ def chart_compute( lat: float = Form(0.0), lon: float = Form(0.0), house_system: str = Form("whole_sign"), + house_systems: list[str] = Form([]), # PRE-05: dodatkowe systemy do porównania 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, "stations": stations, + "lat": lat, "lon": lon, "house_system": house_system, + "house_systems": house_systems, "stations": stations, "zodiac": zodiac, "tables": tables} ctx: dict = {"form": form, "result": None, "error": None, "moment": None} try: @@ -92,7 +94,8 @@ def chart_compute( ctx["moment"] = label ctx["result"] = logic.positions( when_utc_iso=iso_utc, lat=lat, lon=lon, - house_system=house_system, stations=stations, zodiac=zodiac, tables=tables, + house_system=house_system, house_systems=house_systems, + stations=stations, zodiac=zodiac, tables=tables, ) from app import chartwheel # kosmogram (PRE-12), SVG po stronie serwera ctx["wheel_svg"] = chartwheel.render(ctx["result"]) diff --git a/services/presentation/app/templates/chart.html b/services/presentation/app/templates/chart.html index 784edec..6ef6549 100644 --- a/services/presentation/app/templates/chart.html +++ b/services/presentation/app/templates/chart.html @@ -57,6 +57,13 @@ +
Wstępnie wpisano lokalizację: {{ location_label }} ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".
{% endif %}| Dom | {% for hs in result.house_systems %}{{ HSN.get(hs.system, hs.system) }} | {% endfor %}
|---|---|
| {{ i + 1 }} | + {% for hs in result.house_systems %} +{{ hs.cusps[i].sign_glyph }} {{ hs.cusps[i].in_sign }} | + {% endfor %} +