mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 21:38:37 +00:00
c4a181b810
Realizuje przepływ 1B->2B z notes2: predykcyjne sygnifikatory (z datami)
dopasowane do interpretacji z bazy.
Logika:
- timeline.py: zdarzenia niosą strukturę (directed/aspect/target dla dyrekcji,
lord/sign dla profekcji) do budowy tokenów.
- significators.interpret_events + _event_tokens: z każdego zdarzenia buduje
tokeny bazy (dyrekcja: [planeta][aspekt][cel]; profekcja: [władca][znak]) i
dopina interpretacje reużywając _facet_samples (AND tokenów, dedup, rozwinięcie).
- /chart/timeline: flaga interpret=true.
Prezentacja:
- nowa strona /timeline "Kalendarz": formularz (urodzenie + zakres dat) -> oś
czasu z technikami, datami i interpretacjami; nawigacja + wspólne now.js.
Walidacja E2E na realnym main_base.xlsx (2025-2026):
- dyr. Saturn kwadratura MC -> 50 interpret. ("...5th house" -> "abortion/miscarriage")
- Władca Roku Saturn (wiek 42) -> 63; strona renderuje badge dat/technik.
71 testów przechodzi (nowy test_timeline_interpret).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
5.5 KiB
Python
137 lines
5.5 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)
|
|
row = _row(
|
|
"solar_arc",
|
|
f"dyr. {p} {PL_NAME[asp]} {q}",
|
|
_add_years(birth, age - orb_years),
|
|
exact,
|
|
_add_years(birth, age + orb_years),
|
|
)
|
|
row.update(directed=p, aspect=asp, target=q) # do budowy tokenów (1B->2B)
|
|
out.append(row)
|
|
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)
|
|
lord = DOMICILE_RULERS[sign]
|
|
row = _row(
|
|
"profection", f"Władca Roku: {lord} (Asc {sign}, wiek {age})",
|
|
start, start, end,
|
|
)
|
|
row.update(lord=lord, sign=sign) # do budowy tokenów (1B->2B)
|
|
out.append(row)
|
|
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
|