9d5b65ddbd
- engine/firdaria.py: sekta (dzień = Słońce nad horyzontem, ta sama półkula osi Asc-Dsc co MC); kolejność diurnalna/nokturnalna; klasyczne długości okresów (Su10 Ve8 Me13 Mo9 Sa11 Ju12 Ma7 + NN3 + SN2 = 75 lat); okresy główne planet dzielone na 7 podokresów (sub-lord od władcy okresu), węzły bez sub. - endpoint /chart/firdaria. - oś czasu: firdaria_events — starty (pod)okresów w oknie; wpięte w build_timeline (domyślnie) + tokeny [major][sub] do dopięcia interpretacji (1B->2B). Walidacja: - sekta = day dla horoskopu referencyjnego (zgodnie z notes3 "Day birth"); night gdy Słońce po stronie IC; sumy i przyleganie okresów; podokresy 7x sumujące się do okresu; wiek 42 w okresie Saturna. - E2E: okresy Sun 1984-1994 ... Saturn 2024-2035; w osi czasu "Firdaria: Saturn / Mars" 2027 -> 223 interpretacje ([Sa+[Ma -> "injury"). - 78 testów przechodzi (nowy test_firdaria). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
6.7 KiB
Python
161 lines
6.7 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 firdaria_events(natal_points: dict[str, float], birth: datetime, lo: datetime, hi: datetime) -> list[dict]:
|
|
"""Starty okresów/podokresów Firdarii (LOG-11) nachodzące na okno."""
|
|
from app.engine.firdaria import firdaria
|
|
|
|
fd = firdaria(birth, natal_points["Sun"], natal_points["Asc"], natal_points["MC"])
|
|
out: list[dict] = []
|
|
for period in fd["periods"]:
|
|
if "sub" in period:
|
|
for s in period["sub"]:
|
|
if lo <= _as_dt(s["start"]) <= hi:
|
|
row = _row("firdaria", f"Firdaria: {period['lord']} / {s['lord']}",
|
|
s["start"], s["start"], s["end"])
|
|
row.update(fd_major=period["lord"], fd_sub=s["lord"])
|
|
out.append(row)
|
|
elif lo <= _as_dt(period["start"]) <= hi: # węzeł — bez podokresów
|
|
row = _row("firdaria", f"Firdaria: {period['lord']}",
|
|
period["start"], period["start"], period["end"])
|
|
row.update(fd_major=period["lord"])
|
|
out.append(row)
|
|
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", "firdaria"])
|
|
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)
|
|
if "firdaria" in want:
|
|
events += firdaria_events(natal_points, birth, lo, hi)
|
|
events.sort(key=lambda e: e["exact"])
|
|
return events
|