mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 21:38:37 +00:00
Silnik: osie (Asc/MC) i systemy domów (LOG-05)
Kontynuacja silnika efemeryd o osie i domy. - engine/houses.py: czysta matematyka sferyczna — Asc, MC (z RAMC + ε + φ), cusps dla Whole Sign / Equal / Porphyry, przypisanie obiektu do domu. - SkyfieldEngine.sidereal(): RAMC (lokalny apparent ST) + średnie nachylenie ekliptyki ze Skyfielda. - engine/chart.py: build_chart() składa pełny horoskop (pozycje + osie + domy). - Endpoint /chart/positions rozszerzony o house_system i zwraca angles + cusps + numer domu per obiekt. - Prezentacja: lokalizacja i wybór systemu domów w formularzu, tabela osi, kolumna Dom, rozwijane cusps. Walidacja względem astro.com (30.04.1984, Warszawa): Asc Can 22°10'43", MC Pis 22°35'29" (~1' od referencji); wszystkie przypisania domów Whole Sign zgodne (Sun 11, Mercury 10, Mars 5, ...). 20 testów przechodzi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""Złożenie pełnego horoskopu: pozycje + osie + domy (LOG-01 + LOG-05).
|
||||
|
||||
Silnik-agnostyczne: potrzebuje tylko `positions()` oraz (dla osi/domów)
|
||||
`sidereal()`. Jeśli silnik nie umie policzyć czasu gwiazdowego, zwraca same
|
||||
pozycje.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.engine import houses as H
|
||||
from app.engine.base import EphemerisEngine
|
||||
from app.engine.formats import SIGNS, in_sign, norm360, sign_index
|
||||
from app.engine.models import ChartMoment
|
||||
|
||||
|
||||
def _fmt(name: str, lon: float) -> dict:
|
||||
return {
|
||||
"name": name,
|
||||
"sign": SIGNS[sign_index(lon)],
|
||||
"in_sign": in_sign(lon),
|
||||
"decimal": round(norm360(lon), 6),
|
||||
}
|
||||
|
||||
|
||||
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN) -> dict:
|
||||
positions = engine.positions(moment)
|
||||
result: dict = {"engine": engine.name, "positions": [p.as_dict() for p in positions]}
|
||||
|
||||
if not hasattr(engine, "sidereal"):
|
||||
return result
|
||||
|
||||
ramc, eps = engine.sidereal(moment)
|
||||
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
|
||||
cusp_list = H.cusps(asc, mc, system)
|
||||
|
||||
result["house_system"] = system
|
||||
result["angles"] = {
|
||||
"Asc": _fmt("Asc", asc),
|
||||
"MC": _fmt("MC", mc),
|
||||
"Dsc": _fmt("Dsc", norm360(asc + 180.0)),
|
||||
"IC": _fmt("IC", norm360(mc + 180.0)),
|
||||
}
|
||||
result["cusps"] = [
|
||||
{"house": i + 1, "sign": SIGNS[sign_index(c)], "in_sign": in_sign(c)}
|
||||
for i, c in enumerate(cusp_list)
|
||||
]
|
||||
for pdict, obj in zip(result["positions"], positions):
|
||||
pdict["house"] = H.assign_house(obj.longitude, cusp_list)
|
||||
return result
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Osie i domy — czysta matematyka sferyczna (LOG-05).
|
||||
|
||||
Bezstanowe funkcje: z lokalnego czasu gwiazdowego (RAMC), nachylenia ekliptyki
|
||||
(ε) i szerokości geograficznej (φ) wyliczają Ascendent i MC, a stąd cusps domów
|
||||
dla prostych systemów (Whole Sign, Equal, Porphyry). Niezależne od silnika —
|
||||
silnik dostarcza tylko RAMC i ε.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from app.engine.formats import SIGN_ABBR, norm360, sign_index # noqa: F401
|
||||
|
||||
WHOLE_SIGN = "whole_sign"
|
||||
EQUAL = "equal"
|
||||
PORPHYRY = "porphyry"
|
||||
SYSTEMS = (WHOLE_SIGN, EQUAL, PORPHYRY)
|
||||
|
||||
|
||||
def mean_obliquity(tt_jd: float) -> float:
|
||||
"""Średnie nachylenie ekliptyki [°] dla daty (Julian TT). Wystarcza do domów."""
|
||||
t = (tt_jd - 2451545.0) / 36525.0
|
||||
arcsec = 84381.448 - 46.8150 * t - 0.00059 * t * t + 0.001813 * t ** 3
|
||||
return arcsec / 3600.0
|
||||
|
||||
|
||||
def compute_mc(ramc_deg: float, eps_deg: float) -> float:
|
||||
r, e = math.radians(ramc_deg), math.radians(eps_deg)
|
||||
mc = math.atan2(math.sin(r), math.cos(r) * math.cos(e))
|
||||
return norm360(math.degrees(mc))
|
||||
|
||||
|
||||
def compute_asc(ramc_deg: float, eps_deg: float, lat_deg: float) -> float:
|
||||
r, e, phi = math.radians(ramc_deg), math.radians(eps_deg), math.radians(lat_deg)
|
||||
asc = math.atan2(
|
||||
math.cos(r),
|
||||
-(math.sin(r) * math.cos(e) + math.tan(phi) * math.sin(e)),
|
||||
)
|
||||
return norm360(math.degrees(asc))
|
||||
|
||||
|
||||
def _trisect(a: float, b: float) -> tuple[float, float]:
|
||||
"""Dwa punkty dzielące łuk a→b (w kierunku zodiaku) na trzy równe części."""
|
||||
span = (b - a) % 360.0
|
||||
return norm360(a + span / 3.0), norm360(a + 2.0 * span / 3.0)
|
||||
|
||||
|
||||
def cusps(asc: float, mc: float, system: str) -> list[float]:
|
||||
"""Zwraca 12 cusps (długości) domów 1..12."""
|
||||
if system == WHOLE_SIGN:
|
||||
start = sign_index(asc) * 30.0
|
||||
return [norm360(start + 30.0 * i) for i in range(12)]
|
||||
if system == EQUAL:
|
||||
return [norm360(asc + 30.0 * i) for i in range(12)]
|
||||
if system == PORPHYRY:
|
||||
dsc, ic = norm360(asc + 180.0), norm360(mc + 180.0)
|
||||
c = [0.0] * 12
|
||||
c[0], c[3], c[6], c[9] = asc, ic, dsc, mc
|
||||
c[1], c[2] = _trisect(asc, ic) # domy 2,3
|
||||
c[4], c[5] = _trisect(ic, dsc) # domy 5,6
|
||||
c[7], c[8] = _trisect(dsc, mc) # domy 8,9
|
||||
c[10], c[11] = _trisect(mc, asc) # domy 11,12
|
||||
return c
|
||||
raise ValueError(f"nieznany system domów: {system}")
|
||||
|
||||
|
||||
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)
|
||||
for i in range(12):
|
||||
start = cusp_list[i]
|
||||
end = cusp_list[(i + 1) % 12]
|
||||
span = (end - start) % 360.0
|
||||
offset = (lon - start) % 360.0
|
||||
if offset < span:
|
||||
return i + 1
|
||||
return 12
|
||||
@@ -88,5 +88,17 @@ class SkyfieldEngine(EphemerisEngine):
|
||||
)
|
||||
return out
|
||||
|
||||
def sidereal(self, moment: ChartMoment) -> tuple[float, float]:
|
||||
"""(RAMC, ε) w stopniach — lokalny apparent sidereal time i nachylenie ekliptyki.
|
||||
|
||||
Materiał wejściowy do osi i domów (LOG-05). RAMC = GAST·15 + długość geo.
|
||||
"""
|
||||
from app.engine.houses import mean_obliquity
|
||||
|
||||
t = self.ts.from_datetime(moment.when_utc)
|
||||
ramc = norm360(t.gast * 15.0 + moment.lon)
|
||||
eps = mean_obliquity(t.tt)
|
||||
return ramc, eps
|
||||
|
||||
def health(self) -> dict:
|
||||
return {"engine": self.name, "status": "ok", "kernel": self.kernel}
|
||||
|
||||
Reference in New Issue
Block a user