From 9d5b65ddbd260e436abc818aeaa5043466881c44 Mon Sep 17 00:00:00 2001 From: migatu Date: Fri, 17 Jul 2026 22:48:53 +0200 Subject: [PATCH] =?UTF-8?q?Firdaria=20(LOG-11)=20=E2=80=94=20perska=20tech?= =?UTF-8?q?nika=20time-lord?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- services/logic/README.md | 3 +- services/logic/app/engine/firdaria.py | 57 ++++++++++++++++++++++++++ services/logic/app/engine/timeline.py | 26 +++++++++++- services/logic/app/main.py | 21 ++++++++++ services/logic/app/significators.py | 9 +++++ services/logic/tests/test_firdaria.py | 58 +++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 services/logic/app/engine/firdaria.py create mode 100644 services/logic/tests/test_firdaria.py diff --git a/services/logic/README.md b/services/logic/README.md index f8115a2..30c97a6 100644 --- a/services/logic/README.md +++ b/services/logic/README.md @@ -10,7 +10,8 @@ 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); z interpret=true dopina interpretacje z bazy do dat (LOG-14, 1B->2B) +- `POST /chart/firdaria` → `{when_utc, lat, lon}` → Firdaria: sekta (dzień/noc), okresy główne i podokresy time-lordów (LOG-11) +- `POST /chart/timeline` → `{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc + Firdaria, posortowane (technique | significator | start | exact | end); z interpret=true dopina interpretacje z bazy do dat (LOG-14, 1B->2B) - `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B) - `GET /health` (sprawdza też warstwę bazodanową) diff --git a/services/logic/app/engine/firdaria.py b/services/logic/app/engine/firdaria.py new file mode 100644 index 0000000..2c1f6fa --- /dev/null +++ b/services/logic/app/engine/firdaria.py @@ -0,0 +1,57 @@ +"""Firdaria (LOG-11) — perska technika time-lord. + +Sekwencja okresów głównych zależy od sekty (dzień/noc). Sekta: urodzenie dzienne, +gdy Słońce jest nad horyzontem, czyli po tej samej stronie osi Asc–Dsc co MC. + +Klasyczne długości okresów (lata): Su 10, Ve 8, Me 13, Mo 9, Sa 11, Ju 12, Ma 7 +(razem 70) + Węzeł Północny 3 + Węzeł Południowy 2 = 75 lat. Każdy okres główny +planety dzieli się na 7 podokresów (sub-lord w tej samej kolejności, cyklicznie). +Węzły — bez podokresów (najczęstsza konwencja). +""" +from __future__ import annotations + +from datetime import datetime, timedelta + +DAY_ORDER = ["Sun", "Venus", "Mercury", "Moon", "Saturn", "Jupiter", "Mars"] +NIGHT_ORDER = ["Moon", "Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury"] +YEARS = {"Sun": 10, "Venus": 8, "Mercury": 13, "Moon": 9, + "Saturn": 11, "Jupiter": 12, "Mars": 7} +NODES = [("North Node", 3), ("South Node", 2)] +DAYS_PER_YEAR = 365.2422 + + +def is_day_birth(sun_lon: float, asc: float, mc: float) -> bool: + """Słońce nad horyzontem = ta sama półkula osi Asc–Dsc co MC.""" + return (((sun_lon - asc) % 360.0) < 180.0) == (((mc - asc) % 360.0) < 180.0) + + +def _date(birth: datetime, years: float) -> str: + return (birth + timedelta(days=years * DAYS_PER_YEAR)).date().isoformat() + + +def firdaria(birth: datetime, sun_lon: float, asc: float, mc: float) -> dict: + """Pełny rozkład Firdarii: sekta, kolejność, okresy główne i podokresy.""" + day = is_day_birth(sun_lon, asc, mc) + order = DAY_ORDER if day else NIGHT_ORDER + majors = [(lord, YEARS[lord]) for lord in order] + NODES + + periods: list[dict] = [] + age = 0.0 + for lord, yrs in majors: + period = {"lord": lord, "years": yrs, + "start": _date(birth, age), "end": _date(birth, age + yrs)} + if lord in YEARS: # planeta -> 7 podokresów + sub_len = yrs / 7.0 + i = order.index(lord) + sub_age = age + subs: list[dict] = [] + for k in range(7): + sub_lord = order[(i + k) % 7] + subs.append({"lord": sub_lord, + "start": _date(birth, sub_age), + "end": _date(birth, sub_age + sub_len)}) + sub_age += sub_len + period["sub"] = subs + periods.append(period) + age += yrs + return {"sect": "day" if day else "night", "order": order, "periods": periods} diff --git a/services/logic/app/engine/timeline.py b/services/logic/app/engine/timeline.py index 2eac9a0..7e8158f 100644 --- a/services/logic/app/engine/timeline.py +++ b/services/logic/app/engine/timeline.py @@ -109,6 +109,28 @@ def solar_return_events(engine, natal_moment, birth: datetime, lo: datetime, hi: 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) @@ -124,7 +146,7 @@ def build_timeline( """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"]) + 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) @@ -132,5 +154,7 @@ def build_timeline( 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 diff --git a/services/logic/app/main.py b/services/logic/app/main.py index b3a5c50..572b526 100644 --- a/services/logic/app/main.py +++ b/services/logic/app/main.py @@ -173,6 +173,27 @@ def chart_return(req: ReturnRequest) -> dict: "return_utc": hit.isoformat(), **chart} +class FirdariaRequest(BaseModel): + when_utc: datetime # moment urodzenia (UTC) + lat: float = 0.0 + lon: float = 0.0 + + +@app.post("/chart/firdaria") +def chart_firdaria(req: FirdariaRequest) -> dict: + """Firdaria (LOG-11): sekta + okresy główne i podokresy time-lordów.""" + from app.engine import houses as H + from app.engine.firdaria import firdaria + from app.engine.models import ChartMoment + + engine = get_engine() + natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon) + ramc, eps = engine.sidereal(natal) + asc, mc = H.compute_asc(ramc, eps, natal.lat), H.compute_mc(ramc, eps) + sun = engine.positions(natal, ["Sun"])[0].longitude + return {"engine": engine.name, **firdaria(req.when_utc, sun, asc, mc)} + + class TimelineRequest(BaseModel): when_utc: datetime # moment urodzenia (UTC) lat: float = 0.0 diff --git a/services/logic/app/significators.py b/services/logic/app/significators.py index cdc5dad..01c008e 100644 --- a/services/logic/app/significators.py +++ b/services/logic/app/significators.py @@ -222,6 +222,15 @@ def _event_tokens(event: dict) -> list[str]: if sign: toks.append("[" + sign) return toks if lord else [] + if event.get("technique") == "firdaria": + major = PLANET_ABBR.get(event.get("fd_major")) + sub = PLANET_ABBR.get(event.get("fd_sub")) + toks = [] + if major: + toks.append("[" + major) + if sub: + toks.append("[" + sub) + return toks if major else [] return [] diff --git a/services/logic/tests/test_firdaria.py b/services/logic/tests/test_firdaria.py new file mode 100644 index 0000000..930d4cf --- /dev/null +++ b/services/logic/tests/test_firdaria.py @@ -0,0 +1,58 @@ +"""Firdaria (LOG-11) — sekta, kolejność, długości okresów, podokresy.""" +import datetime as dt + +from app.engine.firdaria import DAY_ORDER, NIGHT_ORDER, YEARS, firdaria, is_day_birth +from app.significators import _event_tokens + +BIRTH = dt.datetime(1984, 4, 30, 7, 35, tzinfo=dt.timezone.utc) +# horoskop referencyjny: Asc 112.18, MC 352.59, Sun 40.14 -> Day birth (notes3) +ASC, MC, SUN = 112.18, 352.59, 40.14 + + +def test_reference_is_day_birth(): + assert is_day_birth(SUN, ASC, MC) is True # notes3: "Day birth (Diurnal)" + + +def test_night_birth_uses_night_order(): + # Słońce po stronie IC (przeciwna półkula) -> noc + assert is_day_birth((SUN + 180) % 360, ASC, MC) is False + fd = firdaria(BIRTH, (SUN + 180) % 360, ASC, MC) + assert fd["sect"] == "night" and fd["order"] == NIGHT_ORDER + + +def test_diurnal_sequence_and_totals(): + fd = firdaria(BIRTH, SUN, ASC, MC) + assert fd["sect"] == "day" and fd["order"] == DAY_ORDER + lords = [p["lord"] for p in fd["periods"]] + assert lords == DAY_ORDER + ["North Node", "South Node"] + assert sum(p["years"] for p in fd["periods"]) == 75 # 70 + 3 + 2 + + +def test_planet_majors_have_seven_subperiods_summing_to_period(): + fd = firdaria(BIRTH, SUN, ASC, MC) + for p in fd["periods"]: + if p["lord"] in YEARS: + assert len(p["sub"]) == 7 + assert p["sub"][0]["lord"] == p["lord"] # sub zaczyna się od władcy okresu + assert p["sub"][0]["start"] == p["start"] + assert p["sub"][-1]["end"] == p["end"] + else: + assert "sub" not in p # węzły bez podokresów + + +def test_periods_are_contiguous(): + fd = firdaria(BIRTH, SUN, ASC, MC) + for a, b in zip(fd["periods"], fd["periods"][1:]): + assert a["end"] == b["start"] + + +def test_age_42_is_saturn_major(): + # kumulatywnie: Su10 Ve8 Me13 Mo9 -> 40; Saturn 40-51 -> wiek 42 w Saturnie + fd = firdaria(BIRTH, SUN, ASC, MC) + saturn = next(p for p in fd["periods"] if p["lord"] == "Saturn") + assert saturn["start"].startswith("2024") and saturn["end"].startswith("2035") + + +def test_event_tokens_firdaria(): + ev = {"technique": "firdaria", "fd_major": "Saturn", "fd_sub": "Jupiter"} + assert _event_tokens(ev) == ["[Sa", "[Ju"]