mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 13:34:38 +00:00
Merge pull request #19 from migatu/feat/timeline-log14
Zbiorcza tabela dat z technik (LOG-14)
This commit is contained in:
@@ -10,6 +10,7 @@ i nie w bazie.
|
||||
- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów, odsiewaniem duplikatów (ten sam sygnifikator i opis), rankingiem siły (LOG-21) oraz opcją group (grupowanie identycznych opisów)
|
||||
- `POST /chart/profections` → `{when_utc, lat, lon, start_age?, count?}` → profekcje roczne: wiek, profektowany Asc, Władca Roku (+MC/Su/Mo) (LOG-10)
|
||||
- `POST /chart/return` → `{when_utc, lat, lon, kind, around?}` → Solar/Lunar Return: moment powrotu + pełny horoskop na ten moment (LOG-12)
|
||||
- `POST /chart/timeline` → `{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc, posortowane (technique | significator | start | exact | end) (LOG-14)
|
||||
- `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
|
||||
- `GET /health` (sprawdza też warstwę bazodanową)
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Zbiorcza tabela dat z technik (LOG-14).
|
||||
|
||||
Spina w jedną, posortowaną oś czasu daty z kilku technik:
|
||||
- profekcje roczne (LOG-10) — rok życia,
|
||||
- Solar Return (LOG-12) — moment powrotu Słońca,
|
||||
- dyrekcje solar-arc — daty dokładnych aspektów kierowanych planet do punktów
|
||||
natalnych (wzorzec z notes3: „Profection planet | Aspect | Birth planet |
|
||||
Exact Date"). Klucz łuku konfigurowalny; domyślnie Naiboda (0°59'08"/rok).
|
||||
|
||||
Każdy wiersz ma kształt z notes2: technique | significator | start | exact | end.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from app.engine.aspects import MAJOR, PL_NAME
|
||||
from app.engine.profections import DOMICILE_RULERS, profected_sign
|
||||
from app.engine.returns import find_return
|
||||
|
||||
NAIBOD_KEY = 0.9856472 # °/rok (0°59'08") — domyślny klucz solar-arc
|
||||
DAYS_PER_YEAR = 365.2422
|
||||
DIRECTED = ["Sun", "Moon", "Mercury", "Venus", "Mars",
|
||||
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"]
|
||||
|
||||
|
||||
def _add_years(birth: datetime, years: float) -> datetime:
|
||||
return birth + timedelta(days=years * DAYS_PER_YEAR)
|
||||
|
||||
|
||||
def _row(technique, significator, start, exact, end) -> dict:
|
||||
def iso(x):
|
||||
return x.date().isoformat() if isinstance(x, datetime) else x
|
||||
return {"technique": technique, "significator": significator,
|
||||
"start": iso(start), "exact": iso(exact), "end": iso(end)}
|
||||
|
||||
|
||||
def solar_arc_directions(
|
||||
natal: dict[str, float], birth: datetime, lo: datetime, hi: datetime,
|
||||
key: float = NAIBOD_KEY, orb_years: float = 1.0,
|
||||
) -> list[dict]:
|
||||
"""Daty dyrekcji solar-arc w oknie [lo, hi].
|
||||
|
||||
natal: nazwa punktu -> długość natalna (planety + Asc/MC). Kierowane są planety
|
||||
(DIRECTED), celem każdy punkt natalny. Aspekt dokładny gdy łuk = odległość
|
||||
kątowa (mod 360). Wiek = łuk/klucz; data = urodziny + wiek.
|
||||
"""
|
||||
out: list[dict] = []
|
||||
lo_age = (lo - birth).days / DAYS_PER_YEAR - orb_years
|
||||
hi_age = (hi - birth).days / DAYS_PER_YEAR + orb_years
|
||||
for p in DIRECTED:
|
||||
if p not in natal:
|
||||
continue
|
||||
for q, q_lon in natal.items():
|
||||
for asp, angle in MAJOR.items():
|
||||
for target in ({angle, (360.0 - angle) % 360.0}):
|
||||
arc = (q_lon + target - natal[p]) % 360.0
|
||||
age = arc / key
|
||||
if not (lo_age <= age <= hi_age) or (p == q and arc < 1e-6):
|
||||
continue
|
||||
exact = _add_years(birth, age)
|
||||
out.append(_row(
|
||||
"solar_arc",
|
||||
f"dyr. {p} {PL_NAME[asp]} {q}",
|
||||
_add_years(birth, age - orb_years),
|
||||
exact,
|
||||
_add_years(birth, age + orb_years),
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def profection_events(natal_asc: float, birth: datetime, lo: datetime, hi: datetime) -> list[dict]:
|
||||
"""Lata profekcyjne (LOG-10) nachodzące na okno."""
|
||||
out: list[dict] = []
|
||||
for age in range((lo.year - birth.year) - 1, (hi.year - birth.year) + 1):
|
||||
if age < 0:
|
||||
continue
|
||||
try:
|
||||
start = birth.replace(year=birth.year + age)
|
||||
end = birth.replace(year=birth.year + age + 1)
|
||||
except ValueError: # 29 lutego
|
||||
start = birth.replace(year=birth.year + age, day=28)
|
||||
end = birth.replace(year=birth.year + age + 1, day=28)
|
||||
if end < lo or start > hi:
|
||||
continue
|
||||
sign = profected_sign(natal_asc, age)
|
||||
out.append(_row(
|
||||
"profection", f"Władca Roku: {DOMICILE_RULERS[sign]} (Asc {sign}, wiek {age})",
|
||||
start, start, end,
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def solar_return_events(engine, natal_moment, birth: datetime, lo: datetime, hi: datetime) -> list[dict]:
|
||||
"""Solariusze w oknie (LOG-12) — jeden na rok."""
|
||||
out: list[dict] = []
|
||||
for year in range(lo.year, hi.year + 1):
|
||||
try:
|
||||
around = birth.replace(year=year)
|
||||
except ValueError:
|
||||
around = birth.replace(year=year, day=28)
|
||||
hit = find_return(engine, "solar", natal_moment, around)
|
||||
if hit and lo <= hit <= hi:
|
||||
out.append(_row("solar_return", "Solar Return", hit, hit, _add_years(hit, 1)))
|
||||
return out
|
||||
|
||||
|
||||
def _as_dt(d) -> datetime:
|
||||
if isinstance(d, datetime):
|
||||
return d if d.tzinfo else d.replace(tzinfo=timezone.utc)
|
||||
if isinstance(d, date):
|
||||
return datetime(d.year, d.month, d.day, tzinfo=timezone.utc)
|
||||
return datetime.fromisoformat(str(d)).replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def build_timeline(
|
||||
engine, natal_moment, natal_points: dict[str, float],
|
||||
from_d, to_d, techniques: list[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Scala wybrane techniki w jedną oś czasu, posortowaną po dacie dokładnej."""
|
||||
lo, hi = _as_dt(from_d), _as_dt(to_d)
|
||||
birth = natal_moment.when_utc
|
||||
want = set(techniques or ["profection", "solar_return", "solar_arc"])
|
||||
events: list[dict] = []
|
||||
if "profection" in want:
|
||||
events += profection_events(natal_points["Asc"], birth, lo, hi)
|
||||
if "solar_return" in want:
|
||||
events += solar_return_events(engine, natal_moment, birth, lo, hi)
|
||||
if "solar_arc" in want:
|
||||
events += solar_arc_directions(natal_points, birth, lo, hi)
|
||||
events.sort(key=lambda e: e["exact"])
|
||||
return events
|
||||
@@ -173,6 +173,33 @@ def chart_return(req: ReturnRequest) -> dict:
|
||||
"return_utc": hit.isoformat(), **chart}
|
||||
|
||||
|
||||
class TimelineRequest(BaseModel):
|
||||
when_utc: datetime # moment urodzenia (UTC)
|
||||
lat: float = 0.0
|
||||
lon: float = 0.0
|
||||
from_date: str # zakres: YYYY-MM-DD
|
||||
to_date: str
|
||||
techniques: list[str] | None = None # profection | solar_return | solar_arc
|
||||
|
||||
|
||||
@app.post("/chart/timeline")
|
||||
def chart_timeline(req: TimelineRequest) -> dict:
|
||||
"""Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end."""
|
||||
from app.engine import houses as H
|
||||
from app.engine.models import ChartMoment
|
||||
from app.engine.timeline import build_timeline
|
||||
|
||||
engine = get_engine()
|
||||
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||
ramc, eps = engine.sidereal(natal)
|
||||
points = {"Asc": H.compute_asc(ramc, eps, natal.lat), "MC": H.compute_mc(ramc, eps)}
|
||||
for p in engine.positions(natal):
|
||||
points[p.name] = p.longitude
|
||||
events = build_timeline(engine, natal, points, req.from_date, req.to_date, req.techniques)
|
||||
return {"engine": engine.name, "from": req.from_date, "to": req.to_date,
|
||||
"count": len(events), "events": events}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict:
|
||||
info = {"status": "ok", "layer": "logic"}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Zbiorcza oś czasu z technik (LOG-14)."""
|
||||
import datetime as dt
|
||||
|
||||
from app.engine.timeline import (
|
||||
NAIBOD_KEY,
|
||||
build_timeline,
|
||||
profection_events,
|
||||
solar_arc_directions,
|
||||
)
|
||||
|
||||
BIRTH = dt.datetime(1984, 4, 30, 7, 35, tzinfo=dt.timezone.utc)
|
||||
# natalne długości (z horoskopu referencyjnego)
|
||||
NATAL = {
|
||||
"Asc": 112.18, "MC": 352.59, "Sun": 40.14, "Moon": 30.55, "Mercury": 27.38,
|
||||
"Venus": 27.68, "Mars": 234.54, "Jupiter": 282.96, "Saturn": 223.31,
|
||||
"Uranus": 252.81, "Neptune": 271.22, "Pluto": 210.48,
|
||||
}
|
||||
|
||||
|
||||
def _win(y0, y1):
|
||||
return (dt.datetime(y0, 1, 1, tzinfo=dt.timezone.utc),
|
||||
dt.datetime(y1, 12, 31, tzinfo=dt.timezone.utc))
|
||||
|
||||
|
||||
def test_profection_events_in_window():
|
||||
lo, hi = _win(2024, 2026)
|
||||
rows = profection_events(NATAL["Asc"], BIRTH, lo, hi)
|
||||
# rok profekcyjny wiek 42 zaczyna się 30.04.2026 -> Asc Capricorn
|
||||
ages = [r["significator"] for r in rows]
|
||||
assert any("Asc Capricorn" in a and "wiek 42" in a for a in ages)
|
||||
|
||||
|
||||
def test_solar_arc_exact_matches_arc_over_key():
|
||||
lo, hi = _win(2020, 2030)
|
||||
rows = solar_arc_directions(NATAL, BIRTH, lo, hi, key=NAIBOD_KEY)
|
||||
assert rows, "brak dyrekcji w oknie"
|
||||
# dla każdej dyrekcji: łuk = (wiek * klucz), a data = urodziny + wiek -> spójne
|
||||
for r in rows[:5]:
|
||||
exact = dt.date.fromisoformat(r["exact"])
|
||||
age_years = (dt.datetime(exact.year, exact.month, exact.day, tzinfo=dt.timezone.utc)
|
||||
- BIRTH).days / 365.2422
|
||||
assert 36 <= age_years <= 47 # okno 2020-2030 = wiek ~36-46
|
||||
assert r["technique"] == "solar_arc" and "dyr." in r["significator"]
|
||||
|
||||
|
||||
def test_directed_sun_conjunct_natal_mc_date():
|
||||
# Sun natal 40.14 -> MC natal 352.59: łuk koniunkcji = (352.59-40.14)%360 = 312.45
|
||||
# to > lifespan przy Naibod (~317 lat) -> NIE powinno być w oknie życia
|
||||
lo, hi = _win(1984, 2084)
|
||||
rows = solar_arc_directions(NATAL, BIRTH, lo, hi)
|
||||
sun_mc = [r for r in rows if r["significator"] == "dyr. Sun koniunkcja MC"]
|
||||
assert not sun_mc # łuk 312° = poza życiem
|
||||
|
||||
|
||||
def test_build_timeline_sorted_and_merged():
|
||||
events = build_timeline(_FakeEngine(), _FakeNatal(), NATAL,
|
||||
"2025-01-01", "2027-01-01",
|
||||
techniques=["profection", "solar_arc"])
|
||||
assert events
|
||||
dates = [e["exact"] for e in events]
|
||||
assert dates == sorted(dates) # posortowane po dacie dokładnej
|
||||
techs = {e["technique"] for e in events}
|
||||
assert "profection" in techs and "solar_arc" in techs
|
||||
|
||||
|
||||
class _FakeNatal:
|
||||
when_utc = BIRTH
|
||||
|
||||
|
||||
class _FakeEngine:
|
||||
"""Silnik-atrapa — build_timeline z solar_return by go użył, tu go pomijamy."""
|
||||
Reference in New Issue
Block a user