Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aec3f84331 | |||
| 7266f671a4 | |||
| 548d9301f3 |
@@ -97,7 +97,11 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
|||||||
asc = H.compute_asc(ramc, eps, moment.lat)
|
asc = H.compute_asc(ramc, eps, moment.lat)
|
||||||
mc = H.compute_mc(ramc, eps)
|
mc = H.compute_mc(ramc, eps)
|
||||||
system = house_system if house_system in H.SYSTEMS else H.WHOLE_SIGN
|
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
|
# cusps_detailed, nie cusps_for: Placidus i Koch nie istnieją powyżej koła
|
||||||
|
# podbiegunowego, a astrolog z Tromsø ma dostać wynik ZE ŚLADEM, czym go
|
||||||
|
# policzyliśmy. Ten ślad musi dojść aż do raportu i PDF-a.
|
||||||
|
primary = H.cusps_detailed(ramc, eps, moment.lat, system)
|
||||||
|
cusp_list = primary.cusps # tropikalne — geometria domów jest niezmiennicza
|
||||||
|
|
||||||
def _cusps_out(cl: list[float]) -> list[dict]:
|
def _cusps_out(cl: list[float]) -> list[dict]:
|
||||||
"""Cuspy → wiersze pod UI/kosmogram: znak, stopień w znaku, długość, glif."""
|
"""Cuspy → wiersze pod UI/kosmogram: znak, stopień w znaku, długość, glif."""
|
||||||
@@ -109,7 +113,11 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
|||||||
for i, c in enumerate(cl)
|
for i, c in enumerate(cl)
|
||||||
]
|
]
|
||||||
|
|
||||||
result["house_system"] = system
|
result["house_system"] = primary.system # FAKTYCZNIE użyty
|
||||||
|
result["house_system_requested"] = primary.requested
|
||||||
|
# Lista ostrzeżeń dla całego horoskopu — prezentacja, raport i PDF czytają
|
||||||
|
# jedno miejsce, więc żaden z nich nie może „zapomnieć" o fallbacku.
|
||||||
|
result["house_warnings"] = [primary.notice] if primary.notice else []
|
||||||
result["angles"] = {
|
result["angles"] = {
|
||||||
"Asc": _fmt("Asc", asc, off),
|
"Asc": _fmt("Asc", asc, off),
|
||||||
"MC": _fmt("MC", mc, off),
|
"MC": _fmt("MC", mc, off),
|
||||||
@@ -132,10 +140,15 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
|||||||
if len(ordered) > 1:
|
if len(ordered) > 1:
|
||||||
result["house_systems"] = []
|
result["house_systems"] = []
|
||||||
for s in ordered:
|
for s in ordered:
|
||||||
cl = cusp_list if s == system else H.cusps(asc, mc, s)
|
cs = primary if s == system else H.cusps_detailed(ramc, eps, moment.lat, s)
|
||||||
result["house_systems"].append({"system": s, "cusps": _cusps_out(cl)})
|
block = {"system": s, "used_system": cs.system, "cusps": _cusps_out(cs.cusps)}
|
||||||
|
if cs.notice:
|
||||||
|
block["notice"] = cs.notice
|
||||||
|
if cs.notice not in result["house_warnings"]:
|
||||||
|
result["house_warnings"].append(cs.notice)
|
||||||
|
result["house_systems"].append(block)
|
||||||
for pdict, obj in zip(result["positions"], positions):
|
for pdict, obj in zip(result["positions"], positions):
|
||||||
pdict.setdefault("houses", {})[s] = H.assign_house(obj.longitude, cl)
|
pdict.setdefault("houses", {})[s] = H.assign_house(obj.longitude, cs.cusps)
|
||||||
|
|
||||||
# Lots (LOG-08) — wymagają Asc i sekty (dzień/noc)
|
# Lots (LOG-08) — wymagają Asc i sekty (dzień/noc)
|
||||||
from app.engine.firdaria import is_day_birth
|
from app.engine.firdaria import is_day_birth
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ silnik dostarcza tylko RAMC i ε.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from app.engine.formats import SIGN_ABBR, norm360, sign_index # noqa: F401
|
from app.engine.formats import SIGN_ABBR, norm360, sign_index # noqa: F401
|
||||||
|
|
||||||
@@ -22,14 +23,13 @@ REGIOMONTANUS = "regiomontanus"
|
|||||||
CAMPANUS = "campanus"
|
CAMPANUS = "campanus"
|
||||||
ALCABITUS = "alcabitus"
|
ALCABITUS = "alcabitus"
|
||||||
TOPOCENTRIC = "topocentric"
|
TOPOCENTRIC = "topocentric"
|
||||||
# Systemy WYPUSZCZONE — każdy zweryfikowany wobec Swiss Ephemeris co do zera
|
PLACIDUS = "placidus"
|
||||||
# (tests/oracle). TOPOCENTRIC celowo POZA listą: implementacja jest zgodna
|
KOCH = "koch"
|
||||||
# z wyrocznią wszędzie poza otoczeniem bieguna (|φ| ≈ 89,9° przy RAMC 90°/270°),
|
# Systemy WYPUSZCZONE — każdy zweryfikowany wobec Swiss Ephemeris
|
||||||
# gdzie kolejność domów się odwraca i konstrukcja traci sens — jego „biegun"
|
# (tests/oracle). Placidus i Koch jako jedyne mają granicę dziedziny:
|
||||||
# atan(tan(89,9°)/3) to już 89,7°. Nie zawężamy dziedziny wyłącznie po to, by
|
# powyżej koła podbiegunowego nie istnieją i podlegają jawnemu fallbackowi.
|
||||||
# testy przeszły; system dołączy, gdy będzie poprawny na całej dziedzinie.
|
SYSTEMS = (WHOLE_SIGN, EQUAL, PORPHYRY, VEHLOW, MORINUS, REGIOMONTANUS,
|
||||||
SYSTEMS = (WHOLE_SIGN, EQUAL, PORPHYRY, VEHLOW, MORINUS,
|
CAMPANUS, ALCABITUS, TOPOCENTRIC, PLACIDUS, KOCH)
|
||||||
REGIOMONTANUS, CAMPANUS, ALCABITUS)
|
|
||||||
|
|
||||||
|
|
||||||
def mean_obliquity(tt_jd: float) -> float:
|
def mean_obliquity(tt_jd: float) -> float:
|
||||||
@@ -286,44 +286,54 @@ def _cusps_alcabitus(ramc: float, eps: float, asc: float) -> list[float]:
|
|||||||
|
|
||||||
# Ułamek szerokości geograficznej użyty jako „biegun" koła domu (Polich–Page).
|
# Ułamek szerokości geograficznej użyty jako „biegun" koła domu (Polich–Page).
|
||||||
# Domy na południku (10 i 4) mają biegun 0 — ich koło to sam południk.
|
# 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,
|
# Polich–Page: dom pośredni to Ascendent policzony pod własnym „biegunem"
|
||||||
1.0, 2 / 3, 1 / 3, 0.0, 1 / 3, 2 / 3)
|
# 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 6–11% 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.
|
||||||
|
|
||||||
|
# Dom → (przesunięcie RAMC [°], ułamek bieguna k/3).
|
||||||
def _asc_under_pole_raw(ramc_deg: float, eps_deg: float, pole_deg: float) -> float:
|
_TOPO_STEP = {10: (-60.0, 1 / 3), 11: (-30.0, 2 / 3), # domy 11, 12
|
||||||
"""Surowy wzór na ascendent pod zadanym „biegunem", BEZ korekty gałęzi.
|
1: (30.0, 2 / 3), 2: (60.0, 1 / 3)} # domy 2, 3
|
||||||
|
|
||||||
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)))))
|
|
||||||
|
|
||||||
|
|
||||||
def _cusps_topocentric(ramc: float, eps: float, lat: float,
|
def _cusps_topocentric(ramc: float, eps: float, lat: float,
|
||||||
asc: float, mc: float) -> list[float]:
|
asc: float, mc: float) -> list[float]:
|
||||||
"""Polich–Page: dom pośredni to ASCENDENT policzony pod własnym „biegunem"
|
"""Ascendenty pod biegunami tan(P) = tan(φ)·k/3, liczone wektorowo.
|
||||||
tan(P) = tan(φ)·k/3, dla RAMC przesuniętego o pozycję domu.
|
|
||||||
|
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
|
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
|
bliski, ale nie równy (kilka sekund łuku); wyrocznia rozstrzygnęła na rzecz
|
||||||
konstrukcji „ascendent pod biegunem".
|
konstrukcji „ascendent pod biegunem"."""
|
||||||
|
er, tan_lat = math.radians(eps), math.tan(math.radians(lat))
|
||||||
Liczymy tylko domy 11, 12, 2, 3, a 5, 6, 8, 9 bierzemy jako ich OPOZYCJE —
|
epole = _ecliptic_pole(er)
|
||||||
to nie skrót, lecz własność tych systemów: przeciwległe domy leżą na tym samym
|
# Gdy MC górujące rozjeżdża się z matematycznym (za kołem podbiegunowym),
|
||||||
kole wielkim, więc ich cuspy są dokładnie antypodyczne."""
|
# cała rodzina obraca się razem z dziesiątym domem — stąd zwrot iloczynu.
|
||||||
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.
|
|
||||||
culminating = _culminating_mc(mc, eps, lat)
|
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
|
out = [0.0] * 12
|
||||||
for i in (10, 11, 1, 2): # domy 11, 12, 2, 3
|
for i, (offset, fraction) in _TOPO_STEP.items():
|
||||||
pole = math.degrees(math.atan(tan_lat * _TOPO_POLE_FRACTION[i]))
|
th = math.radians(ramc + offset)
|
||||||
lon = _asc_under_pole_raw(ramc + 30.0 * i, eps, pole)
|
pole = math.atan(tan_lat * fraction)
|
||||||
if ((lon - culminating) % 360.0 < 180.0) != _INTERMEDIATE[i]:
|
zenith = (math.cos(pole) * math.cos(th),
|
||||||
lon = norm360(lon + 180.0)
|
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] = lon
|
||||||
out[(i + 6) % 12] = norm360(lon + 180.0)
|
out[(i + 6) % 12] = norm360(lon + 180.0)
|
||||||
out[0], out[3] = asc, norm360(culminating + 180.0)
|
out[0], out[3] = asc, norm360(culminating + 180.0)
|
||||||
@@ -331,6 +341,111 @@ def _cusps_topocentric(ramc: float, eps: float, lat: float,
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ── systemy łuku dobowego (Placidus, Koch) ───────────────────────────────
|
||||||
|
# Różnią się od wszystkich poprzednich tym, że NIE MAJĄ wzoru zamkniętego: cusp
|
||||||
|
# jest zdefiniowany warunkiem na samego siebie („punkt, który przebył 1/3 swojego
|
||||||
|
# półłuku"), więc trzeba go znaleźć iteracyjnie. Mają też jako jedyne REALNĄ
|
||||||
|
# granicę dziedziny — powyżej koła podbiegunowego stopnie ekliptyki bywają
|
||||||
|
# okołobiegunowe, nie wschodzą ani nie zachodzą, i półłuk po prostu nie istnieje.
|
||||||
|
|
||||||
|
|
||||||
|
class HouseSystemUndefined(ValueError):
|
||||||
|
"""System domów nie ma definicji dla podanych parametrów (nie: błąd liczenia).
|
||||||
|
|
||||||
|
Podnoszone zamiast zwrócenia liczby, bo cicha podmiana systemu jest gorsza
|
||||||
|
niż błąd: wykres wygląda poprawnie, a planety siedzą w innych domach, niż
|
||||||
|
astrolog zamawiał. Warstwa aplikacyjna łapie to w cusps_detailed() i robi
|
||||||
|
JAWNY fallback."""
|
||||||
|
|
||||||
|
|
||||||
|
_ITER_MAX = 100
|
||||||
|
_ITER_TOL_DEG = 1e-11
|
||||||
|
|
||||||
|
|
||||||
|
def _declination_of_ecliptic_lon(lon_deg: float, eps_rad: float) -> float:
|
||||||
|
"""Deklinacja punktu LEŻĄCEGO NA EKLIPTYCE o danej długości."""
|
||||||
|
return math.degrees(math.asin(math.sin(eps_rad) * math.sin(math.radians(lon_deg))))
|
||||||
|
|
||||||
|
|
||||||
|
def _ascensional_difference(dec_deg: float, lat_deg: float) -> float:
|
||||||
|
"""Różnica wschodnia: o ile półłuk dobowy odbiega od 90°.
|
||||||
|
|
||||||
|
sin(AD) = tan(φ)·tan(δ). Gdy |tan(φ)·tan(δ)| ≥ 1, punkt jest okołobiegunowy
|
||||||
|
(nigdy nie wschodzi albo nigdy nie zachodzi) i półłuk nie istnieje."""
|
||||||
|
v = math.tan(math.radians(lat_deg)) * math.tan(math.radians(dec_deg))
|
||||||
|
if abs(v) >= 1.0:
|
||||||
|
raise HouseSystemUndefined(
|
||||||
|
f"punkt okołobiegunowy (tan φ·tan δ = {v:.6f}): półłuk dobowy nie istnieje")
|
||||||
|
return math.degrees(math.asin(v))
|
||||||
|
|
||||||
|
|
||||||
|
# Dom → (ułamek półłuku, czy łuk NOCNY). Domy 11 i 12 dzielą łuk dzienny licząc
|
||||||
|
# od MC; domy 2 i 3 — łuk nocny, licząc WSTECZ od IC.
|
||||||
|
_PLACIDUS_STEP = {10: (1 / 3, False), 11: (2 / 3, False),
|
||||||
|
1: (2 / 3, True), 2: (1 / 3, True)}
|
||||||
|
|
||||||
|
|
||||||
|
def _placidus_cusp(ramc: float, eps: float, lat: float,
|
||||||
|
fraction: float, nocturnal: bool) -> float:
|
||||||
|
"""Punkt ekliptyki, który przebył `fraction` swojego półłuku.
|
||||||
|
|
||||||
|
Warunek jest uwikłany: półłuk zależy od deklinacji, deklinacja od długości,
|
||||||
|
a długość od położenia — więc iterujemy po punkcie stałym. Zbieżność jest
|
||||||
|
szybka z dala od koła podbiegunowego i psuje się przy nim, dlatego brak
|
||||||
|
zbieżności traktujemy jako wyjście poza dziedzinę, a nie jako wynik."""
|
||||||
|
eps_rad = math.radians(eps)
|
||||||
|
# Start od podziału równomiernego — to Porphyry na równiku, czyli dokładnie
|
||||||
|
# ten przypadek, w którym Placidus się do niego sprowadza.
|
||||||
|
ra = ramc + 180.0 - 90.0 * fraction if nocturnal else ramc + 90.0 * fraction
|
||||||
|
for _ in range(_ITER_MAX):
|
||||||
|
dec = _declination_of_ecliptic_lon(_ra_to_ecliptic_lon(ra, eps_rad), eps_rad)
|
||||||
|
ad = _ascensional_difference(dec, lat)
|
||||||
|
nxt = (ramc + 180.0 - fraction * (90.0 - ad) if nocturnal
|
||||||
|
else ramc + fraction * (90.0 + ad))
|
||||||
|
if abs(nxt - ra) < _ITER_TOL_DEG:
|
||||||
|
return _ra_to_ecliptic_lon(nxt, eps_rad)
|
||||||
|
ra = nxt
|
||||||
|
raise HouseSystemUndefined(
|
||||||
|
f"brak zbieżności po {_ITER_MAX} krokach (φ={lat:.4f}, RAMC={ramc:.4f})")
|
||||||
|
|
||||||
|
|
||||||
|
def _cusps_placidus(ramc: float, eps: float, lat: float,
|
||||||
|
asc: float, mc: float) -> list[float]:
|
||||||
|
"""Półłuki dobowe i nocne dzielone na trzy — każdy punkt swoim własnym łukiem."""
|
||||||
|
if abs(lat) >= polar_circle(eps):
|
||||||
|
raise HouseSystemUndefined(
|
||||||
|
f"φ={lat:.4f}° poza kołem podbiegunowym ({polar_circle(eps):.4f}° dla ε={eps:.4f}°)")
|
||||||
|
inter = {i: _placidus_cusp(ramc, eps, lat, f, noct)
|
||||||
|
for i, (f, noct) in _PLACIDUS_STEP.items()}
|
||||||
|
inter.update({(i + 6) % 12: norm360(v + 180.0) for i, v in list(inter.items())})
|
||||||
|
return _with_exact_angles(inter, asc, mc)
|
||||||
|
|
||||||
|
|
||||||
|
# Koch dzieli CZAS, nie łuk na niebie. Kryterium: ile czasu minęło od wschodu
|
||||||
|
# tego stopnia zodiaku, który stoi na MC. Ten odcinek (półłuk dobowy stopnia MC)
|
||||||
|
# dzielimy na trzy i dla punktów podziału liczymy ZWYKŁY Ascendent — stąd nazwa
|
||||||
|
# „system miejsca urodzenia". Zgodne z definicją Astrodienst (astro.com/astrowiki).
|
||||||
|
#
|
||||||
|
# W przeciwieństwie do Placidusa NIE wymaga iteracji: półłuk zależy od deklinacji
|
||||||
|
# stopnia MC, którą znamy wprost. Granicę dziedziny dzieli natomiast z Placidusem —
|
||||||
|
# gdy stopień MC jest okołobiegunowy, „moment jego wschodu" nie istnieje.
|
||||||
|
_KOCH_OFFSET = {10: -2 / 3, 11: -1 / 3, 1: 1 / 3, 2: 2 / 3}
|
||||||
|
|
||||||
|
|
||||||
|
def _cusps_koch(ramc: float, eps: float, lat: float,
|
||||||
|
asc: float, mc: float) -> list[float]:
|
||||||
|
"""Ascendenty dla chwil trójdzielących drogę stopnia MC od wschodu do górowania."""
|
||||||
|
if abs(lat) >= polar_circle(eps):
|
||||||
|
raise HouseSystemUndefined(
|
||||||
|
f"φ={lat:.4f}° poza kołem podbiegunowym ({polar_circle(eps):.4f}° dla ε={eps:.4f}°)")
|
||||||
|
dec_mc = _declination_of_ecliptic_lon(mc, math.radians(eps))
|
||||||
|
half_arc = 90.0 + _ascensional_difference(dec_mc, lat)
|
||||||
|
inter = {i: compute_asc(ramc + f * half_arc, eps, lat)
|
||||||
|
for i, f in _KOCH_OFFSET.items()}
|
||||||
|
inter.update({(i + 6) % 12: norm360(v + 180.0) for i, v in list(inter.items())})
|
||||||
|
return _with_exact_angles(inter, asc, mc)
|
||||||
|
|
||||||
|
|
||||||
def cusps_for(ramc: float, eps: float, lat: float, system: str) -> list[float]:
|
def cusps_for(ramc: float, eps: float, lat: float, system: str) -> list[float]:
|
||||||
"""Kanoniczne wejście: (RAMC, ε, φ) → 12 cusps.
|
"""Kanoniczne wejście: (RAMC, ε, φ) → 12 cusps.
|
||||||
|
|
||||||
@@ -356,9 +471,58 @@ def cusps_for(ramc: float, eps: float, lat: float, system: str) -> list[float]:
|
|||||||
return _cusps_alcabitus(ramc, eps, asc)
|
return _cusps_alcabitus(ramc, eps, asc)
|
||||||
if system == TOPOCENTRIC:
|
if system == TOPOCENTRIC:
|
||||||
return _cusps_topocentric(ramc, eps, lat, asc, mc)
|
return _cusps_topocentric(ramc, eps, lat, asc, mc)
|
||||||
|
if system == PLACIDUS:
|
||||||
|
return _cusps_placidus(ramc, eps, lat, asc, mc)
|
||||||
|
if system == KOCH:
|
||||||
|
return _cusps_koch(ramc, eps, lat, asc, mc)
|
||||||
raise ValueError(f"nieznany system domów: {system}")
|
raise ValueError(f"nieznany system domów: {system}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── jawny fallback poza dziedziną ────────────────────────────────────────
|
||||||
|
# Placidus i Koch jako jedyne mają miejsca, w których po prostu NIE ISTNIEJĄ.
|
||||||
|
# Astrolog z Tromsø ma dostać wynik, ale musi wiedzieć, że dostał inny system —
|
||||||
|
# cicha podmiana jest gorsza niż brak wyniku, bo jest nie do wykrycia z wykresu.
|
||||||
|
|
||||||
|
FALLBACK_SYSTEM = PORPHYRY
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CuspSet:
|
||||||
|
"""12 cuspów + uczciwa informacja, czym naprawdę zostały policzone."""
|
||||||
|
|
||||||
|
cusps: list[float]
|
||||||
|
system: str # system FAKTYCZNIE użyty
|
||||||
|
requested: str # o który poproszono
|
||||||
|
reason: str | None = None # dlaczego nie dało się użyć żądanego
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_fallback(self) -> bool:
|
||||||
|
return self.system != self.requested
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notice(self) -> str | None:
|
||||||
|
"""Komunikat dla człowieka. Ma trafić na ekran, do raportu i do PDF-a."""
|
||||||
|
if not self.is_fallback:
|
||||||
|
return None
|
||||||
|
return (f"UWAGA: system domów \u201e{self.requested}\u201d nie ma definicji "
|
||||||
|
f"dla tego miejsca i czasu \u2014 {self.reason}. Domy policzono "
|
||||||
|
f"systemem \u201e{self.system}\u201d. To NIE jest ten sam podzia\u0142: "
|
||||||
|
f"pozycje planet s\u0105 poprawne, ale przypisanie ich do dom\u00f3w "
|
||||||
|
f"pochodzi z innego systemu.")
|
||||||
|
|
||||||
|
|
||||||
|
def cusps_detailed(ramc: float, eps: float, lat: float, system: str) -> CuspSet:
|
||||||
|
"""Jak cusps_for, ale zamiast wyjątku poza dziedziną robi JAWNY fallback.
|
||||||
|
|
||||||
|
cusps_for zostaje funkcją czystą i nieustępliwą (to ją porównuje wyrocznia);
|
||||||
|
ustępstwo wobec rzeczywistości jest tutaj — i zawsze zostawia ślad."""
|
||||||
|
try:
|
||||||
|
return CuspSet(cusps_for(ramc, eps, lat, system), system, system)
|
||||||
|
except HouseSystemUndefined as e:
|
||||||
|
return CuspSet(cusps_for(ramc, eps, lat, FALLBACK_SYSTEM),
|
||||||
|
FALLBACK_SYSTEM, system, str(e))
|
||||||
|
|
||||||
|
|
||||||
def assign_house(lon: float, cusp_list: list[float]) -> int:
|
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."""
|
||||||
lon = norm360(lon)
|
lon = norm360(lon)
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ def _chart_section(chart: dict, moment_label: str | None) -> str:
|
|||||||
if meta:
|
if meta:
|
||||||
lines.append(" · ".join(meta))
|
lines.append(" · ".join(meta))
|
||||||
|
|
||||||
|
# Model interpretuje domy, więc musi wiedzieć, że podział pochodzi z innego
|
||||||
|
# systemu, niż zamówiono — inaczej napisze „Twój Placidus" o Porphyrym.
|
||||||
|
for w in chart.get("house_warnings") or []:
|
||||||
|
lines.append(f"\n{w}")
|
||||||
|
|
||||||
if chart.get("positions"):
|
if chart.get("positions"):
|
||||||
lines.append("\n## Pozycje")
|
lines.append("\n## Pozycje")
|
||||||
for p in chart["positions"]:
|
for p in chart["positions"]:
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
Waliduje względem astro.com dla horoskopu referencyjnego.
|
Waliduje względem astro.com dla horoskopu referencyjnego.
|
||||||
"""
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.engine import houses as H
|
||||||
from app.engine.chart import build_chart
|
from app.engine.chart import build_chart
|
||||||
|
|
||||||
|
|
||||||
@@ -61,8 +64,31 @@ def test_primary_always_first_even_if_not_listed(own_engine, reference_moment):
|
|||||||
|
|
||||||
|
|
||||||
def test_unknown_extra_system_is_ignored(own_engine, reference_moment):
|
def test_unknown_extra_system_is_ignored(own_engine, reference_moment):
|
||||||
"""Nieobsługiwany system (np. placidus — dojdzie przez swisseph osobno) jest
|
"""Nieznana nazwa systemu jest po prostu pomijana, nie wywala horoskopu.
|
||||||
po prostu pomijany, nie wywala horoskopu."""
|
|
||||||
|
(Placidus był tu kiedyś przykładem systemu nieobsługiwanego — od Etapu 2
|
||||||
|
jest liczony, więc przykładem musi być nazwa, której naprawdę nie znamy.)"""
|
||||||
chart = build_chart(own_engine, reference_moment, "whole_sign",
|
chart = build_chart(own_engine, reference_moment, "whole_sign",
|
||||||
house_systems=["placidus", "equal"])
|
house_systems=["nie-ma-takiego-systemu", "equal"])
|
||||||
assert [h["system"] for h in chart["house_systems"]] == ["whole_sign", "equal"]
|
assert [h["system"] for h in chart["house_systems"]] == ["whole_sign", "equal"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("system", H.SYSTEMS)
|
||||||
|
def test_every_advertised_system_builds_a_chart(own_engine, reference_moment, system):
|
||||||
|
"""Każdy system z houses.SYSTEMS musi przejść przez build_chart. Pilnuje,
|
||||||
|
żeby rozszerzenie SYSTEMS bez przepięcia chart.py nie dało 500 przy wyborze
|
||||||
|
systemu, który jest na liście, ale nie w użytej funkcji."""
|
||||||
|
chart = build_chart(own_engine, reference_moment, system)
|
||||||
|
assert chart["house_system"] == system
|
||||||
|
assert len(chart["cusps"]) == 12
|
||||||
|
assert all(1 <= p["house"] <= 12 for p in chart["positions"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_systems_side_by_side_cover_all(own_engine, reference_moment):
|
||||||
|
chart = build_chart(own_engine, reference_moment, H.WHOLE_SIGN,
|
||||||
|
house_systems=list(H.SYSTEMS))
|
||||||
|
blocks = chart["house_systems"]
|
||||||
|
assert blocks[0]["system"] == H.WHOLE_SIGN # prymarny zawsze pierwszy
|
||||||
|
assert {b["system"] for b in blocks} == set(H.SYSTEMS)
|
||||||
|
for p in chart["positions"]:
|
||||||
|
assert set(p["houses"]) == set(H.SYSTEMS)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
"""Testy osi i domów — czysta matematyka (LOG-05, bez efemeryd)."""
|
"""Testy osi i domów — czysta matematyka (LOG-05, bez efemeryd)."""
|
||||||
|
import math
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.engine import houses as H
|
from app.engine import houses as H
|
||||||
@@ -81,7 +83,8 @@ def test_polar_circle_moves_with_obliquity():
|
|||||||
# Zgodność z wyrocznią sprawdza tests/oracle; tu pilnujemy niezmienników, które
|
# Zgodność z wyrocznią sprawdza tests/oracle; tu pilnujemy niezmienników, które
|
||||||
# muszą zachodzić także bez swissepha (czyli w każdym środowisku).
|
# 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)
|
@pytest.mark.parametrize("system", EXOTIC)
|
||||||
@@ -126,3 +129,104 @@ def test_morinus_ignores_latitude():
|
|||||||
def test_unknown_system_is_rejected():
|
def test_unknown_system_is_rejected():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
H.cusps_for(100.0, 23.4393, 50.0, "nie-ma-takiego")
|
H.cusps_for(100.0, 23.4393, 50.0, "nie-ma-takiego")
|
||||||
|
|
||||||
|
|
||||||
|
# ── systemy łuku dobowego: Placidus i Koch (Etap 2) ──────────────────────
|
||||||
|
|
||||||
|
DIURNAL = ("placidus", "koch")
|
||||||
|
TROMSO_LAT = 69.65 # za kołem podbiegunowym — tam te systemy nie istnieją
|
||||||
|
KRAKOW_LAT = 50.06
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("system", DIURNAL)
|
||||||
|
def test_diurnal_systems_refuse_beyond_the_polar_circle(system):
|
||||||
|
"""Odmowa, nie liczba. Cicha podmiana systemu jest niewykrywalna z wykresu."""
|
||||||
|
with pytest.raises(H.HouseSystemUndefined):
|
||||||
|
H.cusps_for(100.0, 23.4393, TROMSO_LAT, system)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("system", DIURNAL)
|
||||||
|
def test_fallback_says_what_it_did(system):
|
||||||
|
cs = H.cusps_detailed(100.0, 23.4393, TROMSO_LAT, system)
|
||||||
|
assert cs.is_fallback and cs.system == H.FALLBACK_SYSTEM and cs.requested == system
|
||||||
|
assert len(cs.cusps) == 12
|
||||||
|
assert cs.notice and system in cs.notice and H.FALLBACK_SYSTEM in cs.notice
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("system", DIURNAL)
|
||||||
|
def test_no_fallback_and_no_notice_where_the_system_exists(system):
|
||||||
|
cs = H.cusps_detailed(100.0, 23.4393, KRAKOW_LAT, system)
|
||||||
|
assert not cs.is_fallback and cs.notice is None
|
||||||
|
assert cs.cusps == H.cusps_for(100.0, 23.4393, KRAKOW_LAT, system)
|
||||||
|
|
||||||
|
|
||||||
|
def test_placidus_cusps_satisfy_their_own_definition():
|
||||||
|
"""Sprawdzian NIEZALEŻNY od wyroczni: czy znaleziony punkt naprawdę przebył
|
||||||
|
zadany ułamek swojego półłuku. Wyrocznia mówi „zgadza się z swissephem",
|
||||||
|
to mówi „zgadza się z definicją Placidusa" — i działa bez swissepha."""
|
||||||
|
ramc, eps, lat = 212.9986, 23.4393, 66.3144 # tuż pod kołem, najtrudniej
|
||||||
|
eps_rad = math.radians(eps)
|
||||||
|
c = H.cusps_for(ramc, eps, lat, "placidus")
|
||||||
|
for house, (frac, nocturnal) in ((11, (1 / 3, False)), (12, (2 / 3, False)),
|
||||||
|
(2, (2 / 3, True)), (3, (1 / 3, True))):
|
||||||
|
lon = c[house - 1]
|
||||||
|
dec = H._declination_of_ecliptic_lon(lon, eps_rad)
|
||||||
|
ad = H._ascensional_difference(dec, lat)
|
||||||
|
ra = math.degrees(math.atan2(math.sin(math.radians(lon)) * math.cos(eps_rad),
|
||||||
|
math.cos(math.radians(lon))))
|
||||||
|
want = (ramc + 180.0 - frac * (90.0 - ad) if nocturnal
|
||||||
|
else ramc + frac * (90.0 + ad))
|
||||||
|
assert abs((ra - want + 180.0) % 360.0 - 180.0) < 1e-9, f"dom {house}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_koch_reduces_to_equal_spacing_on_the_equator():
|
||||||
|
"""Na równiku półłuk każdego stopnia to dokładnie 90°, więc trójdzielenie
|
||||||
|
czasu daje Ascendenty co 30° czasu gwiazdowego — dobry test zdrowia wzoru."""
|
||||||
|
ramc, eps = 100.0, 23.4393
|
||||||
|
c = H.cusps_for(ramc, eps, 0.0, "koch")
|
||||||
|
for house, offset in ((11, -60.0), (12, -30.0), (2, 30.0), (3, 60.0)):
|
||||||
|
assert c[house - 1] == pytest.approx(
|
||||||
|
H.compute_asc(ramc + offset, eps, 0.0), abs=1e-9), f"dom {house}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_polar_circle_boundary_moves_with_the_date():
|
||||||
|
"""Granica dziedziny NIE jest stałą 66,56° — zależy od ε, czyli od daty."""
|
||||||
|
lat = 66.60
|
||||||
|
assert lat > H.polar_circle(23.4393) # dziś: poza dziedziną
|
||||||
|
assert lat < H.polar_circle(23.3528) # rok 2270: już w dziedzinie
|
||||||
|
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
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Katalog systemów domów dla warstwy prezentacji — JEDNO źródło nazw.
|
||||||
|
|
||||||
|
Nazwy były zaszyte w czterech szablonach naraz (selektor na „Horoskop", selektor
|
||||||
|
na „Skompiluj", checkboxy porównania, nagłówki tabeli). Przy trzech systemach
|
||||||
|
uchodziło to na sucho; przy dziesięciu rozjazd jest kwestią czasu — wystarczy
|
||||||
|
dopisać system w jednym miejscu i zapomnieć o trzech pozostałych.
|
||||||
|
|
||||||
|
Kolejność jest celowa: najpierw dwa najpowszechniejsze (Placidus i Whole Sign),
|
||||||
|
potem reszta. `LIMITED` oznacza systemy, które powyżej koła podbiegunowego nie
|
||||||
|
istnieją i podlegają jawnemu fallbackowi — UI ma o tym uprzedzać ZANIM ktoś
|
||||||
|
policzy horoskop dla Tromsø, a nie dopiero w ostrzeżeniu przy wyniku.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
HOUSE_SYSTEMS: list[tuple[str, str]] = [
|
||||||
|
("placidus", "Placidus"),
|
||||||
|
("whole_sign", "Whole Sign"),
|
||||||
|
("koch", "Koch"),
|
||||||
|
("equal", "Equal"),
|
||||||
|
("porphyry", "Porphyry"),
|
||||||
|
("regiomontanus", "Regiomontanus"),
|
||||||
|
("campanus", "Campanus"),
|
||||||
|
("alcabitus", "Alcabitus"),
|
||||||
|
("topocentric", "Topocentric"),
|
||||||
|
("morinus", "Morinus"),
|
||||||
|
("vehlow", "Vehlow"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Systemy oparte na łuku dobowym — jedyne z realną granicą dziedziny.
|
||||||
|
LIMITED = frozenset({"placidus", "koch"})
|
||||||
|
|
||||||
|
HOUSE_SYSTEM_NAMES: dict[str, str] = dict(HOUSE_SYSTEMS)
|
||||||
|
|
||||||
|
|
||||||
|
def label(system: str) -> str:
|
||||||
|
"""Nazwa do pokazania; nieznany identyfikator zwracamy bez zmian, nie gubimy go."""
|
||||||
|
return HOUSE_SYSTEM_NAMES.get(system, system)
|
||||||
@@ -22,6 +22,8 @@ from fastapi.responses import HTMLResponse, JSONResponse
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from app.house_systems import HOUSE_SYSTEMS, LIMITED as HOUSE_LIMITED, label as house_label
|
||||||
|
|
||||||
from app import geocode, security
|
from app import geocode, security
|
||||||
from app.clients.logic_client import LogicClient
|
from app.clients.logic_client import LogicClient
|
||||||
from app.config import DEFAULT_LOCATION_LABEL, default_form
|
from app.config import DEFAULT_LOCATION_LABEL, default_form
|
||||||
@@ -54,6 +56,10 @@ def static_url(name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
templates.env.globals["static"] = static_url
|
templates.env.globals["static"] = static_url
|
||||||
|
# Katalog systemów domów — jedno źródło nazw dla wszystkich szablonów.
|
||||||
|
templates.env.globals["HOUSE_SYSTEMS"] = HOUSE_SYSTEMS
|
||||||
|
templates.env.globals["house_label"] = house_label
|
||||||
|
templates.env.globals["HOUSE_LIMITED"] = HOUSE_LIMITED
|
||||||
|
|
||||||
|
|
||||||
def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]:
|
def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]:
|
||||||
@@ -141,6 +147,10 @@ def chart_compute(
|
|||||||
aspect_minor=aspect_minor,
|
aspect_minor=aspect_minor,
|
||||||
stations=stations, zodiac=zodiac, tables=tables,
|
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
|
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"])
|
||||||
ctx["aspectarian_svg"] = chartwheel.render_aspectarian(ctx["result"]) # PRE-18
|
ctx["aspectarian_svg"] = chartwheel.render_aspectarian(ctx["result"]) # PRE-18
|
||||||
@@ -242,6 +252,7 @@ def compile_pdf(payload: dict):
|
|||||||
return JSONResponse({"detail": f"Niepoprawne dane wejściowe: {e}"}, status_code=422)
|
return JSONResponse({"detail": f"Niepoprawne dane wejściowe: {e}"}, status_code=422)
|
||||||
|
|
||||||
figures: list[dict] = []
|
figures: list[dict] = []
|
||||||
|
warnings: list[str] = []
|
||||||
try:
|
try:
|
||||||
chart = logic.positions(
|
chart = logic.positions(
|
||||||
when_utc_iso=iso_utc,
|
when_utc_iso=iso_utc,
|
||||||
@@ -275,6 +286,7 @@ def compile_pdf(payload: dict):
|
|||||||
"person": payload.get("person") or "",
|
"person": payload.get("person") or "",
|
||||||
"data": {**data, "moment_utc": label},
|
"data": {**data, "moment_utc": label},
|
||||||
"figures": figures,
|
"figures": figures,
|
||||||
|
"warnings": warnings,
|
||||||
"natal": payload.get("natal") or {},
|
"natal": payload.get("natal") or {},
|
||||||
"predictions": payload.get("predictions") or [],
|
"predictions": payload.get("predictions") or [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,18 @@ button:hover { filter: brightness(1.1); }
|
|||||||
.opts { display: flex; gap: 1.5rem; margin-top: .75rem; color: var(--muted); align-items: center; }
|
.opts { display: flex; gap: 1.5rem; margin-top: .75rem; color: var(--muted); align-items: center; }
|
||||||
.opts input[type=number] { width: 5rem; }
|
.opts input[type=number] { width: 5rem; }
|
||||||
.meta { color: var(--muted); margin: 1.5rem 0 .5rem; font-size: .9rem; }
|
.meta { color: var(--muted); margin: 1.5rem 0 .5rem; font-size: .9rem; }
|
||||||
|
|
||||||
|
/* Fallback systemu domów. Celowo NIE jest to dyskretny „muted" — po fallbacku
|
||||||
|
kosmogram wygląda bezbłędnie, więc ten akapit jest jedynym sygnałem, że
|
||||||
|
podział domów pochodzi z innego systemu, niż zamówiono. */
|
||||||
|
.house-warning {
|
||||||
|
margin: 1rem 0; padding: .75rem 1rem; font-size: .92rem; line-height: 1.45;
|
||||||
|
border: 1px solid var(--warn-border, #b8860b);
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--warn-bg, rgba(184, 134, 11, .10));
|
||||||
|
color: var(--warn-fg, inherit);
|
||||||
|
}
|
||||||
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
.error { background: #3a1320; border: 1px solid #6a2233; color: #ffb3c0; padding: .75rem 1rem; border-radius: 8px; margin-top: 1.5rem; }
|
||||||
.empty { color: var(--muted); }
|
.empty { color: var(--muted); }
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: .5rem; background: var(--panel); border-radius: 12px; overflow: hidden; }
|
table { width: 100%; border-collapse: collapse; margin-top: .5rem; background: var(--panel); border-radius: 12px; overflow: hidden; }
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<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.">
|
<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 [] %}
|
{% set chosen = form.house_systems or [] %}
|
||||||
<span class="muted small">Porównaj systemy domów:</span>
|
<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>
|
{% for value, name in HOUSE_SYSTEMS %}
|
||||||
<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="{{ value }}" {{ 'checked' if value in chosen else '' }}> {{ name }}{{ ' *' if value in HOUSE_LIMITED else '' }}</label>
|
||||||
<label><input type="checkbox" name="house_systems" value="porphyry" {{ 'checked' if 'porphyry' in chosen else '' }}> Porphyry</label>
|
{% endfor %}
|
||||||
</div>
|
</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).">
|
<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>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>
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
deklinacja/antyscja) renderuje rodzic; tu są same tabele. Każda sekcja pokazuje
|
deklinacja/antyscja) renderuje rodzic; tu są same tabele. Każda sekcja pokazuje
|
||||||
się tylko, gdy jej dane są w wyniku (opcja policzona → sekcja jest). #}
|
się tylko, gdy jej dane są w wyniku (opcja policzona → sekcja jest). #}
|
||||||
|
|
||||||
|
{# Fallback systemu domów: system nie istniał dla tego miejsca i policzyliśmy
|
||||||
|
inny. Musi być WIDOCZNE, bo z samego wykresu tego nie da się poznać. #}
|
||||||
|
{% if result.house_warnings %}
|
||||||
|
{% for w in result.house_warnings %}
|
||||||
|
<p class="house-warning">{{ w }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if result.angles %}
|
{% if result.angles %}
|
||||||
<table class="angles">
|
<table class="angles">
|
||||||
<thead><tr><th>Oś</th><th>Znak</th><th>W znaku</th></tr></thead>
|
<thead><tr><th>Oś</th><th>Znak</th><th>W znaku</th></tr></thead>
|
||||||
@@ -20,10 +28,9 @@
|
|||||||
|
|
||||||
{# Porównanie systemów domów (PRE-05) — te same osie, inny podział na domy #}
|
{# 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 %}
|
{% 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>
|
<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">
|
<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>
|
<thead><tr><th>Dom</th>{% for hs in result.house_systems %}<th>{{ house_label(hs.system) }}</th>{% endfor %}</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for i in range(12) %}
|
{% for i in range(12) %}
|
||||||
<tr><td>{{ i + 1 }}</td>
|
<tr><td>{{ i + 1 }}</td>
|
||||||
@@ -219,7 +226,7 @@
|
|||||||
|
|
||||||
{% if result.cusps %}
|
{% if result.cusps %}
|
||||||
<details class="loc">
|
<details class="loc">
|
||||||
<summary>Cusps domów ({{ result.house_system }})</summary>
|
<summary>Cusps domów ({{ house_label(result.house_system) }})</summary>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Dom</th><th>Znak</th><th>Cusp</th></tr></thead>
|
<thead><tr><th>Dom</th><th>Znak</th><th>Cusp</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -34,10 +34,11 @@
|
|||||||
<label>System domów
|
<label>System domów
|
||||||
<select name="house_system">
|
<select name="house_system">
|
||||||
{% set hs = form.house_system or 'whole_sign' %}
|
{% set hs = form.house_system or 'whole_sign' %}
|
||||||
<option value="whole_sign" {{ 'selected' if hs == 'whole_sign' else '' }}>Whole Sign</option>
|
{% for value, name in HOUSE_SYSTEMS %}
|
||||||
<option value="equal" {{ 'selected' if hs == 'equal' else '' }}>Equal</option>
|
<option value="{{ value }}" {{ 'selected' if hs == value else '' }}>{{ name }}{{ ' *' if value in HOUSE_LIMITED else '' }}</option>
|
||||||
<option value="porphyry" {{ 'selected' if hs == 'porphyry' else '' }}>Porphyry</option>
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<span class="muted small" title="Placidus i Koch dzielą łuk dobowy, który powyżej koła podbiegunowego (ok. 66,5°) nie istnieje — dla takich miejsc policzymy Porphyry i wyraźnie o tym napiszemy.">* nie działa za kołem podbiegunowym</span>
|
||||||
</label>
|
</label>
|
||||||
<label>Zodiak
|
<label>Zodiak
|
||||||
<select name="zodiac">
|
<select name="zodiac">
|
||||||
@@ -65,10 +66,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if result %}
|
{% if result %}
|
||||||
|
{# Fallback systemu domów — z kosmogramu nie da się go poznać, więc mówimy wprost #}
|
||||||
|
{% for w in result.house_warnings or [] %}
|
||||||
|
<p class="house-warning">{{ w }}</p>
|
||||||
|
{% endfor %}
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
Silnik: <strong>{{ result.engine }}</strong> ·
|
Silnik: <strong>{{ result.engine }}</strong> ·
|
||||||
obiektów: {{ result.positions | length }}
|
obiektów: {{ result.positions | length }}
|
||||||
{% if result.house_system %}· domy: {{ result.house_system }}{% endif %}
|
{% if result.house_system %}· domy: {{ house_label(result.house_system) }}{% endif %}
|
||||||
{% if result.zodiac %}· zodiak: {{ result.zodiac }}{% if result.ayanamsha is defined %} (ayanamsa {{ '%.4f'|format(result.ayanamsha) }}°){% endif %}{% endif %}
|
{% if result.zodiac %}· zodiak: {{ result.zodiac }}{% if result.ayanamsha is defined %} (ayanamsa {{ '%.4f'|format(result.ayanamsha) }}°){% endif %}{% endif %}
|
||||||
{% if moment %}· moment: {{ moment }}{% endif %}
|
{% if moment %}· moment: {{ moment }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie t
|
|||||||
<label>System domów
|
<label>System domów
|
||||||
<select name="house_system">
|
<select name="house_system">
|
||||||
{% set hs = form.house_system or 'whole_sign' %}
|
{% set hs = form.house_system or 'whole_sign' %}
|
||||||
<option value="whole_sign" {{ 'selected' if hs == 'whole_sign' else '' }}>Whole Sign</option>
|
{% for value, name in HOUSE_SYSTEMS %}
|
||||||
<option value="equal" {{ 'selected' if hs == 'equal' else '' }}>Equal</option>
|
<option value="{{ value }}" {{ 'selected' if hs == value else '' }}>{{ name }}{{ ' *' if value in HOUSE_LIMITED else '' }}</option>
|
||||||
<option value="porphyry" {{ 'selected' if hs == 'porphyry' else '' }}>Porphyry</option>
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<span class="muted small" title="Placidus i Koch dzielą łuk dobowy, który powyżej koła podbiegunowego (ok. 66,5°) nie istnieje — dla takich miejsc policzymy Porphyry i wyraźnie o tym napiszemy.">* nie działa za kołem podbiegunowym</span>
|
||||||
</label>
|
</label>
|
||||||
<label>Zodiak
|
<label>Zodiak
|
||||||
<select name="zodiac">
|
<select name="zodiac">
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
|||||||
OPTS = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8") # wspólne opcje
|
OPTS = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8") # wspólne opcje
|
||||||
TABLES = (APP / "templates" / "_result_tables.html").read_text(encoding="utf-8") # wspólne tabele
|
TABLES = (APP / "templates" / "_result_tables.html").read_text(encoding="utf-8") # wspólne tabele
|
||||||
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
from app.house_systems import HOUSE_SYSTEMS, LIMITED, label as house_label # noqa: E402
|
||||||
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
@@ -20,10 +22,37 @@ def test_both_forms_use_shared_option_partial():
|
|||||||
assert '{% include "_form_options.html" %}' in COMPILE
|
assert '{% include "_form_options.html" %}' in COMPILE
|
||||||
|
|
||||||
|
|
||||||
def test_form_lets_you_pick_extra_systems():
|
def _render(name: str, **ctx) -> str:
|
||||||
"""Checkboxy dla trzech systemów — wielokrotny wybór (name powtórzony)."""
|
"""Renderuje szablon z tymi samymi globalsami, co produkcja.
|
||||||
for val in ("whole_sign", "equal", "porphyry"):
|
|
||||||
assert f'name="house_systems" value="{val}"' in OPTS, f"brak checkboxa {val}"
|
Szablony budują listę systemów PĘTLĄ po katalogu, więc sprawdzanie ich
|
||||||
|
surowego tekstu nic już nie mówi — trzeba je wykonać. Przy okazji test
|
||||||
|
łapie literówki w Jinja, których grep po źródle nigdy nie wyłapał."""
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
env = Environment(loader=FileSystemLoader(str(APP / "templates")), autoescape=True)
|
||||||
|
env.globals.update(HOUSE_SYSTEMS=HOUSE_SYSTEMS, HOUSE_LIMITED=LIMITED,
|
||||||
|
house_label=house_label, static=lambda p: p)
|
||||||
|
return env.get_template(name).render(**ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_offers_every_system_the_engine_advertises():
|
||||||
|
"""Każdy system z katalogu ma checkbox. Katalog jest jednym źródłem nazw,
|
||||||
|
więc dopisanie systemu bez odświeżenia UI zapali się tutaj."""
|
||||||
|
html = _render("_form_options.html", form={})
|
||||||
|
for value, name in HOUSE_SYSTEMS:
|
||||||
|
assert f'name="house_systems" value="{value}"' in html, f"brak checkboxa {value}"
|
||||||
|
assert name in html, f"brak nazwy {name}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_marks_systems_with_a_domain_limit():
|
||||||
|
"""Placidus i Koch nie istnieją za kołem podbiegunowym — UI ma uprzedzać
|
||||||
|
ZANIM ktoś policzy horoskop dla Tromsø, nie dopiero w ostrzeżeniu wyniku."""
|
||||||
|
html = _render("_form_options.html", form={})
|
||||||
|
assert LIMITED, "katalog nie oznacza żadnego systemu jako ograniczonego"
|
||||||
|
for value, name in HOUSE_SYSTEMS:
|
||||||
|
starred = f"{name} *" in html
|
||||||
|
assert starred == (value in LIMITED), f"{value}: gwiazdka={starred}"
|
||||||
|
|
||||||
|
|
||||||
def test_handler_accepts_and_passes_house_systems():
|
def test_handler_accepts_and_passes_house_systems():
|
||||||
@@ -49,4 +78,21 @@ def test_comparison_table_shows_cusp_per_system():
|
|||||||
|
|
||||||
def test_checkbox_state_survives_submit():
|
def test_checkbox_state_survives_submit():
|
||||||
"""Zaznaczone systemy zostają zaznaczone po przeliczeniu."""
|
"""Zaznaczone systemy zostają zaznaczone po przeliczeniu."""
|
||||||
assert "in chosen" in OPTS and "form.house_systems" in OPTS
|
html = _render("_form_options.html", form={"house_systems": ["koch", "campanus"]})
|
||||||
|
for value, _ in HOUSE_SYSTEMS:
|
||||||
|
marker = f'value="{value}" checked'
|
||||||
|
assert (marker in html) == (value in ("koch", "campanus")), value
|
||||||
|
|
||||||
|
|
||||||
|
def test_fallback_notice_is_shown_prominently():
|
||||||
|
"""Po fallbacku kosmogram wygląda bezbłędnie — ostrzeżenie jest jedynym
|
||||||
|
sygnałem, że podział domów jest z innego systemu."""
|
||||||
|
html = _render("_result_tables.html",
|
||||||
|
result={"house_warnings": ["UWAGA: system domów nie ma definicji"]})
|
||||||
|
assert "house-warning" in html
|
||||||
|
assert "UWAGA: system domów nie ma definicji" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_notice_when_nothing_was_substituted():
|
||||||
|
html = _render("_result_tables.html", result={"house_warnings": []})
|
||||||
|
assert "house-warning" not in html
|
||||||
|
|||||||
@@ -210,6 +210,22 @@ def build(report: dict, wheel_pdf: str | None = None,
|
|||||||
parts.append(r"\end{tabular}")
|
parts.append(r"\end{tabular}")
|
||||||
parts.append(r"\vspace{1em}")
|
parts.append(r"\vspace{1em}")
|
||||||
|
|
||||||
|
# ── 2a. ostrzeżenia o systemie domów ────────────────────────────────
|
||||||
|
# Muszą stać PRZED rysunkami: kosmogram narysowany po fallbacku wygląda
|
||||||
|
# bezbłędnie, więc jedyną informacją, że podział domów jest z innego systemu,
|
||||||
|
# jest ten akapit. W ramce, żeby nie dało się go wziąć za podpis.
|
||||||
|
warnings = [w for w in (report.get("warnings") or []) if str(w).strip()]
|
||||||
|
if warnings:
|
||||||
|
parts.append(r"\begin{center}")
|
||||||
|
parts.append(r"\fbox{\begin{minipage}{0.92\textwidth}\small")
|
||||||
|
for i, w in enumerate(warnings):
|
||||||
|
if i:
|
||||||
|
parts.append(r"\par\medskip")
|
||||||
|
parts.append(esc(str(w)))
|
||||||
|
parts.append(r"\end{minipage}}")
|
||||||
|
parts.append(r"\end{center}")
|
||||||
|
parts.append(r"\vspace{1em}")
|
||||||
|
|
||||||
# ── 3. rysunki kosmogramu ───────────────────────────────────────────
|
# ── 3. rysunki kosmogramu ───────────────────────────────────────────
|
||||||
# Jedna reguła na wszystkie: keepaspectratio z limitem SZEROKOŚCI i WYSOKOŚCI.
|
# Jedna reguła na wszystkie: keepaspectratio z limitem SZEROKOŚCI i WYSOKOŚCI.
|
||||||
# Koło i aspektarian są kwadratowe (ogranicza je wysokość), deklinacja i
|
# Koło i aspektarian są kwadratowe (ogranicza je wysokość), deklinacja i
|
||||||
|
|||||||
@@ -307,3 +307,24 @@ def test_full_ai_markdown_leaves_no_visible_markers():
|
|||||||
"###Podsekcja bez spacji\n\n- punkt **ważny**\n- i ***kluczowy***\n\n"
|
"###Podsekcja bez spacji\n\n- punkt **ważny**\n- i ***kluczowy***\n\n"
|
||||||
"#### Zamknięty ####")
|
"#### Zamknięty ####")
|
||||||
assert _no_visible_markers(md(src))
|
assert _no_visible_markers(md(src))
|
||||||
|
|
||||||
|
|
||||||
|
def test_house_fallback_warning_lands_in_the_pdf():
|
||||||
|
"""Zasada projektu: co pokazujemy na stronie, trafia do raportu i PDF-a.
|
||||||
|
|
||||||
|
Po fallbacku systemu domów kosmogram wygląda bezbłędnie, więc ten akapit
|
||||||
|
jest JEDYNĄ informacją, że podział domów pochodzi z innego systemu, niż
|
||||||
|
zamówiono. Musi też stać PRZED rysunkami, nie po nich."""
|
||||||
|
tex = build({"person": "Jan Kowalski",
|
||||||
|
"data": {"date": "1984-04-30", "house_system": "porphyry"},
|
||||||
|
"warnings": ["UWAGA: system domów „placidus” nie ma definicji"]},
|
||||||
|
figures=[{"pdf": "wheel.pdf", "caption": "Kosmogram"}])
|
||||||
|
assert "fbox" in tex
|
||||||
|
assert "nie ma definicji" in tex
|
||||||
|
assert tex.index("nie ma definicji") < tex.index("wheel.pdf"), \
|
||||||
|
"ostrzeżenie musi stać przed rysunkami"
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_warning_box_when_nothing_was_substituted():
|
||||||
|
tex = build({"person": "Jan Kowalski", "data": {}, "warnings": []})
|
||||||
|
assert "fbox" not in tex
|
||||||
|
|||||||
+47
-9
@@ -77,17 +77,55 @@ zarówno zestaw brzegowy (2520 porównań), jak i losowy przemiał 20 000 przypa
|
|||||||
| regiomontanus | podział równika, koła przez punkty N/S horyzontu | 0,000000000° |
|
| regiomontanus | podział równika, koła przez punkty N/S horyzontu | 0,000000000° |
|
||||||
| campanus | podział wertykału pierwszego | 0,000000000° |
|
| campanus | podział wertykału pierwszego | 0,000000000° |
|
||||||
| alcabitus | podział łuków dobowych po równiku | 0,000000000° |
|
| alcabitus | podział łuków dobowych po równiku | 0,000000000° |
|
||||||
|
| topocentric | ascendenty pod biegunami tan(P) = tan(φ)·k/3 | 0,000000000° |
|
||||||
|
|
||||||
**Topocentric (Polich–Page) jest zaimplementowany, ale NIE wypuszczony** — nie ma go
|
Etap 2 — systemy **łuku dobowego**, jedyne z realną granicą dziedziny:
|
||||||
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.
|
|
||||||
|
|
||||||
Placidus i Koch (iteracyjne, z realną granicą dziedziny na kole podbiegunowym) —
|
| System | Konstrukcja | Maks. odchylenie |
|
||||||
Etap 2.
|
|---|---|---|
|
||||||
|
| placidus | trójdzielenie półłuków, iteracyjnie | 0,000001686° |
|
||||||
|
| koch | trójdzielenie CZASU od wschodu stopnia MC | 0,000000000° |
|
||||||
|
|
||||||
|
Placidus jest jedynym systemem bez wzoru zamkniętego: cusp jest zdefiniowany
|
||||||
|
warunkiem na samego siebie, więc szukamy go iteracyjnie do 1e-11°. Pozostałe
|
||||||
|
1,7e-6° to **próg zbieżności wyroczni**, nie nasz błąd — nasz cusp spełnia
|
||||||
|
definicję Placidusa z dokładnością 1e-12° (test `test_placidus_cusps_satisfy_
|
||||||
|
their_own_definition` sprawdza to bez swissepha).
|
||||||
|
|
||||||
|
Powyżej koła podbiegunowego oba **odmawiają liczenia** — i wyrocznia odmawia
|
||||||
|
dokładnie tych samych przypadków (5245 na 20 000 losowych, zero rozjazdów).
|
||||||
|
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 — 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.
|
||||||
|
|
||||||
## Licencja
|
## Licencja
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import random
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
# Zakres dat programu jako dni juliańskie (rok astronomiczny: 370 p.n.e. = -369).
|
# 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_MIN = 1_566_839.5 # -369-01-01
|
||||||
JD_MAX = 2_695_000.5 # ~2270-12-31
|
JD_MAX = 2_695_000.5 # ~2270-12-31
|
||||||
JD_J2000 = 2_451_545.0 # punkt odniesienia — środek dziedziny
|
JD_J2000 = 2_451_545.0 # punkt odniesienia — środek dziedziny
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ from harness import ( # noqa: E402
|
|||||||
# Systemy do sprawdzenia. Rośnie wraz z implementacją kolejnych (Etap 1 i 2) —
|
# Systemy do sprawdzenia. Rośnie wraz z implementacją kolejnych (Etap 1 i 2) —
|
||||||
# dopisanie nazwy tutaj wystarcza, żeby weszła do każdego builda.
|
# dopisanie nazwy tutaj wystarcza, żeby weszła do każdego builda.
|
||||||
SYSTEMS = ["whole_sign", "equal", "porphyry", "vehlow", "morinus",
|
SYSTEMS = ["whole_sign", "equal", "porphyry", "vehlow", "morinus",
|
||||||
"regiomontanus", "campanus", "alcabitus"]
|
"regiomontanus", "campanus", "alcabitus", "topocentric", "placidus", "koch"]
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
|||||||
Reference in New Issue
Block a user