Aspekty (LOG-06) + faseta aspektu, dedup i dopieszczenie wyników

Aspekty:
- engine/aspects.py: aspekty główne (conj/sex/sq/tri/opp) z orbami
  (bonus dla luminarzy), separacja z obsługą zawinięcia. Applying/sep na później.
- build_chart zwraca listę aspektów; /chart/positions je udostępnia;
  widok Horoskop pokazuje tabelę aspektów.

Bogatsze sygnifikatory:
- trzecia faseta "w aspekcie": dla każdego aspektu głównego obiektu filtruje
  rekordy po tokenie aspektu + drugiej planety ([conj + [Mo). Cookbook
  komplet: znak + dom + aspekt.

Dopieszczenie wyników:
- ODSIEWANIE DUPLIKATÓW: duplikat = ten sam sygnifikator ORAZ ten sam opis
  (po normalizacji). Dedup wewnątrz fasety, działa też na wynikach z wielu baz.
- _facet_samples przyjmuje wiele tokenów (AND); dedup + istniejące odsiewanie szumu.

Zweryfikowano na realnym main_base.xlsx (30.04.1984): 16 aspektów zgodnych z
astro.com (Sun conj Moon 9.59°, Sun opp Saturn 3.17°); faseta aspektu daje
bogate trafienia (Sun koniunkcja z Moon 84, opozycja z Saturn 43); dedup obniżył
duplikaty (Sun w znaku 46->44). 36 testów przechodzi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 08:55:27 +02:00
parent 41d7ca491d
commit 166b438f83
8 changed files with 192 additions and 18 deletions
+59
View File
@@ -0,0 +1,59 @@
"""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),
})
break # jedna para = jeden aspekt
return out
+3
View File
@@ -22,8 +22,11 @@ def _fmt(name: str, lon: float) -> dict:
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN) -> dict:
from app.engine.aspects import find_aspects
positions = engine.positions(moment)
result: dict = {"engine": engine.name, "positions": [p.as_dict() for p in positions]}
result["aspects"] = find_aspects(result["positions"]) # aspekty (LOG-06)
if not hasattr(engine, "sidereal"):
return result