feat: wiele systemów domów naraz — porównanie obok siebie (PRE-05/LOG-05, A2a)
build / build (push) Successful in 1m3s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m7s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m55s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 36s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
build / build (push) Successful in 1m3s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m7s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m55s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 36s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
Astrolodzy spierają się o systemy domów; teraz można policzyć kilka na raz i zobaczyć, jak różny podział przesuwa planety między domami. Domy to geometria z Asc/MC — osie są WSPÓLNE, różni się tylko podział. Prymarny system zostaje w `cusps`/`house_system`/`positions[].house` (pod kosmogram i wstecznie — nic się nie zmienia dla dotychczasowych ścieżek). Nowość: - logika: `build_chart(..., house_systems=[...])` → `result["house_systems"]` = pełen zestaw (prymarny pierwszy, bez duplikatów), a `positions[].houses[system]` mówi, w którym domu obiekt siedzi wg każdego systemu. Doklejane tylko gdy > 1. Nieznany system (np. placidus — dojdzie przez swisseph osobno, A2b) pomijany, nie wywala horoskopu. - endpoint `/chart/positions`: pole `house_systems`; klient prezentacji przekazuje. - UI: checkboxy „Porównaj systemy domów" (whole sign / equal / porphyry) + tabela kusp obok siebie (12 domów × systemy), stan zaznaczeń przeżywa submit. Na razie 3 systemy z czystej matmy (`houses.py`) — zero zależności, zero walidacji krzyżowej. Egzotyczne (Placidus/Koch/Regiomontanus/Campanus) dojdą przez endpoint `/houses` w silniku B (swisseph) jako A2b — maszyneria „naraz" jest już gotowa, egzotyczne tylko dopiszą kolejne wpisy. Weryfikacja: żywy /chart/positions — prymarny whole_sign, house_systems [whole_sign, equal, porphyry], 12 kusp/system, dom per system per obiekt. Testy: logika +5, prezentacja +6. Logika 270, prezentacja 179. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #44.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
<label><input type="checkbox" name="tables" value="true" {{ 'checked' if form.tables else '' }}>
|
||||
tabele dodatkowe: żywioły, faza Księżyca, godziny planetarne (wolniejsze)</label>
|
||||
</div>
|
||||
<div class="opts" title="Domy dzielą to samo niebo inaczej. Zaznacz kilka, by porównać kuspy obok siebie (PRE-05). Kosmogram rysuje system wybrany wyżej.">
|
||||
{% set chosen = form.house_systems or [] %}
|
||||
<span class="muted small">Porównaj systemy domów:</span>
|
||||
<label><input type="checkbox" name="house_systems" value="whole_sign" {{ 'checked' if 'whole_sign' in chosen else '' }}> Whole Sign</label>
|
||||
<label><input type="checkbox" name="house_systems" value="equal" {{ 'checked' if 'equal' in chosen else '' }}> Equal</label>
|
||||
<label><input type="checkbox" name="house_systems" value="porphyry" {{ 'checked' if 'porphyry' in chosen else '' }}> Porphyry</label>
|
||||
</div>
|
||||
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
|
||||
<div class="actions">
|
||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||
@@ -104,6 +111,24 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{# Porównanie systemów domów (PRE-05) — te same osie, inny podział na domy #}
|
||||
{% if result.house_systems and result.house_systems | length > 1 %}
|
||||
{% set HSN = {'whole_sign': 'Whole Sign', 'equal': 'Equal', 'porphyry': 'Porphyry'} %}
|
||||
<div class="meta" title="Osie (Asc/MC) są wspólne — różni się podział na domy. Kosmogram rysuje pierwszy z listy.">Porównanie systemów domów ({{ result.house_systems | length }})</div>
|
||||
<table class="angles">
|
||||
<thead><tr><th>Dom</th>{% for hs in result.house_systems %}<th>{{ HSN.get(hs.system, hs.system) }}</th>{% endfor %}</tr></thead>
|
||||
<tbody>
|
||||
{% for i in range(12) %}
|
||||
<tr><td>{{ i + 1 }}</td>
|
||||
{% for hs in result.house_systems %}
|
||||
<td class="mono"><span class="glyph">{{ hs.cusps[i].sign_glyph }}</span> {{ hs.cusps[i].in_sign }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Porównanie systemów domów naraz (PRE-05).
|
||||
|
||||
JS-a tu nie ma — sprawdzamy strukturalnie: formularz pozwala zaznaczyć kilka
|
||||
systemów, klient przekazuje je do logiki, a szablon pokazuje kuspy obok siebie.
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8")
|
||||
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_form_lets_you_pick_extra_systems():
|
||||
"""Checkboxy dla trzech systemów — wielokrotny wybór (name powtórzony)."""
|
||||
for val in ("whole_sign", "equal", "porphyry"):
|
||||
assert f'name="house_systems" value="{val}"' in CHART, f"brak checkboxa {val}"
|
||||
|
||||
|
||||
def test_handler_accepts_and_passes_house_systems():
|
||||
assert "house_systems: list[str] = Form([])" in MAIN
|
||||
assert "house_systems=house_systems" in MAIN # przekazane do logiki
|
||||
|
||||
|
||||
def test_client_forwards_house_systems_to_logic():
|
||||
assert '"house_systems": house_systems' in CLIENT
|
||||
|
||||
|
||||
def test_comparison_table_renders_only_for_multiple_systems():
|
||||
"""Bez dodatkowych systemów nie pokazujemy pustej tabeli porównania."""
|
||||
assert "result.house_systems | length > 1" in CHART
|
||||
assert "Porównanie systemów domów" in CHART
|
||||
|
||||
|
||||
def test_comparison_table_shows_cusp_per_system():
|
||||
"""Tabela iteruje po systemach i po 12 domach — kusp w każdej komórce."""
|
||||
assert "for hs in result.house_systems" in CHART
|
||||
assert "hs.cusps[i].in_sign" in CHART
|
||||
|
||||
|
||||
def test_checkbox_state_survives_submit():
|
||||
"""Zaznaczone systemy zostają zaznaczone po przeliczeniu."""
|
||||
assert "in chosen" in CHART and "form.house_systems" in CHART
|
||||
Reference in New Issue
Block a user