mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 21:38:37 +00:00
2dbd3410e9
engine/timeline.py: scala w jedną posortowaną oś czasu (technique | significator | start | exact | end): - profekcje roczne (LOG-10) — Władca Roku per 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 (klucz Naiboda 0°59'08"/rok, konfigurowalny; okno orbowe ±1 rok). Endpoint /chart/timeline (zakres from_date..to_date, wybór technik). Walidacja: - profekcje spójne z tabelą notes3; dyrekcja Sun koniunkcja MC (łuk 312°) słusznie poza życiem; oś posortowana po dacie dokładnej; wiek = łuk/klucz spójny z datą. - E2E: dla 2025-2026 zwraca 19 zdarzeń (m.in. Władca Roku 42 Saturn 30.04.2026, Solar Return, dyr. Venus koniunkcja North Node 27.05.2025). 66 testów przechodzi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
5.3 KiB
Python
132 lines
5.3 KiB
Python
"""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
|