feat: synastria — aspekty między dwoma horoskopami (PRE-04)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m32s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 11s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 9s
build / build (push) Successful in 25s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m18s
Testy / Kontrola składni wszystkich warstw (push) Successful in 31s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 13m6s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 13m25s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m32s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 11s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 9s
build / build (push) Successful in 25s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m18s
Testy / Kontrola składni wszystkich warstw (push) Successful in 31s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 13m6s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 13m25s
Pierwsza technika relacyjna z pełnym UI (Returns były już w kalendarzu od LOG-12). Dwie osoby → aspekty MIĘDZY ich horoskopami (planeta osoby A do planety osoby B). Silnik: `find_cross_aspects(a, b, orb, luminary_bonus, minor)` — każdy obiekt A × każdy obiekt B. `obj1` = osoba A, `obj2` = osoba B. Statyczne (dwa natale, brak wspólnego czasu) → bez applying/separating. Par sztywnych (NN/SN) NIE wycinamy — między dwiema osobami to realny aspekt, nie artefakt definicji. Wspólny matcher `_first_aspect` (z find_aspects), więc orb/bonus/aspekty poboczne działają tak samo. Endpoint `POST /chart/synastry` (dwie osoby + zodiak + ustawienia aspektów) → pozycje obu + siatka aspektów z glifami. Prezentacja: zakładka „Synastria", formularz dwóch osób (pętla po a_/b_), tabela aspektów A · aspekt · B · orb. Weryfikacja na żywym API: 13+13 obiektów, 63 aspekty synastryczne z poprawnymi glifami i bonusem świateł (A.Sun ☌ B.Venus przy orbie 8.67 = 8+2). Testy: logika +4 (cross-aspekty, kolejność A/B, brak filtra par sztywnych, brak applying), prezentacja +7 (trasa, formularz dwóch osób, klient, tabela). Logika 277, prezentacja 228. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #51.
This commit is contained in:
@@ -71,6 +71,41 @@ def _is_applying(la: float, lb: float, sa: float, sb: float, angle: float, dt: f
|
||||
return dev_next < dev_now
|
||||
|
||||
|
||||
def _first_aspect(sep: float, allowed: float, checks: dict) -> tuple[str, float] | None:
|
||||
"""Pierwszy aspekt z `checks`, w którego orbie mieści się separacja `sep`."""
|
||||
for asp, angle in checks.items():
|
||||
dev = abs(sep - angle)
|
||||
if dev <= allowed:
|
||||
return asp, round(dev, 2)
|
||||
return None
|
||||
|
||||
|
||||
def find_cross_aspects(
|
||||
a_positions: list[dict], b_positions: list[dict],
|
||||
orb: float = DEFAULT_ORB, luminary_bonus: float = LUMINARY_BONUS, minor: bool = False,
|
||||
) -> list[dict]:
|
||||
"""Aspekty MIĘDZY dwoma horoskopami (synastria, PRE-04): każdy obiekt z A vs
|
||||
każdy obiekt z B (`obj1` = osoba A, `obj2` = osoba B). Statyczne — dwa natale,
|
||||
brak wspólnego czasu, więc bez applying/separating. RIGID_PAIRS nie dotyczy
|
||||
(NN osoby A vs SN osoby B to realny aspekt, nie artefakt definicji)."""
|
||||
checks = {**MAJOR, **MINOR} if minor else MAJOR
|
||||
out: list[dict] = []
|
||||
for a in a_positions:
|
||||
la = a.get("decimal")
|
||||
if la is None:
|
||||
continue
|
||||
for b in b_positions:
|
||||
lb = b.get("decimal")
|
||||
if lb is None:
|
||||
continue
|
||||
allowed = orb + (luminary_bonus if (a["name"] in LUMINARIES or b["name"] in LUMINARIES) else 0.0)
|
||||
m = _first_aspect(separation(float(la), float(lb)), allowed, checks)
|
||||
if m:
|
||||
out.append({"obj1": a["name"], "obj2": b["name"],
|
||||
"aspect": m[0], "orb": m[1], "allowed": round(allowed, 2)})
|
||||
return out
|
||||
|
||||
|
||||
def find_aspects(
|
||||
positions: list[dict], orb: float = DEFAULT_ORB, luminary_bonus: float = LUMINARY_BONUS,
|
||||
minor: bool = False,
|
||||
|
||||
@@ -92,6 +92,47 @@ def chart_positions(req: PositionsRequest) -> dict:
|
||||
return chart
|
||||
|
||||
|
||||
class PersonMoment(BaseModel):
|
||||
when_utc: datetime
|
||||
lat: float = 0.0
|
||||
lon: float = 0.0
|
||||
|
||||
|
||||
class SynastryRequest(BaseModel):
|
||||
"""Dwie osoby (PRE-04). Aspekty liczone MIĘDZY horoskopami, nie w środku."""
|
||||
person_a: PersonMoment
|
||||
person_b: PersonMoment
|
||||
zodiac: str = "tropical"
|
||||
aspect_orb: float = 8.0
|
||||
aspect_luminary_bonus: float = 2.0
|
||||
aspect_minor: bool = False
|
||||
|
||||
|
||||
@app.post("/chart/synastry")
|
||||
def chart_synastry(req: SynastryRequest) -> dict:
|
||||
"""Synastria (PRE-04): dwa horoskopy natalne + aspekty MIĘDZY nimi (planeta
|
||||
osoby A vs planeta osoby B). Bez interpretacji z bazy — sama siatka aspektów."""
|
||||
from app.engine import glyphs as GL
|
||||
from app.engine.aspects import find_cross_aspects
|
||||
from app.engine.chart import build_chart
|
||||
from app.engine.models import ChartMoment
|
||||
|
||||
engine = get_engine()
|
||||
a = build_chart(engine, ChartMoment(when_utc=req.person_a.when_utc,
|
||||
lat=req.person_a.lat, lon=req.person_a.lon), zodiac=req.zodiac)
|
||||
b = build_chart(engine, ChartMoment(when_utc=req.person_b.when_utc,
|
||||
lat=req.person_b.lat, lon=req.person_b.lon), zodiac=req.zodiac)
|
||||
cross = find_cross_aspects(
|
||||
a["positions"], b["positions"], orb=req.aspect_orb,
|
||||
luminary_bonus=req.aspect_luminary_bonus, minor=req.aspect_minor)
|
||||
for c in cross:
|
||||
c["glyph"] = GL.aspect_glyph(c["aspect"]) # symbol aspektu (LOG-22)
|
||||
return {"engine": engine.name,
|
||||
"person_a": {"positions": a["positions"]},
|
||||
"person_b": {"positions": b["positions"]},
|
||||
"aspects": cross}
|
||||
|
||||
|
||||
@app.post("/chart/compare")
|
||||
def chart_compare(req: PositionsRequest) -> dict:
|
||||
"""Tryb dwu-silnikowy (LOG-26): policz oboma silnikami i zwróć raport różnic.
|
||||
|
||||
Reference in New Issue
Block a user