Ranking siły (LOG-21) + grupowanie identycznych opisów + geolokalizacja
Punktacja siły (LOG-21): - scoring.py: siła fasety z sygnałów obliczalnych (typ fasety, rodzaj aspektu, ciasnota orbu). Konfigurowalne wagi. Hook na przyszłość: kolumny countas*/level* z SIGNIFICATORS KEY (obecnie puste). - aspekty niosą orb+allowed; fasety dostają "score"; ranking faset malejąco. Grupowanie: - opcja group: zwija próbki po opisie (ten sam efekt = jedna grupa z listą sygnifikatorów i licznikiem). Checkbox "grupuj identyczne opisy" w /interpret. Geolokalizacja (bajer): - "Tu i teraz" (widok Horoskop i Interpretacje) uzupełnia lat/lon z przeglądarki (navigator.geolocation; wymaga zgody, https/localhost). Zweryfikowano na realnym main_base.xlsx: ranking sensowny (ciasna opozycja z Saturn 9.59 > szeroka koniunkcja z Moon 6.25 > znak/dom 5.0); grupowanie zwija powtórzone opisy. 41 testów przechodzi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,7 @@ def find_aspects(
|
||||
if dev <= allowed:
|
||||
out.append({
|
||||
"obj1": a["name"], "obj2": b["name"],
|
||||
"aspect": asp, "orb": round(dev, 2),
|
||||
"aspect": asp, "orb": round(dev, 2), "allowed": round(allowed, 2),
|
||||
})
|
||||
break # jedna para = jeden aspekt
|
||||
return out
|
||||
|
||||
@@ -83,6 +83,7 @@ class ReportRequest(BaseModel):
|
||||
lat: float = 0.0
|
||||
lon: float = 0.0
|
||||
limit: int = 5000
|
||||
group: bool = False # grupowanie identycznych opisów
|
||||
|
||||
|
||||
@app.post("/chart/report")
|
||||
@@ -99,7 +100,7 @@ def chart_report(req: ReportRequest) -> dict:
|
||||
try:
|
||||
report = build_report(
|
||||
chart["positions"], DataClient(),
|
||||
aspects=chart.get("aspects"), per_object_limit=req.limit,
|
||||
aspects=chart.get("aspects"), per_object_limit=req.limit, group=req.group,
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Punktacja siły trafień — do rankingowania faset (zalążek LOG-21).
|
||||
|
||||
v1 liczy siłę z sygnałów OBLICZALNYCH:
|
||||
- typ fasety (aspekt zwykle mocniejszy od znaku/domu),
|
||||
- rodzaj aspektu (koniunkcja/opozycja mocniejsze od sekstyla),
|
||||
- ciasnota orbu (im bliżej dokładności, tym mocniej).
|
||||
|
||||
Wszystko konfigurowalne. HOOK NA PRZYSZŁOŚĆ: gdy w SIGNIFICATORS KEY zostaną
|
||||
wypełnione kolumny `countas*`/`level*`, można je tu domieszać per sygnifikator.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# bazowe wagi faset (łatwe do strojenia)
|
||||
FACET_BASE = {"sign": 5.0, "house": 5.0, "aspect": 6.0}
|
||||
|
||||
# względna siła aspektów głównych
|
||||
ASPECT_WEIGHT = {
|
||||
"conjunction": 1.0, "opposition": 0.95, "square": 0.85,
|
||||
"trine": 0.85, "sextile": 0.65,
|
||||
}
|
||||
|
||||
|
||||
def score_facet(facet: dict) -> float:
|
||||
"""Siła fasety w skali ~0–12. Deterministyczna, konfigurowalna."""
|
||||
base = FACET_BASE.get(facet.get("type"), 1.0)
|
||||
if facet.get("type") == "aspect":
|
||||
weight = ASPECT_WEIGHT.get(facet.get("aspect"), 0.7)
|
||||
orb = facet.get("orb")
|
||||
allowed = facet.get("allowed") or 10.0
|
||||
tight = max(0.0, 1.0 - orb / allowed) if orb is not None and allowed else 0.0
|
||||
return round(base * weight * (1.0 + tight), 2)
|
||||
return round(base, 2)
|
||||
@@ -15,6 +15,7 @@ from typing import Any, Protocol
|
||||
|
||||
from app.abbreviations import expand
|
||||
from app.engine.formats import SIGN_ABBR, SIGNS
|
||||
from app.scoring import score_facet
|
||||
|
||||
PLANET_ABBR = {
|
||||
"Sun": "Su", "Moon": "Mo", "Mercury": "Me", "Venus": "Ve", "Mars": "Ma",
|
||||
@@ -87,11 +88,30 @@ def _facet_samples(rows: list[dict], tokens: list[str]) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def _group_by_effect(samples: list[dict]) -> list[dict]:
|
||||
"""Grupuje próbki po opisie: ten sam efekt = jedna grupa z listą sygnifikatorów."""
|
||||
groups: dict[str, dict] = {}
|
||||
order: list[str] = []
|
||||
for s in samples:
|
||||
key = _norm(s["effect"])
|
||||
g = groups.get(key)
|
||||
if g is None:
|
||||
g = {"effect": s["effect"], "count": 0, "significators": []}
|
||||
groups[key] = g
|
||||
order.append(key)
|
||||
g["count"] += 1
|
||||
g["significators"].append(s["expanded"])
|
||||
result = [groups[k] for k in order]
|
||||
result.sort(key=lambda g: g["count"], reverse=True)
|
||||
return result
|
||||
|
||||
|
||||
def build_report(
|
||||
positions: list[dict],
|
||||
data: DataSource,
|
||||
aspects: list[dict] | None = None,
|
||||
per_object_limit: int = 5000,
|
||||
group: bool = False,
|
||||
) -> dict:
|
||||
"""positions: pozycje z build_chart (name, sign, direction, house).
|
||||
|
||||
@@ -150,9 +170,17 @@ def build_report(
|
||||
facets.append({
|
||||
"type": "aspect", "label": f"{ASP_NAME[asp['aspect']]} z {other}",
|
||||
"token": f"{asp_tok} + {other_tok}",
|
||||
"aspect": asp["aspect"], "orb": asp.get("orb"), "allowed": asp.get("allowed"),
|
||||
"count": len(asp_samples), "samples": asp_samples,
|
||||
})
|
||||
|
||||
# punktacja siły (LOG-21), opcjonalne grupowanie po opisie, ranking faset
|
||||
for f in facets:
|
||||
f["score"] = score_facet(f)
|
||||
if group:
|
||||
f["groups"] = _group_by_effect(f["samples"])
|
||||
facets.sort(key=lambda f: f["score"], reverse=True)
|
||||
|
||||
items.append({
|
||||
"object": name,
|
||||
"sign": sign,
|
||||
|
||||
Reference in New Issue
Block a user