Files
Jyotisha/scripts/rectification/dasha_transition_proximity.py
T
Jesse_Chen 8743dcb105
Independent Staging Quality Gate / validate (push) Successful in 11m56s
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(rectification): separate adjacent minutes with transition proximity
Day-level events now score Vimshottari/Narayana transition closeness so
nearby candidate minutes can diverge, with gated quality probes and
answer-prior ranking so high-base-rate existence questions stay out.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 17:41:06 +08:00

199 lines
7.2 KiB
Python

"""Deterministic dasha-transition proximity scoring for day/month events.
Birth-time drift of about 1 minute moves Vimshottari/Narayana transition
dates by a few days. A dated event near a candidate's AD/PD change is a
bounded auxiliary signal, never larger than one day-level event body.
"""
from __future__ import annotations
from collections.abc import Callable, Sequence
from datetime import date
from typing import Any
from scripts.rectification.event_probes import _narayana_start_dates, _vim_start_dates
PROXIMITY_WINDOW_DAYS = 45
DAY_KERNEL_DAYS = 15
MONTH_KERNEL_DAYS = 45
DAY_MAX_POINTS = 1.0
MONTH_MAX_POINTS = 0.35
VIM_SHARE = 0.6
NARAYANA_SHARE = 0.4
def representative_event_date(event: dict[str, Any]) -> date | None:
precision = str(event.get("precision") or "")
if precision not in {"day", "month"}:
return None
raw_start = event.get("date_start") or event.get("date")
raw_end = event.get("date_end") or raw_start
try:
start = date.fromisoformat(str(raw_start)[:10])
end = date.fromisoformat(str(raw_end)[:10])
except ValueError:
return None
if precision == "day" or start == end:
return start
mid_day = min(15, end.day)
try:
return start.replace(day=mid_day)
except ValueError:
return start
def _nearest_delta(starts: Sequence[date], event_date: date) -> tuple[date | None, int | None]:
eligible = [
item for item in starts
if abs((item - event_date).days) <= PROXIMITY_WINDOW_DAYS
]
if not eligible:
return None, None
nearest = min(eligible, key=lambda item: (abs((item - event_date).days), item.toordinal()))
return nearest, abs((nearest - event_date).days)
def _kernel(delta_days: int | None, width: float) -> float:
if delta_days is None or width <= 0:
return 0.0
return max(0.0, 1.0 - (delta_days / width))
def score_transition_proximity(
*,
event_date: date,
precision: str,
vim_starts: Sequence[date],
narayana_starts: Sequence[date] | None = None,
vim_pd_starts: Sequence[date] | None = None,
) -> dict[str, Any]:
if precision not in {"day", "month"}:
return {
"points": 0.0,
"rule_ids": [],
"nearest_vim_delta_days": None,
"nearest_narayana_delta_days": None,
}
kernel_width = float(DAY_KERNEL_DAYS if precision == "day" else MONTH_KERNEL_DAYS)
cap = DAY_MAX_POINTS if precision == "day" else MONTH_MAX_POINTS
ad_starts = list(vim_starts)
pd_starts = list(vim_pd_starts or ())
ad_date, ad_delta = _nearest_delta(ad_starts, event_date)
pd_date, pd_delta = _nearest_delta(pd_starts, event_date)
if pd_delta is not None and (ad_delta is None or pd_delta < ad_delta):
vim_delta = pd_delta
vim_kind = "pd"
vim_date = pd_date
else:
vim_delta = ad_delta
vim_kind = "ad"
vim_date = ad_date
_, narayana_delta = _nearest_delta(list(narayana_starts or ()), event_date)
vim_kernel = _kernel(vim_delta, kernel_width)
narayana_kernel = _kernel(narayana_delta, kernel_width)
points = round(cap * (VIM_SHARE * vim_kernel + NARAYANA_SHARE * narayana_kernel), 4)
rules: list[str] = []
if vim_kernel > 0:
rules.append(f"vim_transition_proximity_{vim_kind}")
if narayana_kernel > 0:
rules.append("narayana_transition_proximity_ad")
return {
"points": points,
"rule_ids": rules,
"nearest_vim_delta_days": vim_delta,
"nearest_narayana_delta_days": narayana_delta,
"nearest_vim_date": vim_date,
}
def _context_time(context: dict[str, Any]) -> str | None:
feature = context.get("feature") if isinstance(context.get("feature"), dict) else {}
raw = feature.get("time")
if isinstance(raw, str) and len(raw) >= 5:
return raw[:5]
at = context.get("candidate_at")
if hasattr(at, "strftime"):
return at.strftime("%H:%M")
return None
def merge_transition_proximity(
matrix: dict[str, dict[str, dict[str, Any]]],
events: Sequence[dict[str, Any]],
static_contexts: Sequence[dict[str, Any]],
birth_date: str,
*,
public_technique_layers: Callable[[str, Sequence[str]], list[str]],
) -> None:
by_time = {
time: context
for context in static_contexts
if isinstance(context, dict) and (time := _context_time(context))
}
vim_cache: dict[tuple[Any, ...], list[date]] = {}
pd_cache: dict[tuple[Any, ...], list[date]] = {}
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
for event in events:
if not isinstance(event, dict):
continue
event_id = str(event.get("id") or "")
cells = matrix.get(event_id)
if not event_id or not isinstance(cells, dict):
continue
event_date = representative_event_date(event)
if event_date is None:
continue
precision = str(event.get("precision") or "")
lo, hi = event_date.year - 1, event_date.year + 1
for time, cell in cells.items():
context = by_time.get(str(time)[:5])
if not isinstance(cell, dict) or not isinstance(context, dict):
continue
moon = (context.get("planet_longitudes") or {}).get("Moon")
if not isinstance(moon, (int, float)):
continue
vim_key = (birth_date, round(float(moon), 6), lo, hi)
if vim_key not in vim_cache:
vim_cache[vim_key] = _vim_start_dates(birth_date, float(moon), lo, hi)
pd_cache[vim_key] = _vim_start_dates(
birth_date,
float(moon),
lo,
hi,
include_pratyantar=True,
)
planets = context.get("planet_longitudes") or {}
asc = context.get("ascendant_index")
narayana_key = (
birth_date,
int(asc) if isinstance(asc, int) else None,
lo,
hi,
round(float(moon), 6),
)
if narayana_key not in narayana_cache:
narayana_cache[narayana_key] = (
_narayana_start_dates(int(asc), planets, birth_date, lo, hi)
if isinstance(asc, int) and isinstance(planets, dict)
else None
)
ad_starts = vim_cache[vim_key]
ad_set = set(ad_starts)
pd_only = [item for item in pd_cache[vim_key] if item not in ad_set]
scored = score_transition_proximity(
event_date=event_date,
precision=precision,
vim_starts=ad_starts,
vim_pd_starts=pd_only,
narayana_starts=narayana_cache[narayana_key] or [],
)
if scored["points"] <= 0 and not scored["rule_ids"]:
continue
cell["points"] = round(float(cell.get("points") or 0) + float(scored["points"]), 4)
cell["rule_ids"] = sorted({
*list(cell.get("rule_ids") or []),
*scored["rule_ids"],
})
domain = str(event.get("domain") or cell.get("domain") or "")
cell["technique_layers"] = public_technique_layers(domain, cell["rule_ids"])