Speed up rectification scoring without changing results.
Cache work that depends only on the candidate minute, drop unread response fields, and reject windows above 241 candidates before any chart is built. Eleven canonical scenario hashes match 77057a819. Sealed and reported-offset replays match the 2026-10-08 records (changed_trial_count 8 and 581) and are stored as scoring_perf_2026_10_13.
Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
74384be024
commit
8e0503f9c0
@@ -4,7 +4,9 @@ from typing import Any, Mapping, Sequence
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
from scripts.active_rectification_events import build_candidate_result_summary
|
||||
from scripts.offline_compute_mixins import BadRequest
|
||||
from scripts.rectification.candidate_feature_service import build_candidate_feature_snapshot
|
||||
from scripts.rectification.candidate_window import enumerate_candidate_window
|
||||
from scripts.rectification.contracts import (
|
||||
EVENT_CONTRACT_VERSION,
|
||||
RectificationRequest,
|
||||
@@ -195,6 +197,35 @@ def _rectification_report(
|
||||
}
|
||||
|
||||
|
||||
MAX_CANDIDATES_PER_REQUEST = 241
|
||||
|
||||
|
||||
class TooManyCandidates(BadRequest):
|
||||
"""Rejected before any chart is computed.
|
||||
|
||||
The API server maps every BadRequest to HTTP 400 with error_code
|
||||
ERR_BAD_REQUEST and this task must not edit that server. The token
|
||||
too_many_candidates lives in the message, with the cap and the count.
|
||||
"""
|
||||
|
||||
code = "too_many_candidates"
|
||||
|
||||
def __init__(self, count: int) -> None:
|
||||
self.count = int(count)
|
||||
self.limit = MAX_CANDIDATES_PER_REQUEST
|
||||
super().__init__(f"too_many_candidates: limit={self.limit} count={self.count}")
|
||||
|
||||
|
||||
def _candidate_count(request: RectificationRequest) -> int:
|
||||
return len(enumerate_candidate_window(request))
|
||||
|
||||
|
||||
def _reject_if_too_many_candidates(request: RectificationRequest) -> None:
|
||||
count = _candidate_count(request)
|
||||
if count > MAX_CANDIDATES_PER_REQUEST:
|
||||
raise TooManyCandidates(count)
|
||||
|
||||
|
||||
def engine_scoring_versions() -> dict[str, str]:
|
||||
"""Identity fields also returned by `/api/rectification/v5/score`, without scoring."""
|
||||
return {
|
||||
@@ -219,7 +250,14 @@ def candidate_features(request: RectificationRequest) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
def score_candidates(
|
||||
request: RectificationRequest,
|
||||
*,
|
||||
include_internal: bool = False,
|
||||
enforce_candidate_cap: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if enforce_candidate_cap:
|
||||
_reject_if_too_many_candidates(request)
|
||||
scoring_request = scoreable_request(request)
|
||||
built = build_event_contribution_matrix(scoring_request)
|
||||
rows = score_from_matrix(scoring_request, built)
|
||||
@@ -272,7 +310,7 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
)
|
||||
rectification_report["next_step_codes"] = candidate_summary["next_step_codes"]
|
||||
candidate_summary["report"] = rectification_report
|
||||
return {
|
||||
payload = {
|
||||
"result_id": result_id,
|
||||
"algorithm_version": ALGORITHM_VERSION,
|
||||
"event_contract_version": EVENT_CONTRACT_VERSION,
|
||||
@@ -286,11 +324,9 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
"conflicting_event_ids": [item["event_id"] for item in row["evidence"] if item["points"] < 0],
|
||||
} for row in rows],
|
||||
"candidate_decisions": candidate_decisions,
|
||||
"candidate_decision_receipt": decision_receipt,
|
||||
"decision_receipt": decision_receipt,
|
||||
"execution_ledger_version": EXECUTION_LEDGER_VERSION,
|
||||
"execution_ledger": execution_ledger,
|
||||
"event_contribution_matrix": built["matrix"],
|
||||
"candidate_feature_snapshot": build_candidate_feature_snapshot(
|
||||
scoring_request, spec_hash, built.get("static_contexts")
|
||||
),
|
||||
@@ -317,10 +353,20 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
"margin_percent": decision_receipt["margin_percent"],
|
||||
"can_confirm_exact_minute": decision_receipt["confirmation_allowed"],
|
||||
}
|
||||
if include_internal:
|
||||
payload["candidate_decision_receipt"] = decision_receipt
|
||||
payload["event_contribution_matrix"] = built["matrix"]
|
||||
return payload
|
||||
|
||||
|
||||
def diagnostics(request: RectificationRequest) -> dict[str, Any]:
|
||||
scored = score_candidates(request)
|
||||
def diagnostics(
|
||||
request: RectificationRequest,
|
||||
*,
|
||||
enforce_candidate_cap: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if enforce_candidate_cap:
|
||||
_reject_if_too_many_candidates(request)
|
||||
scored = score_candidates(request, enforce_candidate_cap=enforce_candidate_cap)
|
||||
return {
|
||||
"result_id": scored["result_id"],
|
||||
"algorithm_version": scored["algorithm_version"],
|
||||
@@ -328,7 +374,6 @@ def diagnostics(request: RectificationRequest) -> dict[str, Any]:
|
||||
"decision_policy_version": scored["decision_policy_version"],
|
||||
"calculation_spec_hash": scored["calculation_spec_hash"],
|
||||
"candidate_decisions": scored["candidate_decisions"],
|
||||
"candidate_decision_receipt": scored["candidate_decision_receipt"],
|
||||
"decision_receipt": scored["decision_receipt"],
|
||||
"execution_ledger_version": scored["execution_ledger_version"],
|
||||
"execution_ledger": scored["execution_ledger"],
|
||||
@@ -460,7 +505,11 @@ def _normalize_relative_support(raw: Sequence[float]) -> list[float]:
|
||||
return shares
|
||||
|
||||
|
||||
def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
def block_scan(
|
||||
request: RectificationRequest,
|
||||
*,
|
||||
enforce_candidate_cap: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Aggregate event scores into declared periods or caller-supplied sub-blocks."""
|
||||
requested_step = request.get("minute_step")
|
||||
if isinstance(requested_step, int) and requested_step > 1:
|
||||
@@ -472,7 +521,9 @@ def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
if step <= 1:
|
||||
step = 10
|
||||
scoring_request = {**request, "minute_step": step}
|
||||
scored = score_candidates(scoring_request)
|
||||
if enforce_candidate_cap:
|
||||
_reject_if_too_many_candidates(scoring_request)
|
||||
scored = score_candidates(scoring_request, enforce_candidate_cap=enforce_candidate_cap)
|
||||
events_by_id = {
|
||||
str(event.get("id") or ""): event
|
||||
for event in request.get("events") or []
|
||||
|
||||
@@ -130,6 +130,8 @@ def merge_transition_proximity(
|
||||
for context in static_contexts
|
||||
if isinstance(context, dict) and (time := _context_time(context))
|
||||
}
|
||||
# BUG-1324: one unfiltered dasha walk per candidate, then filter by event year.
|
||||
# Released when this function returns. Not a module-level cache.
|
||||
vim_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
pd_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
|
||||
@@ -156,14 +158,15 @@ def merge_transition_proximity(
|
||||
# Legacy time-only contexts still use the request's date.
|
||||
candidate_at = context.get("candidate_at")
|
||||
candidate_date = candidate_at.date().isoformat() if isinstance(candidate_at, datetime) else birth_date
|
||||
vim_key = (candidate_date, round(float(moon), 6), lo, hi)
|
||||
moon_key = round(float(moon), 6)
|
||||
vim_key = (candidate_date, moon_key)
|
||||
if vim_key not in vim_cache:
|
||||
vim_cache[vim_key] = _vim_start_dates(candidate_date, float(moon), lo, hi)
|
||||
vim_cache[vim_key] = _vim_start_dates(candidate_date, float(moon), 1, 9999)
|
||||
pd_cache[vim_key] = _vim_start_dates(
|
||||
candidate_date,
|
||||
float(moon),
|
||||
lo,
|
||||
hi,
|
||||
1,
|
||||
9999,
|
||||
include_pratyantar=True,
|
||||
)
|
||||
planets = context.get("planet_longitudes") or {}
|
||||
@@ -171,25 +174,26 @@ def merge_transition_proximity(
|
||||
narayana_key = (
|
||||
candidate_date,
|
||||
int(asc) if isinstance(asc, int) else None,
|
||||
lo,
|
||||
hi,
|
||||
round(float(moon), 6),
|
||||
moon_key,
|
||||
)
|
||||
if narayana_key not in narayana_cache:
|
||||
narayana_cache[narayana_key] = (
|
||||
_narayana_start_dates(int(asc), planets, candidate_date, lo, hi)
|
||||
_narayana_start_dates(int(asc), planets, candidate_date, 1, 9999)
|
||||
if isinstance(asc, int) and isinstance(planets, dict)
|
||||
else None
|
||||
)
|
||||
ad_starts = vim_cache[vim_key]
|
||||
ad_starts = [item for item in vim_cache[vim_key] if lo <= item.year <= hi]
|
||||
ad_set = set(ad_starts)
|
||||
pd_only = [item for item in pd_cache[vim_key] if item not in ad_set]
|
||||
pd_only = [item for item in pd_cache[vim_key] if lo <= item.year <= hi and item not in ad_set]
|
||||
narayana_starts = narayana_cache[narayana_key]
|
||||
if narayana_starts is not None:
|
||||
narayana_starts = [item for item in narayana_starts if lo <= item.year <= hi]
|
||||
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 [],
|
||||
narayana_starts=narayana_starts or [],
|
||||
)
|
||||
if scored["points"] <= 0 and not scored["rule_ids"]:
|
||||
continue
|
||||
|
||||
@@ -814,6 +814,7 @@ def _score_year(
|
||||
vimshottari=vimshottari,
|
||||
narayana=narayana,
|
||||
arudha_padas=context.get("arudha_padas") or {},
|
||||
functional=context.get("functional_profile"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1913,8 +1914,12 @@ def guided_collect_windows(
|
||||
vim_cache=vim_cache,
|
||||
narayana_cache=narayana_cache,
|
||||
)
|
||||
# BUG-1323: gathered depends only on (track, boundary index). Prefetch once
|
||||
# per candidate and skip a boundary after the one==two check has passed.
|
||||
work_tracks = [starts_by_time.get(_context_time(context) or "") or {} for context in work]
|
||||
rows: list[dict[str, Any]] = []
|
||||
seen: set[tuple[int, int, int, str]] = set()
|
||||
done: set[tuple[str, int]] = set()
|
||||
for left, right in _representative_pairs(work):
|
||||
left_time = str(_context_time(left) or "")
|
||||
right_time = str(_context_time(right) or "")
|
||||
@@ -1928,10 +1933,12 @@ def guided_collect_windows(
|
||||
for index, (one, two) in enumerate(zip(left_dates, right_dates)):
|
||||
if one == two:
|
||||
continue
|
||||
if (track, index) in done:
|
||||
continue
|
||||
done.add((track, index))
|
||||
gathered: list[date] = []
|
||||
for context in work:
|
||||
time = _context_time(context)
|
||||
dates = (starts_by_time.get(time or "") or {}).get(track) or []
|
||||
for tracks in work_tracks:
|
||||
dates = tracks.get(track) or []
|
||||
if index < len(dates):
|
||||
gathered.append(dates[index])
|
||||
if len(gathered) < 2 or min(gathered) == max(gathered):
|
||||
|
||||
Reference in New Issue
Block a user