feat(domy): Placidus i Koch + jawny fallback poza kołem podbiegunowym
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m30s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m28s
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 8s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m30s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m28s
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 8s
Etap 2: dwa systemy łuku dobowego — jedyne, które mają miejsca, gdzie po prostu NIE ISTNIEJĄ. Koch zgadza się z wyrocznią co do zera; Placidus do 1,7e-6°, przy czym to próg zbieżności WYROCZNI, nie nasz: nasze cuspy spełniają definicję Placidusa z dokładnością 1e-12° (osobny test, działa bez swissepha). Placidus jako jedyny nie ma wzoru zamkniętego — cusp jest zdefiniowany warunkiem na samego siebie („punkt, który przebył 1/3 swojego półłuku"), więc iterujemy po punkcie stałym do 1e-11°. Brak zbieżności traktujemy jako wyjście poza dziedzinę, nie jako wynik. Koch okazał się natomiast ZAMKNIĘTY: jego kryterium to czas od wschodu stopnia stojącego na MC, a półłuk tego stopnia znamy wprost z jego deklinacji. Definicja za Astrodienst (astro.com/astrowiki/en/Koch_House_System) — nie zgadywana. Powyżej koła podbiegunowego oba odmawiają liczenia i wyrocznia odmawia dokładnie tych samych przypadków (5245/20 000 losowych, zero rozjazdów dziedziny). Odmowa jest wyjątkiem, nie liczbą: cicha podmiana systemu jest niewykrywalna z wykresu. Ustępstwo wobec rzeczywistości siedzi osobno, w cusps_detailed(): podstawia Porphyry'ego i ZAWSZE zostawia ślad. Ten ślad idzie wszystkimi trzema wyjściami — na ekran (ramka, nie „muted"), w prompt do modelu (inaczej napisze „Twój Placidus" o Porphyrym) i do PDF-a, w ramce PRZED rysunkami. Astrolog z Tromsø dostaje wynik i wie, że go dostał inaczej. Przy okazji: nazwy systemów były zaszyte w czterech szablonach naraz. Przy trzech systemach uchodziło to na sucho, przy dziesięciu nie — jest katalog w jednym miejscu, a testy szablonów RENDERUJĄ je zamiast szukać tekstu w źródle, więc łapią też literówki w Jinja. Większe jądro efemeryd (de441): świadomie zdegradowane do „nice to have" — rozszerza wyłącznie zakres dat, nie poprawia niczego w obecnym. Odnotowane w domain.py przy JD_MIN/JD_MAX, żeby nie wróciło po cichu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,9 +97,11 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
||||
asc = H.compute_asc(ramc, eps, moment.lat)
|
||||
mc = H.compute_mc(ramc, eps)
|
||||
system = house_system if house_system in H.SYSTEMS else H.WHOLE_SIGN
|
||||
# cusps_for, nie cusps: to drugie zna tylko trzy systemy dzielące ekliptykę.
|
||||
# Systemy egzotyczne dzielą inne koła wielkie i potrzebują pełnego (RAMC, ε, φ).
|
||||
cusp_list = H.cusps_for(ramc, eps, moment.lat, 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]:
|
||||
"""Cuspy → wiersze pod UI/kosmogram: znak, stopień w znaku, długość, glif."""
|
||||
@@ -111,7 +113,11 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
||||
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"] = {
|
||||
"Asc": _fmt("Asc", asc, off),
|
||||
"MC": _fmt("MC", mc, off),
|
||||
@@ -134,10 +140,15 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
||||
if len(ordered) > 1:
|
||||
result["house_systems"] = []
|
||||
for s in ordered:
|
||||
cl = cusp_list if s == system else H.cusps_for(ramc, eps, moment.lat, s)
|
||||
result["house_systems"].append({"system": s, "cusps": _cusps_out(cl)})
|
||||
cs = primary if s == system else H.cusps_detailed(ramc, eps, moment.lat, s)
|
||||
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):
|
||||
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)
|
||||
from app.engine.firdaria import is_day_birth
|
||||
|
||||
@@ -8,6 +8,7 @@ silnik dostarcza tylko RAMC i ε.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.engine.formats import SIGN_ABBR, norm360, sign_index # noqa: F401
|
||||
|
||||
@@ -22,6 +23,8 @@ REGIOMONTANUS = "regiomontanus"
|
||||
CAMPANUS = "campanus"
|
||||
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°),
|
||||
@@ -29,7 +32,7 @@ TOPOCENTRIC = "topocentric"
|
||||
# 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)
|
||||
REGIOMONTANUS, CAMPANUS, ALCABITUS, PLACIDUS, KOCH)
|
||||
|
||||
|
||||
def mean_obliquity(tt_jd: float) -> float:
|
||||
@@ -331,6 +334,111 @@ def _cusps_topocentric(ramc: float, eps: float, lat: float,
|
||||
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]:
|
||||
"""Kanoniczne wejście: (RAMC, ε, φ) → 12 cusps.
|
||||
|
||||
@@ -356,9 +464,58 @@ def cusps_for(ramc: float, eps: float, lat: float, system: str) -> list[float]:
|
||||
return _cusps_alcabitus(ramc, eps, asc)
|
||||
if system == TOPOCENTRIC:
|
||||
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}")
|
||||
|
||||
|
||||
# ── 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:
|
||||
"""Numer domu (1..12), w którym leży dana długość ekliptyczna."""
|
||||
lon = norm360(lon)
|
||||
|
||||
@@ -200,6 +200,11 @@ def _chart_section(chart: dict, moment_label: str | None) -> str:
|
||||
if 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"):
|
||||
lines.append("\n## Pozycje")
|
||||
for p in chart["positions"]:
|
||||
|
||||
@@ -64,10 +64,12 @@ def test_primary_always_first_even_if_not_listed(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
|
||||
po prostu pomijany, nie wywala horoskopu."""
|
||||
"""Nieznana nazwa systemu jest po prostu pomijana, 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",
|
||||
house_systems=["placidus", "equal"])
|
||||
house_systems=["nie-ma-takiego-systemu", "equal"])
|
||||
assert [h["system"] for h in chart["house_systems"]] == ["whole_sign", "equal"]
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Testy osi i domów — czysta matematyka (LOG-05, bez efemeryd)."""
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from app.engine import houses as H
|
||||
@@ -126,3 +128,71 @@ def test_morinus_ignores_latitude():
|
||||
def test_unknown_system_is_rejected():
|
||||
with pytest.raises(ValueError):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user