mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 21:38:37 +00:00
Silnik efemeryd: EngineProvider + SkyfieldEngine + harness porównawczy
Pierwszy increment implementacji warstwy logicznej (ścieżka A). LOG-24: interfejs EphemerisEngine z dwoma backendami — SkyfieldEngine (własny, permisywny: Skyfield MIT + dane JPL public domain) oraz RemoteEngine (klient izolowanej usługi swisseph). Fabryka + leniwa inicjalizacja; endpointy /chart/positions i /chart/compare. LOG-01: pozycje obiektów (długość/szerokość ekliptyczna, prędkość, kierunek, formaty: w znaku / absolutny / dziesiętny). LOG-25/28: harness porównawczy (compare.py) z progami tolerancji oraz wspólny kontrakt parzystości; pełen zestaw testów. LOG-27: services/engine-swisseph — osobna, opcjonalna usługa AGPL (pyswisseph, tryb Moshiera), licencjonowana osobno, w compose pod profilem "comparison"; nie wchodzi do zamkniętego produktu. Walidacja: SkyfieldEngine zgadza się ze Swiss Ephemeris co do ~1" dla wszystkich 10 obiektów na horoskopie referencyjnym (30.04.1984, Warszawa); 12 testów przechodzi (silnik B pomijany gdy nieskonfigurowany). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""Modele domenowe silnika efemeryd — wspólny kontrakt dla KAŻDEGO silnika.
|
||||
|
||||
To jest część LOG-28 (parzystość): każdy silnik (własny Skyfield czy zdalny
|
||||
swisseph) przyjmuje `ChartMoment` i zwraca listę `ObjectPosition` w identycznym
|
||||
kształcie i jednostkach. Dzięki temu wyniki są bezpośrednio porównywalne, a
|
||||
prezentacja/dane nie wiedzą, który silnik liczył.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from app.engine import formats
|
||||
|
||||
# kanoniczny zestaw i kolejność obiektów (LOG-02: światła + 7 klasycznych + 3 nowożytne)
|
||||
DEFAULT_OBJECTS = [
|
||||
"Sun", "Moon", "Mercury", "Venus", "Mars",
|
||||
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChartMoment:
|
||||
"""Wejście silnika: moment w UTC + lokalizacja geograficzna.
|
||||
|
||||
Pozycje obiektów zależą tylko od czasu (geocentrycznie); szerokość/długość
|
||||
geograficzna będą potrzebne dopiero przy osiach i domach (LOG-05).
|
||||
"""
|
||||
|
||||
when_utc: datetime # musi być świadome strefy (UTC)
|
||||
lat: float = 0.0 # szerokość geograficzna, + na północ
|
||||
lon: float = 0.0 # długość geograficzna, + na wschód
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ObjectPosition:
|
||||
"""Wynik dla jednego obiektu — surowe wartości w jednostkach SI astrologii."""
|
||||
|
||||
name: str
|
||||
longitude: float # długość ekliptyczna 0–360° (tropikalna, of-date)
|
||||
latitude: float # szerokość ekliptyczna (°)
|
||||
speed: float # prędkość w długości (°/dobę)
|
||||
retrograde: bool
|
||||
|
||||
@property
|
||||
def sign(self) -> str:
|
||||
return formats.SIGNS[formats.sign_index(self.longitude)]
|
||||
|
||||
@property
|
||||
def direction(self) -> str:
|
||||
return "Rx" if self.retrograde else "D"
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"sign": self.sign,
|
||||
"in_sign": formats.in_sign(self.longitude),
|
||||
"absolute": formats.absolute(self.longitude),
|
||||
"decimal": formats.decimal(self.longitude),
|
||||
"latitude": round(self.latitude, 6),
|
||||
"speed": round(self.speed, 6),
|
||||
"direction": self.direction,
|
||||
}
|
||||
Reference in New Issue
Block a user