Files
gitea 8b6ecc727d
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m44s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 20s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 23s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m38s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 9s
feat(domy): przypisanie do domu przez wyrocznię + poprawki układu strony
ETAP 5, część pierwsza: assign_house pod wyrocznią (swe_house_pos).
Osobny rodzaj błędu niż same cuspy — i od razu jeden znalazł.

BŁĄD: assign_house szło ZAWSZE do przodu. Przy dużych szerokościach systemy
dzielące koła wielkie mają kolejność domów ODWRÓCONĄ (przy φ=−84,3° cusp domu I
wypada na 174,2°, a domu II na 165,3°) — co nie jest usterką, bo wyrocznia zwraca
dokładnie te same wartości. Suma przeskoków „do przodu" wychodziła 3960° zamiast
360°, czyli każdy krok obchodził koło dookoła. Planety lądowały w złych domach
dla regiomontanusa, campanusa i topocentrica: 9,7-12,5% przypadków. Błąd cichy —
wykres wyglądał bez zarzutu. Kierunek bierzemy teraz z samych cuspów.

TOPOCENTRIC MA JEDNAK GRANICĘ DZIEDZINY — korekta tego, co pisałem wcześniej.
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 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°; regiomontanus 100%
w tych samych punktach). Odmawiamy, z jawnym fallbackiem jak Placidus i Koch.

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°). To inny rodzaj granicy niż u Placidusa
i Kocha: tam nie istnieją same cuspy, tu istnieją, tylko nie tworzą podziału.

Framework dostał pojęcie dziedziny WĘŻSZEJ niż wyroczni (NARROWER_THAN_ORACLE),
zamiast wyjątku „bo topocentric": skoro wyrocznia przeczy sama sobie, nie może
rozstrzygać, więc tam nie porównujemy — a nasze przypisanie jest w tym obszarze
sprawdzane testem samospójności z cuspami, bez swissepha.

UKŁAD STRONY — zmierzony na żywej stronie, nie na oko:
- tabela porównania przy 13 systemach miała 14 kolumn i 1863 px, a stała
  w rodzicu bez overflow-x, więc ROZPYCHAŁA CAŁY DOKUMENT: 1713 px przy oknie
  1280 px, poziomy pasek na body. Teraz ma własny kontener przewijany
  (dokument 1265 px, nie przewija się), a numer domu jest przyklejony do lewej,
  bo inaczej po przewinięciu nie wiadomo, który to wiersz.
- przypis „* nie działa za kołem podbiegunowym" siedział WEWNĄTRZ <label>
  selektora, łamał się na dwie linie i rozciągał wiersz siatki ze 66 do 108 px,
  rozjeżdżając go z sąsiednim polem. Wyjaśnienie stoi teraz raz, przy
  checkboxach z gwiazdkami; wiersz wrócił do 66 px.
- 13 checkboxów na flexie zawijało się w poszarpane wiersze — jest siatka
  o stałej szerokości kolumny (auto-fill, więc na wąskim ekranie kolumn mniej).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:55:13 +02:00

154 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Dziedzina wejścia funkcji liczącej domy — klasy równoważności i wartości brzegowe.
Liczenie cuspów traktujemy jako FUNKCJĘ trzech parametrów: (RAMC, ε, φ). ε nie jest
niezależne — wynika z daty, więc realnymi parametrami są (data, φ) plus RAMC, który
zależy od czasu gwiazdowego i długości geograficznej.
Zakres dat programu: 370 p.n.e. 2270 n.e. (rok astronomiczny -369 … 2270).
PUŁAPKA, o którą łatwo się potknąć: granica dziedziny dla Placidusa i Kocha —
koło podbiegunowe — NIE jest stałą 66,56°. Zależy od ε, a ε zmienia się z datą.
W zakresie programu przesuwa się o ~0,3°. Wartości brzegowe MUSZĄ więc być liczone
per data, inaczej testy brzegowe celują obok granicy.
"""
from __future__ import annotations
import random
from dataclasses import dataclass
# Zakres dat programu jako dni juliańskie (rok astronomiczny: 370 p.n.e. = -369).
# Wyznacza go jądro efemeryd de421, którego używa silnik. Większe jądro (de441,
# ~13 000 lat) jest DECYZJĄ ODŁOŻONĄ — świadomie „nice to have", nie „must":
# rozszerza wyłącznie zakres dat, a nie poprawność czegokolwiek w obecnym
# zakresie. Gdy kiedyś dojdzie, tu jest jedno miejsce do zmiany — i wtedy
# wartości brzegowe (ε, koło podbiegunowe) trzeba przeliczyć dla nowych krańców.
JD_MIN = 1_566_839.5 # -369-01-01
JD_MAX = 2_695_000.5 # ~2270-12-31
JD_J2000 = 2_451_545.0 # punkt odniesienia — środek dziedziny
# Systemy oparte na łuku dobowym: tracą definicję za kołem podbiegunowym.
DIURNAL_ARC_SYSTEMS = frozenset({"placidus", "koch"})
# Systemy, w których NASZA dziedzina jest WĘŻSZA niż wyroczni — świadomie, nie przez
# rozjazd. Topocentric: swisseph zwraca powyżej koła podbiegunowego cuspy, które nie
# dzielą okręgu (domy nachodzą na siebie), i sam sobie tam przeczy — swe_house_pos
# nie zgadza się z jego własnymi cuspami. Wolimy odmówić niż podać liczbę bez
# znaczenia, więc harness ma tego NIE zgłaszać jako niezgodności dziedziny.
NARROWER_THAN_ORACLE = frozenset({"topocentric"})
# Jak blisko granicy stawiamy wartości brzegowe (po obu stronach).
EPSILON_NUDGE = 0.01
@dataclass(frozen=True)
class Case:
"""Pojedynczy zestaw danych wejściowych + skąd się wziął (do raportu)."""
ramc: float
lat: float
jd: float
label: str # "brzeg: równik" / "wnętrze" / "losowy" — grupowanie w raporcie
def key(self) -> str:
return f"ramc={self.ramc:.4f} lat={self.lat:.4f} jd={self.jd:.1f}"
def obliquity(jd: float) -> float:
"""ε dla daty — z NASZEJ implementacji, żeby granice liczyć tak samo jak silnik."""
from app.engine.houses import mean_obliquity
return mean_obliquity(jd)
def polar_circle(jd: float) -> float:
from app.engine.houses import polar_circle as pc
return pc(obliquity(jd))
def in_domain(system: str, lat: float, jd: float) -> bool:
"""Czy zestaw należy do dziedziny danego systemu.
Poza dziedziną są wyłącznie systemy łuku dobowego powyżej koła podbiegunowego
(i same bieguny, gdzie degeneruje się cała geometria)."""
if abs(lat) >= 90.0:
return False
if system in DIURNAL_ARC_SYSTEMS or system in NARROWER_THAN_ORACLE:
return abs(lat) < polar_circle(jd)
return True
# ── wartości brzegowe ────────────────────────────────────────────────────
def _boundary_lats(jd: float) -> list[tuple[float, str]]:
"""Szerokości brzegowe DLA DANEJ DATY (koło podbiegunowe zależy od ε)."""
pc = polar_circle(jd)
out: list[tuple[float, str]] = [
(0.0, "brzeg: równik"),
(pc - EPSILON_NUDGE, "brzeg: tuż POD kołem podbieg."),
(pc + EPSILON_NUDGE, "brzeg: tuż ZA kołem podbieg."),
(-(pc - EPSILON_NUDGE), "brzeg: tuż pod kołem (płd.)"),
(-(pc + EPSILON_NUDGE), "brzeg: tuż za kołem (płd.)"),
(89.9, "brzeg: prawie biegun N"),
(-89.9, "brzeg: prawie biegun S"),
]
return out
def _boundary_ramcs() -> list[tuple[float, str]]:
"""RAMC na krańcach i w punktach kardynalnych — tam wzory mają osobliwości."""
return [
(0.0, "brzeg: RAMC 0°"),
(90.0, "brzeg: RAMC 90°"),
(180.0, "brzeg: RAMC 180°"),
(270.0, "brzeg: RAMC 270°"),
(359.99, "brzeg: RAMC ~360°"),
]
def _boundary_jds() -> list[tuple[float, str]]:
return [
(JD_MIN, "brzeg: początek zakresu dat"),
(JD_J2000, "środek zakresu (J2000)"),
(JD_MAX, "brzeg: koniec zakresu dat"),
]
def boundary_cases() -> list[Case]:
"""Iloczyn wartości brzegowych — pełne pokrycie kombinacji granicznych."""
out: list[Case] = []
for jd, jl in _boundary_jds():
for lat, ll in _boundary_lats(jd):
for ramc, rl in _boundary_ramcs():
out.append(Case(ramc, lat, jd, f"{ll} | {rl} | {jl}"))
return out
def interior_cases(count: int, seed: int = 0) -> list[Case]:
"""Tyle samo przypadków z WNĘTRZA klas równoważności, ile brzegowych.
Deterministyczne przy danym ziarnie — ten sam build daje ten sam zestaw.
"""
rnd = random.Random(seed)
out: list[Case] = []
for _ in range(count):
jd = rnd.uniform(JD_MIN, JD_MAX)
pc = polar_circle(jd)
# wnętrze = z dala od granic, żeby nie dublować przypadków brzegowych
lat = rnd.uniform(-pc + 1.0, pc - 1.0)
out.append(Case(rnd.uniform(0.0, 360.0), lat, jd, "wnętrze"))
return out
def random_cases(count: int, seed: int) -> list[Case]:
"""Wielki losowy przemiał — CAŁA dziedzina, także za kołem podbiegunowym.
Ziarno jest wymagane i raportowane: losowy test, którego nie da się odtworzyć,
jest bezużyteczny, bo czerwień raz na tydzień nie daje się zdiagnozować.
"""
rnd = random.Random(seed)
return [
Case(rnd.uniform(0.0, 360.0), rnd.uniform(-89.9, 89.9),
rnd.uniform(JD_MIN, JD_MAX), "losowy")
for _ in range(count)
]