"""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