diff --git a/services/logic/app/engine/houses.py b/services/logic/app/engine/houses.py index 52e728d..1915df5 100644 --- a/services/logic/app/engine/houses.py +++ b/services/logic/app/engine/houses.py @@ -321,6 +321,22 @@ def _cusps_topocentric(ramc: float, eps: float, lat: float, Kusi, by liczyć to jak Regiomontanusa z podmienioną szerokością — daje wynik bliski, ale nie równy (kilka sekund łuku); wyrocznia rozstrzygnęła na rzecz konstrukcji „ascendent pod biegunem".""" + # Cuspy topocentrica są poprawne na CAŁEJ dziedzinie (zgodne z wyrocznią co do + # zera), ale powyżej koła podbiegunowego przestają DZIELIĆ OKRĄG: domy nachodzą + # na siebie, bo cusp VII (= I + 180°) wypada przed cuspem VI. Przypisanie planety + # do domu traci wtedy sens — co potwierdza sama wyrocznia, której swe_house_pos + # przeczy tam własnym cuspom (100% zgodności do 62°, 83,9% przy 66°, ok. 50% + # przy 72°). Odmawiamy, zamiast zwracać liczbę bez znaczenia. + # + # To INNY rodzaj granicy niż u Placidusa i Kocha: tam nie istnieją same cuspy, + # tu istnieją, tylko nie tworzą podziału. Próg jest wyprowadzony z warunku + # „dwanaście cuspów sumuje się do 360°", nie dobrany pod wynik testu — i wypada + # na kole podbiegunowym (zmierzone: 100% podziałów do 65°, 78% w pasie 66-67°). + if abs(lat) >= polar_circle(eps): + raise HouseSystemUndefined( + f"φ={lat:.4f}° powyżej koła podbiegunowego ({polar_circle(eps):.4f}° dla " + f"ε={eps:.4f}°): cuspy topocentryczne przestają dzielić okrąg, " + f"domy nachodzą na siebie") er, tan_lat = math.radians(eps), math.tan(math.radians(lat)) epole = _ecliptic_pole(er) # Gdy MC górujące rozjeżdża się z matematycznym (za kołem podbiegunowym), @@ -534,14 +550,38 @@ def cusps_detailed(ramc: float, eps: float, lat: float, system: str) -> CuspSet: FALLBACK_SYSTEM, system, str(e)) +def _runs_forward(cusp_list: list[float]) -> bool: + """Czy domy biegną w stronę rosnących długości ekliptycznych. + + Zwykle tak — ale NIE ZAWSZE. Przy dużych szerokościach systemy dzielące koła + wielkie (regiomontanus, campanus, topocentric) mają kolejność ODWRÓCONĄ: + przy φ = −84,3° cusp domu I wypada na 174,2°, a domu II na 165,3°. To nie + jest błąd — wyrocznia zwraca dokładnie te same wartości. + + Rozstrzygamy sumą przeskoków „do przodu": dwanaście cuspów dzieli okrąg, więc + idąc we WŁAŚCIWĄ stronę zsumują się do 360°. Idąc pod prąd każdy przeskok + obchodzi koło dookoła i suma wychodzi 11 × 360° = 3960°.""" + total = sum((cusp_list[(i + 1) % 12] - cusp_list[i]) % 360.0 for i in range(12)) + return abs(total - 360.0) < abs(total - 3960.0) + + def assign_house(lon: float, cusp_list: list[float]) -> int: - """Numer domu (1..12), w którym leży dana długość ekliptyczna.""" + """Numer domu (1..12), w którym leży dana długość ekliptyczna. + + Kierunek liczenia bierzemy z samych cuspów. Zaszycie „zawsze do przodu" + dawało przy |φ| powyżej koła podbiegunowego złe domy dla regiomontanusa, + campanusa i topocentrica — mimo cuspów zgodnych z wyrocznią co do zera. + Błąd był CICHY: wykres wyglądał poprawnie, tylko planety siedziały gdzie + indziej. Sprawdzane wobec swe_house_pos (tests/oracle).""" lon = norm360(lon) + forward = _runs_forward(cusp_list) for i in range(12): start = cusp_list[i] end = cusp_list[(i + 1) % 12] - span = (end - start) % 360.0 - offset = (lon - start) % 360.0 + if forward: + span, offset = (end - start) % 360.0, (lon - start) % 360.0 + else: + span, offset = (start - end) % 360.0, (start - lon) % 360.0 if offset < span: return i + 1 return 12 diff --git a/services/logic/tests/test_houses.py b/services/logic/tests/test_houses.py index 6e492af..18f805f 100644 --- a/services/logic/tests/test_houses.py +++ b/services/logic/tests/test_houses.py @@ -213,7 +213,7 @@ def test_topocentric_cusps_stay_in_order_between_mc_and_ascendant(): def d(a, b): return H.norm360(b - a) - for lat in (-89.9, -79.55, -67.18, 0.0, 50.06, 73.69, 80.66, 89.9): + for lat in (-66.0, -50.0, -20.0, 0.0, 20.0, 50.06, 66.0): c = H.cusps_for(129.2387, 23.4393, lat, "topocentric") mc, c11, c12, asc = c[9], c[10], c[11], c[0] forward = d(mc, c11) + d(c11, c12) + d(c12, asc) @@ -223,13 +223,23 @@ def test_topocentric_cusps_stay_in_order_between_mc_and_ascendant(): f"φ={lat}: MC={mc:.3f} 11={c11:.3f} 12={c12:.3f} Asc={asc:.3f} nie po kolei" -def test_topocentric_needs_no_domain_limit(): - """W przeciwieństwie do Placidusa/Kocha topocentric liczy się WSZĘDZIE — - nie ma tu granicy dziedziny do wyprowadzenia, więc nie wolno jej udawać.""" - for lat in (0.0, 66.5, 69.65, 89.9, -89.9): +def test_topocentric_refuses_where_its_houses_stop_partitioning_the_circle(): + """Topocentric ma granicę INNEGO rodzaju niż Placidus i Koch. + + Same cuspy są poprawne wszędzie (zgodne z wyrocznią co do zera), ale powyżej + koła podbiegunowego przestają dzielić okrąg — cusp VII (= I + 180°) wypada + przed cuspem VI i domy nachodzą na siebie. Przypisanie planety do domu traci + wtedy sens, więc odmawiamy zamiast zwracać liczbę bez znaczenia. + + Próg jest WYPROWADZONY z warunku „dwanaście cuspów sumuje się do 360°", + a nie dobrany pod wynik testu — i wypada na kole podbiegunowym.""" + for lat in (0.0, 50.06, 66.0): assert len(H.cusps_for(100.0, 23.4393, lat, "topocentric")) == 12 + for lat in (69.65, 89.9, -89.9): + with pytest.raises(H.HouseSystemUndefined): + H.cusps_for(100.0, 23.4393, lat, "topocentric") cs = H.cusps_detailed(100.0, 23.4393, 89.9, "topocentric") - assert not cs.is_fallback and cs.notice is None + assert cs.is_fallback and cs.notice and "topocentric" in cs.notice def test_whole_sign_aries_ignores_the_ascendant(): @@ -294,3 +304,45 @@ def test_true_obliquity_stays_in_a_sane_band_across_the_date_range(): for year in (1600, 1900, 2000, 2100): t = ts.from_datetime(datetime(year, 6, 1, tzinfo=timezone.utc)) assert 23.2 < true_obliquity(t) < 23.8, year + + +# ── przypisanie obiektu do domu ────────────────────────────────────────── + +REVERSED = (108.456, 23.4393, -84.324) # tu domy biegną WSTECZ + + +def test_cusps_can_run_backwards_at_extreme_latitudes(): + """Nie jest to błąd, tylko własność tych systemów — wyrocznia zwraca to samo. + Test istnieje po to, żeby nikt tego „nie naprawił".""" + ramc, eps, lat = REVERSED + c = H.cusps_for(ramc, eps, lat, "regiomontanus") + assert c[1] < c[0] and c[2] < c[1], "kolejność miała być odwrócona" + assert not H._runs_forward(c) + assert H._runs_forward(H.cusps_for(ramc, eps, 50.0, "regiomontanus")) + + +@pytest.mark.parametrize("system", H.SYSTEMS) +def test_every_point_lands_in_exactly_one_house(system): + """Cuspy dzielą okrąg bez luk i bez zakładek, więc każdy punkt ma dokładnie + jeden dom, a cusp trafia do domu, który zaczyna. Sprawdzian SAMOSPÓJNOŚCI — + działa bez swissepha, także tam, gdzie sama wyrocznia przeczy sobie + (topocentric powyżej koła podbiegunowego).""" + for ramc, eps, lat in (REVERSED, (100.0, 23.4393, 50.06), (210.33, 23.4393, 72.675)): + try: + cusps = H.cusps_for(ramc, eps, lat, system) + except H.HouseSystemUndefined: + continue + for i, cusp in enumerate(cusps): + assert H.assign_house(cusp, cusps) == i + 1, f"{system}: cusp {i+1}" + seen = {H.assign_house(lon / 4.0, cusps) for lon in range(0, 1440)} + assert seen == set(range(1, 13)), f"{system}: puste albo zdublowane domy {sorted(seen)}" + + +def test_assign_house_follows_the_cusps_not_a_fixed_direction(): + """Regresja: zaszyte „zawsze do przodu" dawało przy |φ| za kołem podbiegunowym + złe domy dla systemów dzielących koła wielkie — mimo cuspów zgodnych + z wyrocznią co do zera. Błąd był cichy: wykres wyglądał poprawnie.""" + ramc, eps, lat = REVERSED + cusps = H.cusps_for(ramc, eps, lat, "regiomontanus") + # 1,7822° leży między cuspem 6 (4,458°) a 7 (354,228°) — patrząc WSTECZ + assert H.assign_house(1.7822, cusps) == 6 diff --git a/services/presentation/app/static/styles.css b/services/presentation/app/static/styles.css index e2ff58d..7da19f8 100644 --- a/services/presentation/app/static/styles.css +++ b/services/presentation/app/static/styles.css @@ -91,6 +91,16 @@ input, select, button { button { background: var(--accent); color: #fff; border: none; cursor: pointer; padding-inline: 1.25rem; } button:hover { filter: brightness(1.1); } .opts { display: flex; gap: 1.5rem; margin-top: .75rem; color: var(--muted); align-items: center; } + +/* Lista systemów domów urosła z 3 do 13 pozycji. Na flexie zawijały się w cztery + poszarpane wiersze o różnej długości; siatka o stałej szerokości kolumny ustawia + nazwy w pionie i daje się przeczytać wzrokiem. auto-fill, więc na wąskim ekranie + kolumn jest po prostu mniej. */ +.opts.systems { display: grid; grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr)); + gap: .35rem 1rem; align-items: start; } +.opts.systems > .opts-label { grid-column: 1 / -1; margin-bottom: .1rem; } +.opts.systems label { display: flex; align-items: center; gap: .35rem; } +.opts .hint { font-style: normal; opacity: .75; } .opts input[type=number] { width: 5rem; } .meta { color: var(--muted); margin: 1.5rem 0 .5rem; font-size: .9rem; } @@ -108,6 +118,21 @@ button:hover { filter: brightness(1.1); } .error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; } .empty { color: var(--muted); } table { width: 100%; border-collapse: collapse; margin-top: .5rem; background: var(--panel); border-radius: 12px; overflow: hidden; } + +/* Tabela porównania systemów domów ma tyle kolumn, ile zaznaczono systemów — + przy komplecie trzynastu robi się 1520 px i ROZPYCHAŁA CAŁĄ STRONĘ (dokument + 1713 px przy oknie 1280 px, poziomy pasek przewijania na body). Przewijać ma + się sama tabela, nigdy strona. */ +.table-scroll { overflow-x: auto; margin-top: .5rem; border-radius: 12px; } +.table-scroll > table { margin-top: 0; border-radius: 0; } +.table-scroll th, .table-scroll td { white-space: nowrap; } + +/* Numer domu przyklejony do lewej krawędzi: bez tego po przewinięciu w prawo + nie wiadomo, który to wiersz. */ +.table-scroll th:first-child, .table-scroll td:first-child { + position: sticky; left: 0; background: var(--panel); z-index: 1; + box-shadow: 1px 0 0 var(--line); +} th, td { text-align: left; padding: .6rem .8rem; border-bottom: 1px solid var(--line); } th { color: var(--accent); font-size: .8rem; text-transform: uppercase; letter-spacing: .5px; } tr:last-child td { border-bottom: none; } diff --git a/services/presentation/app/templates/_form_options.html b/services/presentation/app/templates/_form_options.html index c49d8ce..91816ba 100644 --- a/services/presentation/app/templates/_form_options.html +++ b/services/presentation/app/templates/_form_options.html @@ -7,9 +7,11 @@ -
| Dom | {% for hs in result.house_systems %}{{ house_label(hs.system) }} | {% endfor %}
|---|