Compare commits

..

2 Commits

Author SHA1 Message Date
gitea 4d1daab35a feat(domy): warianty od Barana i od MC + obrót kosmogramu — LOG-05 zamknięte
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m34s
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 18s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 10s
build / build (push) Successful in 26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m37s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 10s
Ostatnie trzy pozycje z treści LOG-05, których wcześniej nie było:

- whole_sign_aries — znaki jako domy, ale dom I ZAWSZE na 0° Barana, niezależnie
  od Ascendentu (tradycja indyjska i część szkół hellenistycznych),
- equal_mc — równe domy zakotwiczone na MC: dom X zaczyna się dokładnie na
  południku, a nie ma go gdzieś w środku,
- obrót kosmogramu: Ascendent albo 0° Barana po lewej stronie koła. Zmienia
  WYŁĄCZNIE rysunek, żadna liczba nie jest przeliczana. Wariant „od Barana"
  unieruchamia koło względem zodiaku, więc dwa horoskopy da się porównać na oko.

Oba nowe systemy zgodne z wyrocznią co do zera od pierwszego uruchomienia.
Razem 13 systemów: 2 833 424 porównania w przemiale, zero przekroczeń.

PRZY OKAZJI — DWA BŁĘDY, KTÓRE SAM WPROWADZIŁEM I KTÓRYCH TESTY NIE WIDZIAŁY:
- linia zbierająca ostrzeżenia o fallbacku trafiła do handlera strony głównej
  zamiast do compile_pdf: odwoływała się do nieistniejącej zmiennej, czyli 500
  na stronie głównej, a do PDF-a ostrzeżenia nie docierały wcale,
- compile_build używał parametru formularza, którego nie miał w sygnaturze.

Oba przeszły przez komplet zielonych testów, bo żaden nie wywoływał POST-a —
testy prezentacji sprawdzały teksty w szablonach i w main.py. Doszły więc testy
uderzające w prawdziwe trasy (POST / i POST /compile ze stubowaną logiką), które
łapią tę klasę błędu.

Z tego samego powodu przepisane dwa testy PDF-a: greppowały z main.py dokładny
kształt wywołania render(chart, theme="print") i pękały przy dopisaniu argumentu,
mimo poprawnego zachowania. Teraz wołają trasę i sprawdzają, że KAŻDY z czterech
rysunków dostaje motyw druku.

LOG-05 i PRE-05 → Zrobione w docs/astrololo_wymagania.xlsx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:38:43 +02:00
gitea aec3f84331 feat(domy): topocentric zgodny co do zera — wybór gałęzi liczony, nie zgadywany
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m29s
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 18s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 9s
build-render / build (push) Successful in 5m34s
build / build (push) Successful in 39s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m31s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 19s
Testy / Kontrola składni wszystkich warstw (push) Successful in 11s
Topocentric był wycofany z powodu rozjazdu przy |φ| ≈ 89,9°. Ta diagnoza była
BŁĘDNA: opierała się wyłącznie na zestawie brzegowym, który próbkuje tylko wybrane
szerokości. Przemiał losowy pokazał prawdziwą skalę — błędy od ~70° w górę,
do 50% przypadków blisko biegunów, 6,5% całości. Nie dwa przypadki brzegowe.

Przyczyną nie była jednak konstrukcja, tylko wybór gałęzi: dwa koła wielkie
przecinają się w dwóch punktach antypodycznych. Kolejno zawiodły reguły „po której
stronie MC", „w łuku kwadrantu", „wschodnia połowa horyzontu" i śledzenie ciągłości
krokami (to ostatnie maskuje własną patologię — po korekcie do bliższej gałęzi
zmierzony skok ZAWSZE wychodzi ≤ 90°, więc detektor nigdy się nie zapala).

Wszystkie te reguły rozstrzygają lokalnie, a przy dużych szerokościach kolejność
domów potrafi się odwrócić: przy φ = −79,55° MC wypada na 306,8°, a dom 11 na
291,4°. To jest poprawne — wyrocznia zwraca to samo.

Rozwiązanie: nie wybierać w ogóle. Iloczyn wektorowy zenitu z biegunem ekliptyki
jest ciągłą funkcją parametru rodziny i sam niesie właściwy zwrot; dwuznaczność
wprowadza dopiero atan2. Zostajemy w wektorach, znak ustalamy raz — kotwicząc
rodzinę na MC górującym.

Wynik: 0,000000000° na 240 000 porównań, cała dziedzina, bez iteracji i bez
zawężania szerokości. Żadna granica dziedziny nie jest tu potrzebna, więc żadnej
nie udajemy — test pilnuje, że topocentric liczy się wszędzie i nie fallbackuje.

Morał do frameworka: zestaw brzegowy mówi, CZY system się psuje; dopiero przemiał
losowy mówi JAK BARDZO. Wniosek o skali wyciągnięty z samych brzegów był tu
zaniżony o trzy rzędy wielkości. Odnotowane w tests/oracle/README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:24:40 +02:00
13 changed files with 318 additions and 99 deletions
Binary file not shown.
+54 -36
View File
@@ -14,6 +14,8 @@ from app.engine.formats import SIGN_ABBR, norm360, sign_index # noqa: F401
WHOLE_SIGN = "whole_sign"
EQUAL = "equal"
EQUAL_MC = "equal_mc" # równe domy zakotwiczone na MC, nie na Asc
WHOLE_SIGN_ARIES = "whole_sign_aries" # znaki jako domy, ale dom I to ZAWSZE Baran
PORPHYRY = "porphyry"
# Systemy o ZAMKNIĘTYM wzorze (bez iteracji). Placidus i Koch wymagają rozwiązania
# iteracyjnego i dochodzą osobno.
@@ -25,14 +27,12 @@ ALCABITUS = "alcabitus"
TOPOCENTRIC = "topocentric"
PLACIDUS = "placidus"
KOCH = "koch"
# Systemy WYPUSZCZONE — każdy zweryfikowany wobec Swiss Ephemeris co do zera
# (tests/oracle). TOPOCENTRIC celowo POZA listą: implementacja jest zgodna
# z wyrocznią wszędzie poza otoczeniem bieguna (|φ| ≈ 89,9° przy RAMC 90°/270°),
# gdzie kolejność domów się odwraca i konstrukcja traci sens — jego „biegun"
# atan(tan(89,9°)/3) to już 89,7°. Nie zawężamy dziedziny wyłącznie po to, by
# testy przeszły; system dołączy, gdy będzie poprawny na całej dziedzinie.
SYSTEMS = (WHOLE_SIGN, EQUAL, PORPHYRY, VEHLOW, MORINUS,
REGIOMONTANUS, CAMPANUS, ALCABITUS, PLACIDUS, KOCH)
# Systemy WYPUSZCZONE — każdy zweryfikowany wobec Swiss Ephemeris
# (tests/oracle). Placidus i Koch jako jedyne mają granicę dziedziny:
# powyżej koła podbiegunowego nie istnieją i podlegają jawnemu fallbackowi.
SYSTEMS = (WHOLE_SIGN, WHOLE_SIGN_ARIES, EQUAL, EQUAL_MC, PORPHYRY, VEHLOW,
MORINUS, REGIOMONTANUS, CAMPANUS, ALCABITUS, TOPOCENTRIC,
PLACIDUS, KOCH)
def mean_obliquity(tt_jd: float) -> float:
@@ -289,44 +289,54 @@ def _cusps_alcabitus(ramc: float, eps: float, asc: float) -> list[float]:
# Ułamek szerokości geograficznej użyty jako „biegun" koła domu (PolichPage).
# Domy na południku (10 i 4) mają biegun 0 — ich koło to sam południk.
_TOPO_POLE_FRACTION = (1.0, 2 / 3, 1 / 3, 0.0, 1 / 3, 2 / 3,
1.0, 2 / 3, 1 / 3, 0.0, 1 / 3, 2 / 3)
# PolichPage: dom pośredni to Ascendent policzony pod własnym „biegunem"
# tan(P) = tan(φ)·k/3, dla RAMC przesuniętego o pozycję domu. Rodzina jest CIĄGŁA:
# przy k=0 (biegun 0, przesunięcie 90°) daje MC, przy k=3 (biegun φ, przesunięcie 0)
# Ascendent, a domy 11 i 12 leżą po drodze.
#
# Cała trudność tego systemu siedziała w wyborze gałęzi — dwa koła wielkie
# przecinają się w dwóch punktach antypodycznych. Heurystyki („po której stronie
# MC", „w łuku kwadrantu", „wschodnia połowa horyzontu", śledzenie ciągłości
# krokami) myliły się na 611% przypadków powyżej ~70°, bo każda z nich rozstrzyga
# LOKALNIE, a przy dużych szerokościach kolejność domów potrafi się odwrócić.
#
# Rozwiązanie: nie wybierać w ogóle. Iloczyn wektorowy zenitu z biegunem ekliptyki
# jest ciągłą funkcją parametru rodziny i sam niesie właściwy zwrot — dwuznaczność
# wprowadza dopiero atan2. Zostajemy więc w wektorach, a znak ustalamy RAZ, kotwicząc
# rodzinę na MC górującym. Stąd zgodność co do zera na całej dziedzinie, bez iteracji
# i bez zawężania szerokości.
def _asc_under_pole_raw(ramc_deg: float, eps_deg: float, pole_deg: float) -> float:
"""Surowy wzór na ascendent pod zadanym „biegunem", BEZ korekty gałęzi.
Korektę stosuje wywołujący — względem PRAWDZIWEGO MC horoskopu. `compute_asc`
poprawia gałąź względem MC dla PRZESUNIĘTEGO RAMC, co dla cuspu domu jest złym
punktem odniesienia i w okolicach biegunów dawało obrót o 180°."""
r, e, phi = math.radians(ramc_deg), math.radians(eps_deg), math.radians(pole_deg)
return norm360(math.degrees(math.atan2(
math.cos(r), -(math.sin(r) * math.cos(e) + math.tan(phi) * math.sin(e)))))
# Dom → (przesunięcie RAMC [°], ułamek bieguna k/3).
_TOPO_STEP = {10: (-60.0, 1 / 3), 11: (-30.0, 2 / 3), # domy 11, 12
1: (30.0, 2 / 3), 2: (60.0, 1 / 3)} # domy 2, 3
def _cusps_topocentric(ramc: float, eps: float, lat: float,
asc: float, mc: float) -> list[float]:
"""PolichPage: dom pośredni to ASCENDENT policzony pod własnym „biegunem"
tan(P) = tan(φ)·k/3, dla RAMC przesuniętego o pozycję domu.
"""Ascendenty pod biegunami tan(P) = tan(φ)·k/3, liczone wektorowo.
Domy 5, 6, 8, 9 bierzemy jako OPOZYCJE domów 11, 12, 2, 3 — to nie skrót,
lecz własność konstrukcji: przeciwległe domy leżą na tym samym kole wielkim.
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".
Liczymy tylko domy 11, 12, 2, 3, a 5, 6, 8, 9 bierzemy jako ich OPOZYCJE —
to nie skrót, lecz własność tych systemów: przeciwległe domy leżą na tym samym
kole wielkim, więc ich cuspy są dokładnie antypodyczne."""
tan_lat = math.tan(math.radians(lat))
# Gałąź liczymy względem MC GÓRUJĄCEGO, nie matematycznego: gdy za kołem
# podbiegunowym te dwa się rozjeżdżają, cała czwórka domów pośrednich musi
# obrócić się razem z dziesiątym domem.
konstrukcji „ascendent pod biegunem"."""
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),
# cała rodzina obraca się razem z dziesiątym domem — stąd zwrot iloczynu.
culminating = _culminating_mc(mc, eps, lat)
sign = 1.0 if abs(((culminating - mc + 180.0) % 360.0) - 180.0) > 90.0 else -1.0
out = [0.0] * 12
for i in (10, 11, 1, 2): # domy 11, 12, 2, 3
pole = math.degrees(math.atan(tan_lat * _TOPO_POLE_FRACTION[i]))
lon = _asc_under_pole_raw(ramc + 30.0 * i, eps, pole)
if ((lon - culminating) % 360.0 < 180.0) != _INTERMEDIATE[i]:
lon = norm360(lon + 180.0)
for i, (offset, fraction) in _TOPO_STEP.items():
th = math.radians(ramc + offset)
pole = math.atan(tan_lat * fraction)
zenith = (math.cos(pole) * math.cos(th),
math.cos(pole) * math.sin(th),
math.sin(pole))
v = _cross(zenith, epole)
lon = _equatorial_to_lon(tuple(sign * x for x in v), er)
out[i] = lon
out[(i + 6) % 12] = norm360(lon + 180.0)
out[0], out[3] = asc, norm360(culminating + 180.0)
@@ -451,6 +461,14 @@ def cusps_for(ramc: float, eps: float, lat: float, system: str) -> list[float]:
mc = compute_mc(ramc, eps)
if system in (WHOLE_SIGN, EQUAL, PORPHYRY):
return cusps(asc, mc, system)
if system == WHOLE_SIGN_ARIES:
# Znaki jako domy, ale numeracja rusza od Barana niezależnie od Ascendentu.
# Wariant spotykany w tradycji indyjskiej i w części szkół hellenistycznych.
return [norm360(30.0 * i) for i in range(12)]
if system == EQUAL_MC:
# Równe domy jak `equal`, ale zakotwiczone na MC: dom X zaczyna się
# DOKŁADNIE na MC, więc oś południka wypada na granicy domu, a nie w środku.
return [norm360(mc + 90.0 + 30.0 * i) for i in range(12)]
if system == VEHLOW:
# equal, ale Ascendent leży w ŚRODKU domu I, nie na jego początku
return [norm360(asc - 15.0 + 30.0 * i) for i in range(12)]
+54 -1
View File
@@ -83,7 +83,8 @@ def test_polar_circle_moves_with_obliquity():
# Zgodność z wyrocznią sprawdza tests/oracle; tu pilnujemy niezmienników, które
# muszą zachodzić także bez swissepha (czyli w każdym środowisku).
EXOTIC = ("vehlow", "morinus", "regiomontanus", "campanus", "alcabitus")
EXOTIC = ("vehlow", "morinus", "regiomontanus", "campanus", "alcabitus",
"topocentric")
@pytest.mark.parametrize("system", EXOTIC)
@@ -196,3 +197,55 @@ def test_polar_circle_boundary_moves_with_the_date():
with pytest.raises(H.HouseSystemUndefined):
H.cusps_for(100.0, 23.4393, lat, "placidus")
assert len(H.cusps_for(100.0, 23.3528, lat, "placidus")) == 12
def test_topocentric_cusps_stay_in_order_between_mc_and_ascendant():
"""Regresja na to, co topocentrica psuło najdłużej — wybór gałęzi.
Cuspy 11 i 12 to punkty CIĄGŁEJ rodziny biegnącej od MC do Ascendentu, więc
czwórka MC → 11 → 12 → Asc leży na okręgu w kolejności, bez wyprzedzania się.
Zły wybór gałęzi przerzuca któryś punkt o 180° i kolejność pęka.
Kierunek celowo NIE jest ustalony: przy dużych szerokościach domy biegną
wstecz (przy φ=79,55° MC=306,8°, a dom 11 to 291,4°) i to jest poprawne —
wyrocznia zwraca tam dokładnie to samo. Sprawdzamy więc, że kolejność trzyma
się w JEDNĄ ze stron, a nie że rośnie."""
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):
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)
backward = d(c11, mc) + d(c12, c11) + d(asc, c12)
assert (abs(forward - d(mc, asc)) < 1e-9
or abs(backward - d(asc, mc)) < 1e-9), \
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):
assert len(H.cusps_for(100.0, 23.4393, lat, "topocentric")) == 12
cs = H.cusps_detailed(100.0, 23.4393, 89.9, "topocentric")
assert not cs.is_fallback and cs.notice is None
def test_whole_sign_aries_ignores_the_ascendant():
"""Wariant „od Barana": dom I zaczyna się na 0° Barana niezależnie od tego,
co wschodzi. To odróżnia go od zwykłego whole sign."""
for lat in (0.0, 50.06, -33.9):
c = H.cusps_for(100.0, 23.4393, lat, "whole_sign_aries")
assert c == [pytest.approx(30.0 * i) for i in range(12)]
def test_equal_mc_starts_the_tenth_house_exactly_on_the_midheaven():
"""W `equal` MC leży GDZIEŚ w domu X; w `equal_mc` zaczyna go dokładnie."""
ramc, eps, lat = 100.0, 23.4393, 50.0
mc = H.compute_mc(ramc, eps)
c = H.cusps_for(ramc, eps, lat, "equal_mc")
assert c[9] == pytest.approx(mc, abs=1e-9) # dom X rusza na MC
assert c[0] == pytest.approx(H.norm360(mc + 90.0), abs=1e-9)
for i in range(12): # nadal równe 30°
assert H.norm360(c[(i + 1) % 12] - c[i]) == pytest.approx(30.0, abs=1e-9)
+43 -27
View File
@@ -114,8 +114,15 @@ def _aspect_pen(orb: float, allowed: float | None) -> tuple[float, float]:
return 0.7 + 1.0 * frac, 0.35 + 0.5 * frac
def _pt(lon: float, asc: float, r: float) -> tuple[float, float]:
phi = math.radians(180.0 - (lon - asc))
# Co stoi po lewej stronie koła (LOG-05: „warianty 0°Aries / fixed Asc").
ASC_LEFT = "asc"
ARIES_LEFT = "aries"
ORIENTATIONS = ((ASC_LEFT, "Ascendent po lewej"), (ARIES_LEFT, "0° Barana po lewej"))
def _pt(lon: float, ref: float, r: float) -> tuple[float, float]:
"""Punkt na kole dla długości `lon`, przy `ref` po lewej stronie rysunku."""
phi = math.radians(180.0 - (lon - ref))
return _CX + r * math.cos(phi), _CY + r * math.sin(phi)
@@ -206,12 +213,19 @@ def _delta(a: float, b: float) -> float:
return abs(((a - b + 180.0) % 360.0) - 180.0)
def render(chart: dict, theme: str = "screen") -> str:
def render(chart: dict, theme: str = "screen", orientation: str = ASC_LEFT) -> str:
"""Zwraca SVG koła albo '' gdy brakuje danych (silnik bez osi/domów).
theme='screen' — kolory ze zmiennych CSS aplikacji (dostraja się do motywu).
theme='print' — konkretne kolory na białym tle; konieczne, gdy SVG trafia
do samodzielnego konwertera (PDF), który zmiennych CSS nie rozwiąże.
orientation — co stoi po LEWEJ stronie koła (LOG-05, warianty rysunku):
'asc' — Ascendent (domyślne; tak rysuje większość szkół zachodnich,
dom I zawsze zaczyna się w tym samym miejscu rysunku),
'aries' — 0° Barana (koło nieruchome względem zodiaku, więc dwa horoskopy
da się porównywać „na oko"; osie wypadają za to gdzie indziej).
Zmienia się WYŁĄCZNIE obrót rysunku — żadna liczba nie jest przeliczana.
"""
T = _THEMES.get(theme, _THEMES["screen"])
# tylko dla druku — na stronie font podaje CSS (.glyph)
@@ -223,6 +237,9 @@ def render(chart: dict, theme: str = "screen") -> str:
return ""
asc = float(angles["Asc"]["decimal"])
# Oś obrotu rysunku. Długości ekliptyczne zostają nietknięte — przesuwamy
# tylko punkt, który ląduje po lewej stronie koła.
ref = 0.0 if orientation == ARIES_LEFT else asc
parts: list[str] = []
# tło i okręgi
@@ -244,8 +261,8 @@ def render(chart: dict, theme: str = "screen") -> str:
continue
color = T["aspects"].get(asp.get("aspect"), T["muted"])
w, op = _aspect_pen(float(asp.get("orb", 0.0)), asp.get("allowed"))
ax1, ay1 = _pt(la, asc, R_HUB)
bx1, by1 = _pt(lb, asc, R_HUB)
ax1, ay1 = _pt(la, ref, R_HUB)
bx1, by1 = _pt(lb, ref, R_HUB)
tip = (f'{asp.get("obj1")} {_ASPECT_PL.get(asp.get("aspect"), asp.get("aspect"))} '
f'{asp.get("obj2")} · orb {float(asp.get("orb", 0.0)):.2f}°')
parts.append(_line(ax1, ay1, bx1, by1, color, w, op, title=tip))
@@ -253,17 +270,17 @@ def render(chart: dict, theme: str = "screen") -> str:
# drobne podziałki co 5°, mocniejsze co 10° (na pasie znaków)
for deg in range(0, 360, 5):
r_in = R_TICK if deg % 10 else R_TICK - 4
x1, y1 = _pt(deg, asc, R_ZOD)
x2, y2 = _pt(deg, asc, r_in)
x1, y1 = _pt(deg, ref, R_ZOD)
x2, y2 = _pt(deg, ref, r_in)
parts.append(_line(x1, y1, x2, y2, T["line"], 0.6, 0.7))
# pas znaków: granice co 30° + glif znaku w środku sektora, kolorem żywiołu
for i, sg in enumerate(signs):
b = 30.0 * i
x1, y1 = _pt(b, asc, R_ZOD)
x2, y2 = _pt(b, asc, R_OUT)
x1, y1 = _pt(b, ref, R_ZOD)
x2, y2 = _pt(b, ref, R_OUT)
parts.append(_line(x1, y1, x2, y2, T["line"], 1.0))
gx, gy = _pt(b + 15.0, asc, R_SIGN)
gx, gy = _pt(b + 15.0, ref, R_SIGN)
parts.append(_text(gx, gy, sg.get("glyph") or "", size=17,
fill=T["elements"][i % 4], cls="glyph", font=glyph_font))
@@ -271,31 +288,31 @@ def render(chart: dict, theme: str = "screen") -> str:
n = len(cusps)
for i, c in enumerate(cusps):
lon = float(c["decimal"])
x1, y1 = _pt(lon, asc, R_ZOD)
x2, y2 = _pt(lon, asc, R_HUB)
x1, y1 = _pt(lon, ref, R_ZOD)
x2, y2 = _pt(lon, ref, R_HUB)
parts.append(_line(x1, y1, x2, y2, T["line"], 0.8, 0.85))
nxt = float(cusps[(i + 1) % n]["decimal"])
span = (nxt - lon) % 360.0 or 360.0
mid = lon + span / 2.0
hx, hy = _pt(mid, asc, R_HNUM)
hx, hy = _pt(mid, ref, R_HNUM)
parts.append(_text(hx, hy, str(c["house"]), size=11, fill=T["muted"]))
# Stopień cuspu tuż przy szprysze, na pasie. W whole sign wszystkie cuspy
# są na 0° znaku — wtedy pomijamy, żeby nie kłaść dwunastu zbędnych zer;
# w systemach kwadratowych ta liczba realnie coś mówi.
deg_in_sign = lon % 30.0
if deg_in_sign >= 0.5:
cx1, cy1 = _pt(lon, asc, R_ZOD - 9)
cx1, cy1 = _pt(lon, ref, R_ZOD - 9)
parts.append(_text(cx1, cy1, str(int(deg_in_sign)), size=6.5, fill=T["muted"]))
# osie: AscDsc i MCIC przez całe koło, wyróżnione akcentem
for a, b, la, lb in (("Asc", "Dsc", "AC", "DC"), ("MC", "IC", "MC", "IC")):
lon_a = float(angles[a]["decimal"])
ax, ay = _pt(lon_a, asc, R_ZOD)
bx, by = _pt(lon_a + 180.0, asc, R_ZOD)
ax, ay = _pt(lon_a, ref, R_ZOD)
bx, by = _pt(lon_a + 180.0, ref, R_ZOD)
parts.append(_line(ax, ay, bx, by, T["accent"], 1.6, 0.9))
# etykiety POZA kołem — w środku kolidowały z glifami obiektów
lax, lay = _pt(lon_a, asc, R_AXIS_LABEL)
lbx, lby = _pt(lon_a + 180.0, asc, R_AXIS_LABEL)
lax, lay = _pt(lon_a, ref, R_AXIS_LABEL)
lbx, lby = _pt(lon_a + 180.0, ref, R_AXIS_LABEL)
parts.append(_text(lax, lay, la, size=10, fill=T["accent"]))
parts.append(_text(lbx, lby, lb, size=10, fill=T["accent"]))
@@ -310,15 +327,15 @@ def render(chart: dict, theme: str = "screen") -> str:
for obj, true_lon, draw_lon in zip(objects, true_lons, draw_lons):
retro = obj.get("direction") == "Rx"
mx1, my1 = _pt(true_lon, asc, R_ZOD)
mx2, my2 = _pt(true_lon, asc, R_MARK)
mx1, my1 = _pt(true_lon, ref, R_ZOD)
mx2, my2 = _pt(true_lon, ref, R_MARK)
parts.append(_line(mx1, my1, mx2, my2, T["ink"], 1.0, 0.75))
if _delta(draw_lon, true_lon) > 0.4:
lx, ly = _pt(draw_lon, asc, R_PLANET + 12)
lx, ly = _pt(draw_lon, ref, R_PLANET + 12)
parts.append(_line(mx2, my2, lx, ly, T["line"], 0.7, 0.9))
gx, gy = _pt(draw_lon, asc, R_PLANET)
gx, gy = _pt(draw_lon, ref, R_PLANET)
tip = obj.get("name") or ""
if obj.get("in_sign"):
tip += f" · {obj['in_sign']}"
@@ -332,7 +349,7 @@ def render(chart: dict, theme: str = "screen") -> str:
# Sam stopień w znaku, bez „°" — tak robią programy astrologiczne i tylko
# tak podpisy mieszczą się obok siebie w skupiskach. Retrogradacja: ℞ oraz
# kolor, żeby dało się ją wyłapać nie czytając znaku po znaku.
dx, dy = _pt(draw_lon, asc, R_DEG)
dx, dy = _pt(draw_lon, ref, R_DEG)
label = f"{int(true_lon % 30)}" + ("" if retro else "")
parts.append(_text(dx, dy, label, size=8,
fill=T["retro"] if retro else T["muted"]))
@@ -346,10 +363,10 @@ def render(chart: dict, theme: str = "screen") -> str:
if not glyph or lot.get("longitude") is None:
continue
lon = float(lot["longitude"])
mx1, my1 = _pt(lon, asc, R_ZOD)
mx2, my2 = _pt(lon, asc, R_MARK)
mx1, my1 = _pt(lon, ref, R_ZOD)
mx2, my2 = _pt(lon, ref, R_MARK)
parts.append(_line(mx1, my1, mx2, my2, T["accent"], 1.0, 0.7))
lx, ly = _pt(lon, asc, R_LOT)
lx, ly = _pt(lon, ref, R_LOT)
tip = lot.get("name") or ""
if lot.get("in_sign"):
tip += f" · {lot['in_sign']}"
@@ -367,7 +384,6 @@ def render(chart: dict, theme: str = "screen") -> str:
# ── aspektarian (PRE-18) ──────────────────────────────────────────────────
ASP_CELL = 30.0 # bok komórki siatki aspektów
def render_aspectarian(chart: dict, theme: str = "screen") -> str:
"""Aspektarian (PRE-18) — trójkątna siatka aspektów obiekt×obiekt jako SVG.
+4 -1
View File
@@ -15,12 +15,15 @@ from __future__ import annotations
HOUSE_SYSTEMS: list[tuple[str, str]] = [
("placidus", "Placidus"),
("whole_sign", "Whole Sign"),
("whole_sign_aries", "Whole Sign (od Barana)"),
("koch", "Koch"),
("equal", "Equal"),
("equal", "Equal (od Asc)"),
("equal_mc", "Equal (od MC)"),
("porphyry", "Porphyry"),
("regiomontanus", "Regiomontanus"),
("campanus", "Campanus"),
("alcabitus", "Alcabitus"),
("topocentric", "Topocentric"),
("morinus", "Morinus"),
("vehlow", "Vehlow"),
]
+17 -9
View File
@@ -22,6 +22,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app import chartwheel as chartwheel_mod
from app.house_systems import HOUSE_SYSTEMS, LIMITED as HOUSE_LIMITED, label as house_label
from app import geocode, security
@@ -60,6 +61,7 @@ templates.env.globals["static"] = static_url
templates.env.globals["HOUSE_SYSTEMS"] = HOUSE_SYSTEMS
templates.env.globals["house_label"] = house_label
templates.env.globals["HOUSE_LIMITED"] = HOUSE_LIMITED
templates.env.globals["WHEEL_ORIENTATIONS"] = chartwheel_mod.ORIENTATIONS
def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]:
@@ -130,12 +132,14 @@ def chart_compute(
stations: bool = Form(False),
zodiac: str = Form("tropical"),
tables: bool = Form(False),
wheel_orientation: str = Form(chartwheel_mod.ASC_LEFT),
):
form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset,
"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}
"stations": stations, "zodiac": zodiac, "tables": tables,
"wheel_orientation": wheel_orientation}
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
try:
iso_utc, label = _build_utc(date, time, tz_offset)
@@ -147,12 +151,8 @@ def chart_compute(
aspect_minor=aspect_minor,
stations=stations, zodiac=zodiac, tables=tables,
)
# Fallback systemu domów musi dojechać do PDF-a — z samego kosmogramu
# nie da się poznać, że podział jest z innego systemu, niż zamówiono.
warnings = [w for w in (chart.get("house_warnings") or []) if w]
from app import chartwheel # kosmogram (PRE-12), SVG po stronie serwera
ctx["wheel_svg"] = chartwheel.render(ctx["result"])
ctx["wheel_svg"] = chartwheel.render(ctx["result"], orientation=wheel_orientation)
ctx["aspectarian_svg"] = chartwheel.render_aspectarian(ctx["result"]) # PRE-18
ctx["declination_svg"] = chartwheel.render_declination(ctx["result"]) # LOG-07 (etap 6)
ctx["antiscia_svg"] = chartwheel.render_antiscia(ctx["result"]) # LOG-07 (etap 6)
@@ -189,6 +189,7 @@ def compile_build(
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
wheel_orientation: str = Form(chartwheel_mod.ASC_LEFT), # LOG-05 — obrót koła
):
"""Składa raport: horoskop liczymy TU NA NOWO, a części od AI (interpretacja
natalna i predykcje okresowe) dokłada przeglądarka z magazynu (PRE-22/23).
@@ -203,7 +204,8 @@ def compile_build(
"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}
"stations": stations, "zodiac": zodiac, "tables": tables,
"wheel_orientation": wheel_orientation}
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
try:
iso_utc, label = _build_utc(date, time, tz_offset)
@@ -216,7 +218,7 @@ def compile_build(
stations=stations, zodiac=zodiac, tables=tables,
)
from app import chartwheel
ctx["wheel_svg"] = chartwheel.render(ctx["result"])
ctx["wheel_svg"] = chartwheel.render(ctx["result"], orientation=wheel_orientation)
ctx["aspectarian_svg"] = chartwheel.render_aspectarian(ctx["result"]) # PRE-18
ctx["declination_svg"] = chartwheel.render_declination(ctx["result"]) # LOG-07 (etap 6)
ctx["antiscia_svg"] = chartwheel.render_antiscia(ctx["result"]) # LOG-07 (etap 6)
@@ -265,13 +267,19 @@ def compile_pdf(payload: dict):
stations=bool(data.get("stations")), tables=bool(data.get("tables")),
zodiac=str(data.get("zodiac") or "tropical"),
)
# Fallback systemu domów musi dojechać do PDF-a — z samego kosmogramu
# nie da się poznać, że podział jest z innego systemu, niż zamówiono.
warnings = [w for w in (chart.get("house_warnings") or []) if w]
from app import chartwheel
# Zasada: co pokazujemy na stronie, ma trafić do PDF-a. Wszystkie rysunki
# w motywie DRUKU — samodzielny konwerter SVG→PDF nie zna arkusza, więc
# zmienne CSS i font glifów muszą być wprost w rysunku.
for svg, caption in (
(chartwheel.render(chart, theme="print"), "Kosmogram"),
(chartwheel.render(chart, theme="print",
orientation=str(data.get("wheel_orientation") or chartwheel_mod.ASC_LEFT)),
"Kosmogram"),
(chartwheel.render_aspectarian(chart, theme="print"), "Aspektarian — siatka aspektów"),
(chartwheel.render_declination(chart, theme="print"), "Wykres deklinacji"),
(chartwheel.render_antiscia(chart, theme="print"), "Oś antyscji"),
@@ -14,6 +14,13 @@
<label><input type="checkbox" name="house_systems" value="{{ value }}" {{ 'checked' if value in chosen else '' }}> {{ name }}{{ ' *' if value in HOUSE_LIMITED else '' }}</label>
{% endfor %}
</div>
<div class="opts" title="Zmienia wyłącznie OBRÓT rysunku — żadna liczba nie jest przeliczana. „Ascendent po lewej” to tradycja zachodnia: dom I zawsze zaczyna się w tym samym miejscu. „0° Barana po lewej” unieruchamia koło względem zodiaku, dzięki czemu dwa horoskopy da się porównywać na oko.">
{% set wo = form.wheel_orientation or 'asc' %}
<span class="muted small">Kosmogram — po lewej stronie:</span>
{% for value, name in WHEEL_ORIENTATIONS %}
<label><input type="radio" name="wheel_orientation" value="{{ value }}" {{ 'checked' if wo == value else '' }}> {{ name }}</label>
{% endfor %}
</div>
<div class="opts" title="Orb = dopuszczalne odchylenie od dokładnego kąta aspektu. „Bonus świateł" powiększa orb dla aspektów ze Słońcem/Księżycem. Aspekty poboczne: półsekstyl 30°, półkwadratura 45°, kwinkunks 150° (PRE-06).">
<label>Orb aspektów (°) <input type="number" name="aspect_orb" step="0.5" min="1" max="15" value="{{ form.aspect_orb if form.aspect_orb is not none else 8 }}"></label>
<label>Bonus świateł (°) <input type="number" name="aspect_luminary_bonus" step="0.5" min="0" max="5" value="{{ form.aspect_luminary_bonus if form.aspect_luminary_bonus is not none else 2 }}"></label>
+39 -10
View File
@@ -76,16 +76,45 @@ def test_compile_page_renders_all_figures():
assert var in MAIN, f"/compile nie ustawia {var}"
def test_pdf_bundles_all_figures_in_print_theme():
"""compile_pdf składa KOMPLET rysunków (motyw druku) i wysyła jako `figures`."""
assert '"figures": figures' in MAIN
for call in (
'render(chart, theme="print")',
'render_aspectarian(chart, theme="print")',
'render_declination(chart, theme="print")',
'render_antiscia(chart, theme="print")',
):
assert call in MAIN, f"PDF nie składa: {call}"
def test_pdf_bundles_all_figures_in_print_theme(monkeypatch):
"""compile_pdf składa KOMPLET rysunków w motywie DRUKU.
Sprawdzamy przez WYWOŁANIE trasy, nie przez szukanie tekstu w main.py:
poprzednia wersja greppowała `render(chart, theme="print")` i pękała przy
każdym dopisaniu argumentu, mimo że zachowanie zostawało poprawne."""
seen: dict[str, str] = {}
from app import chartwheel
from app.clients.render_client import RenderClient
from app.main import logic
for name in ("render", "render_aspectarian", "render_declination", "render_antiscia"):
original = getattr(chartwheel, name)
def spy(chart, theme="screen", _n=name, _o=original, **kw):
seen[_n] = theme
return _o(chart, theme=theme, **kw)
monkeypatch.setattr(chartwheel, name, spy)
monkeypatch.setattr(logic, "positions", lambda **kw: _pdf_sample_chart())
monkeypatch.setattr(RenderClient, "pdf", lambda self, report: b"%PDF-1.4 stub")
r = _client().post("/compile/pdf", json={"person": "Jan", "data": {
"date": "1984-04-30", "time": "11:20", "tz_offset": 2, "lat": 50.06, "lon": 19.94}})
assert r.status_code == 200, r.text[:300]
assert set(seen) == {"render", "render_aspectarian",
"render_declination", "render_antiscia"}, seen
assert set(seen.values()) == {"print"}, seen
def _pdf_sample_chart() -> dict:
cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0", "decimal": float(i * 30),
"sign_glyph": ""} for i in range(12)]
ang = {k: {"name": k, "sign": "Aries", "in_sign": "0", "decimal": 0.0,
"sign_glyph": ""} for k in ("Asc", "MC", "Dsc", "IC")}
return {"engine": "test", "positions": [], "cusps": cusps, "angles": ang,
"sign_glyphs": [{"sign": "Aries", "glyph": ""}], "house_system": "equal"}
# ──────────────────────── zbieranie materiału z magazynów ────────────────
@@ -5,6 +5,8 @@ systemów, klient przekazuje je do logiki, a szablon pokazuje kuspy obok siebie.
"""
import pathlib
import pytest
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8")
COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
@@ -96,3 +98,59 @@ def test_fallback_notice_is_shown_prominently():
def test_no_notice_when_nothing_was_substituted():
html = _render("_result_tables.html", result={"house_warnings": []})
assert "house-warning" not in html
# ── ścieżka PRZEZ handlery, nie obok nich ───────────────────────────────
# Dwa błędy przeszły przez komplet testów szablonowych, bo żaden nie wywołał
# POST-a: odwołanie do nieistniejącej zmiennej w handlerze strony głównej oraz
# użycie parametru formularza, którego w sygnaturze nie było. Oba dają 500 na
# żywo i oba łapie dopiero prawdziwe żądanie.
def _client_with_stub_logic(monkeypatch, chart):
import os
os.environ.pop("APP_PASSWORD", None)
from starlette.testclient import TestClient
from app.main import app, logic
monkeypatch.setattr(logic, "positions", lambda **kw: chart)
return TestClient(app)
def _minimal_chart():
cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0°00'00''",
"decimal": float(i * 30), "sign_glyph": ""} for i in range(12)]
ang = {k: {"name": k, "sign": "Aries", "in_sign": "0°00'00''", "decimal": 0.0,
"sign_glyph": ""} for k in ("Asc", "MC", "Dsc", "IC")}
return {"engine": "test", "positions": [], "cusps": cusps, "angles": ang,
"sign_glyphs": [{"sign": "Aries", "glyph": ""}],
"house_system": "porphyry", "house_system_requested": "placidus",
"house_warnings": ["UWAGA: system domów „placidus” nie ma definicji"]}
FORM = {"date": "1984-04-30", "time": "11:20", "tz_offset": "2",
"lat": "50.06", "lon": "19.94", "house_system": "placidus"}
def test_chart_page_survives_a_real_post(monkeypatch):
c = _client_with_stub_logic(monkeypatch, _minimal_chart())
r = c.post("/", data=FORM)
assert r.status_code == 200, r.text[:400]
assert "nie ma definicji" in r.text, "ostrzeżenie o fallbacku nie dotarło na stronę"
def test_compile_page_survives_a_real_post(monkeypatch):
c = _client_with_stub_logic(monkeypatch, _minimal_chart())
r = c.post("/compile", data=FORM)
assert r.status_code == 200, r.text[:400]
@pytest.mark.parametrize("orientation", ("asc", "aries"))
def test_wheel_orientation_reaches_the_renderer(monkeypatch, orientation):
"""Wybór obrotu koła musi dojść z formularza do rysunku — i wrócić
zaznaczony, żeby nie gubił się przy przeliczeniu."""
c = _client_with_stub_logic(monkeypatch, _minimal_chart())
r = c.post("/", data={**FORM, "wheel_orientation": orientation})
assert r.status_code == 200, r.text[:400]
assert f'value="{orientation}" checked' in r.text
@@ -66,8 +66,12 @@ def test_key_is_read_lazily():
# ──────────────────────── kosmogram w wariancie do druku ─────────────────
def test_pdf_route_uses_print_theme():
"""Samodzielny konwerter SVG→PDF nie zna naszego arkusza stylów."""
assert 'chartwheel.render(chart, theme="print")' in MAIN
"""Samodzielny konwerter SVG→PDF nie zna naszego arkusza stylów.
Sprawdzamy, że trasa PDF-a w ogóle podaje motyw druku — bez wiązania się
z dokładnym kształtem wywołania (dochodzą do niego kolejne argumenty).
Pełny sprawdzian, że KAŻDY rysunek dostaje 'print', jest w test_compile.py."""
assert 'theme="print"' in MAIN
def test_print_theme_has_no_css_variables():
+29 -8
View File
@@ -70,13 +70,16 @@ zarówno zestaw brzegowy (2520 porównań), jak i losowy przemiał 20 000 przypa
| System | Konstrukcja | Maks. odchylenie |
|---|---|---|
| whole sign | podział ekliptyki | 0,000000000° |
| equal | podział ekliptyki | 0,000000000° |
| whole sign (od Barana) | dom I zawsze na 0° Barana | 0,000000000° |
| equal | podział ekliptyki od Ascendentu | 0,000000000° |
| equal (od MC) | dom X dokładnie na MC | 0,000000000° |
| porphyry | podział kwadrantów po ekliptyce | 0,000000000° |
| vehlow | equal z Ascendentem w środku domu I | 0,000000000° |
| morinus | równik rzutowany wprost na ekliptykę | 0,000000000° |
| regiomontanus | podział równika, koła przez punkty N/S horyzontu | 0,000000000° |
| campanus | podział wertykału pierwszego | 0,000000000° |
| alcabitus | podział łuków dobowych po równiku | 0,000000000° |
| topocentric | ascendenty pod biegunami tan(P) = tan(φ)·k/3 | 0,000000000° |
Etap 2 — systemy **łuku dobowego**, jedyne z realną granicą dziedziny:
@@ -97,13 +100,31 @@ Warstwa aplikacyjna łapie tę odmowę i podstawia Porphyry'ego z **głośnym
komunikatem**, który idzie na ekran, w prompt do modelu i do PDF-a: po podmianie
kosmogram wygląda bezbłędnie, więc jest to jedyny sygnał dla astrologa.
**Topocentric (PolichPage) jest zaimplementowany, ale NIE wypuszczony** — nie ma go
w `houses.SYSTEMS`. Zgadza się z wyrocznią na całej dziedzinie poza otoczeniem
bieguna: przy |φ| ≈ 89,9° i RAMC 90°/270° kolejność domów się odwraca i żadna reguła
oparta na łuku kwadrantu nie rozstrzyga wyboru gałęzi. Konstrukcja jest tam z natury
źle uwarunkowana — „biegun" `atan(tan(φ)·k/3)` dla φ = 89,9° wynosi już 89,7°.
Nie zawężamy dziedziny po to, żeby testy przeszły; system dołączy, gdy będzie
poprawny wszędzie.
### Topocentric — i czego nauczył
Topocentric był początkowo **wycofany** z powodu rozjazdu przy |φ| ≈ 89,9°.
Ta diagnoza była **błędna**, bo opierała się wyłącznie na zestawie brzegowym,
który próbkuje tylko wybrane szerokości. Przemiał losowy pokazał prawdziwą skalę:
błędy zaczynały się już od ~70° i sięgały 50% przypadków blisko biegunów —
**6,5% całości**, nie dwa przypadki brzegowe.
Przyczyną nie była jednak konstrukcja, tylko wybór gałęzi. Dwa koła wielkie
przecinają się w dwóch punktach antypodycznych i trzeba wskazać właściwy.
Kolejno zawiodły: „po której stronie MC", „w łuku kwadrantu", „wschodnia połowa
horyzontu" oraz śledzenie ciągłości krokami. Każda z nich rozstrzyga **lokalnie**,
a przy dużych szerokościach kolejność domów potrafi się odwrócić — przy φ = 79,55°
MC wypada na 306,8°, a dom 11 na 291,4°, czyli domy biegną wstecz. To jest poprawne
i wyrocznia zwraca dokładnie to samo.
Rozwiązaniem było **nie wybierać w ogóle**. Iloczyn wektorowy zenitu z biegunem
ekliptyki jest ciągłą funkcją parametru rodziny i sam niesie właściwy zwrot;
dwuznaczność wprowadza dopiero `atan2`. Zostając w wektorach i kotwicząc rodzinę
raz — na MC górującym — dostajemy zgodność **co do zera na całej dziedzinie**,
bez iteracji i bez zawężania szerokości.
Morał praktyczny: zestaw brzegowy mówi, **czy** system się psuje; dopiero przemiał
losowy mówi **jak bardzo**. Wniosek o skali wyciągnięty z samych brzegów był tu
zaniżony o trzy rzędy wielkości.
Większe jądro efemeryd (de441) — **odłożone, „nice to have"**: rozszerza tylko
zakres dat, nie poprawia niczego w obecnym.
+5 -3
View File
@@ -24,7 +24,9 @@ from domain import Case, in_domain, obliquity
# Litery systemów w Swiss Ephemeris.
SWE_CODE = {
"whole_sign": b"W", "equal": b"E", "porphyry": b"O",
"whole_sign": b"W", "equal": b"E",
"equal_mc": b"D",
"whole_sign_aries": b"N", "porphyry": b"O",
"placidus": b"P", "koch": b"K", "regiomontanus": b"R", "campanus": b"C",
"morinus": b"M", "alcabitus": b"B", "vehlow": b"V", "topocentric": b"T",
}
@@ -158,9 +160,9 @@ def format_report(results: list[Result], seed: int | None = None) -> str:
"ZGODNOŚĆ Z WYROCZNIĄ (Swiss Ephemeris) — domy astrologiczne",
f"tolerancja: {TOLERANCE_DEG:.8f}° (1″)" + (f" ziarno: {seed}" if seed is not None else ""),
"=" * 78,
f"{'SYSTEM':<14}{'SPRAWDZONYCH':>13}{'>TOL':>7}{'POZA DZIEDZ.':>14}{'MAX ODCH.':>14} WYNIK"]
f"{'SYSTEM':<18}{'SPRAWDZONYCH':>13}{'>TOL':>7}{'POZA DZIEDZ.':>14}{'MAX ODCH.':>14} WYNIK"]
for r in results:
lines.append(f"{r.system:<14}{r.checked:>13}{r.over_tolerance:>7}"
lines.append(f"{r.system:<18}{r.checked:>13}{r.over_tolerance:>7}"
f"{r.skipped_out_of_domain:>14}{r.max_dev:>14.9f} "
f"{'OK' if r.passed else 'BŁĄD'}")
for r in results:
+2 -2
View File
@@ -27,8 +27,8 @@ from harness import ( # noqa: E402
# Systemy do sprawdzenia. Rośnie wraz z implementacją kolejnych (Etap 1 i 2) —
# dopisanie nazwy tutaj wystarcza, żeby weszła do każdego builda.
SYSTEMS = ["whole_sign", "equal", "porphyry", "vehlow", "morinus",
"regiomontanus", "campanus", "alcabitus", "placidus", "koch"]
SYSTEMS = ["whole_sign", "whole_sign_aries", "equal", "equal_mc", "porphyry", "vehlow", "morinus",
"regiomontanus", "campanus", "alcabitus", "topocentric", "placidus", "koch"]
def main() -> int: