"""Aspekty — kąty między obiektami (LOG-06, wersja: aspekty główne). Czysta matematyka na policzonych długościach ekliptycznych. Dla każdej pary obiektów sprawdzamy, czy ich separacja kątowa mieści się w orbie któregoś z aspektów głównych. Applying/separating (aplikacja/separacja) — na później. Tokeny bazy (z SIGNIFICATORS KEY): [conj, [sex, [sq, [tri, [opp. """ from __future__ import annotations MAJOR = { "conjunction": 0.0, "sextile": 60.0, "square": 90.0, "trine": 120.0, "opposition": 180.0, } DB_TOKEN = { "conjunction": "[conj", "sextile": "[sex", "square": "[sq", "trine": "[tri", "opposition": "[opp", } PL_NAME = { "conjunction": "koniunkcja", "sextile": "sekstyl", "square": "kwadratura", "trine": "trygon", "opposition": "opozycja", } LUMINARIES = {"Sun", "Moon"} DEFAULT_ORB = 8.0 LUMINARY_BONUS = 2.0 def separation(a: float, b: float) -> float: """Najmniejsza separacja kątowa [0,180].""" d = abs(a - b) % 360.0 return min(d, 360.0 - d) def find_aspects( positions: list[dict], orb: float = DEFAULT_ORB, luminary_bonus: float = LUMINARY_BONUS ) -> list[dict]: """positions: dicty z 'name' i 'decimal' (długość). Zwraca listę aspektów.""" out: list[dict] = [] n = len(positions) for i in range(n): for j in range(i + 1, n): a, b = positions[i], positions[j] la, lb = a.get("decimal"), b.get("decimal") if la is None or lb is None: continue sep = separation(float(la), float(lb)) allowed = orb + (luminary_bonus if (a["name"] in LUMINARIES or b["name"] in LUMINARIES) else 0.0) for asp, angle in MAJOR.items(): dev = abs(sep - angle) if dev <= allowed: out.append({ "obj1": a["name"], "obj2": b["name"], "aspect": asp, "orb": round(dev, 2), "allowed": round(allowed, 2), }) break # jedna para = jeden aspekt return out