feat(rectification): 出卡加精度门槛,补经历改成系统点名
宽度超过 10 分钟或头名并列时不再出交付卡,改为按大运边界逐条问、 用类型芯片和年/月选择器录入。跳过的线换问法再问一次;答「这类事 都没有过」的不再问。用户说「没有了」仍立刻给目前范围。Skill 10.0.27。 BUG-740~743
This commit is contained in:
@@ -218,7 +218,7 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
fingerprint = sha256({
|
||||
key: value
|
||||
for key, value in request.items()
|
||||
if key not in {"asked_probe_keys", "dropped_asked_probe_keys", "column_times", "refresh_probes"}
|
||||
if key not in {"asked_probe_keys", "dropped_asked_probe_keys", "declined_domains", "column_times", "refresh_probes"}
|
||||
})
|
||||
result_id = str(uuid5(NAMESPACE_URL, f"{ALGORITHM_VERSION}:{fingerprint}"))
|
||||
candidate_decisions = build_candidate_decisions(
|
||||
@@ -517,6 +517,7 @@ def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
"precision_stage": {"current": "block_scan"},
|
||||
"blocks": blocks,
|
||||
"discriminating_event_probes": [],
|
||||
"guided_collect_windows": [],
|
||||
"acceptance_allowed": False,
|
||||
"selection_allowed": False,
|
||||
"display_allowed": False,
|
||||
@@ -524,6 +525,7 @@ def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
**receipt,
|
||||
"precision_stage": {"current": "block_scan"},
|
||||
"discriminating_event_probes": [],
|
||||
"guided_collect_windows": [],
|
||||
"acceptance_allowed": False,
|
||||
"selection_allowed": False,
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ _EVENT_PROVENANCE_FIELDS = frozenset({
|
||||
})
|
||||
_REQUEST_FIELDS = frozenset({
|
||||
"birth_date", "start_time", "end_time", "lat", "lon", "tz", "events",
|
||||
"ayanamsa", "node_mode", "asked_probe_keys", "column_times", "minute_step", "blocks",
|
||||
"ayanamsa", "node_mode", "asked_probe_keys", "declined_domains", "column_times", "minute_step", "blocks",
|
||||
"refresh_probes",
|
||||
}) | _REQUEST_PROVENANCE_FIELDS
|
||||
ASKED_PROBE_KEY_MAX_LENGTH = 200
|
||||
@@ -169,6 +169,7 @@ class RectificationRequest(TypedDict):
|
||||
timezone_source: NotRequired[str | None]
|
||||
local_time_status: NotRequired[str | None]
|
||||
asked_probe_keys: NotRequired[list[str]]
|
||||
declined_domains: NotRequired[list[str]]
|
||||
dropped_asked_probe_keys: NotRequired[int]
|
||||
column_times: NotRequired[list[str]]
|
||||
refresh_probes: NotRequired[bool]
|
||||
@@ -357,6 +358,27 @@ def normalize_rectification_request(body: Any, *, today: date | None = None) ->
|
||||
cleaned_request["asked_probe_keys"] = cleaned_keys
|
||||
if dropped:
|
||||
cleaned_request["dropped_asked_probe_keys"] = dropped
|
||||
if "declined_domains" in body:
|
||||
raw_domains = body.get("declined_domains")
|
||||
if not isinstance(raw_domains, list) or len(raw_domains) > 20:
|
||||
raise ValueError("declined_domains must contain between 0 and 20 strings")
|
||||
cleaned_domains: list[str] = []
|
||||
seen_domains: set[str] = set()
|
||||
allowed = {
|
||||
"education", "career", "relocation", "relationship",
|
||||
"family", "finance", "health_pressure",
|
||||
}
|
||||
for index, item in enumerate(raw_domains):
|
||||
if not isinstance(item, str) or not item.strip():
|
||||
raise ValueError(f"declined_domains[{index}] must be a non-empty domain name")
|
||||
domain = item.strip()
|
||||
if domain not in allowed:
|
||||
raise ValueError(f"declined_domains[{index}] is not a collect domain")
|
||||
if domain in seen_domains:
|
||||
continue
|
||||
seen_domains.add(domain)
|
||||
cleaned_domains.append(domain)
|
||||
cleaned_request["declined_domains"] = cleaned_domains
|
||||
if "column_times" in body:
|
||||
raw_times = body.get("column_times")
|
||||
if not isinstance(raw_times, list) or not 1 <= len(raw_times) <= 64:
|
||||
|
||||
@@ -755,6 +755,7 @@ def build_decision_receipt(
|
||||
"precision_stage": packet["precision_stage"],
|
||||
"oos_blind_prompts": packet["oos_blind_prompts"],
|
||||
"discriminating_event_probes": packet.get("discriminating_event_probes") or [],
|
||||
"guided_collect_windows": packet.get("guided_collect_windows") or [],
|
||||
"event_clarification_probes": packet.get("event_clarification_probes") or [],
|
||||
"evidence_collection_probes": packet.get("evidence_collection_probes") or [],
|
||||
"candidate_contrast_opportunities": packet.get("candidate_contrast_opportunities") or [],
|
||||
|
||||
@@ -1779,6 +1779,203 @@ def discriminating_event_probes(
|
||||
return probes
|
||||
|
||||
|
||||
GUIDED_COLLECT_LIMIT = 6
|
||||
GUIDED_DOMAIN_ORDER = (
|
||||
"education",
|
||||
"career",
|
||||
"relocation",
|
||||
"relationship",
|
||||
"family",
|
||||
"finance",
|
||||
"health_pressure",
|
||||
)
|
||||
_GUIDED_TRACK_DOMAIN = {
|
||||
"nara:d9": "relationship",
|
||||
"nara:d10": "career",
|
||||
}
|
||||
|
||||
|
||||
def _guided_track_starts(
|
||||
context: dict[str, Any],
|
||||
*,
|
||||
birth_date: str,
|
||||
lo: int,
|
||||
hi: int,
|
||||
include_pratyantar: bool,
|
||||
varga_narayana: bool,
|
||||
vim_cache: dict[tuple[Any, ...], list[date]],
|
||||
narayana_cache: dict[tuple[Any, ...], list[date] | None],
|
||||
) -> dict[str, list[date]]:
|
||||
tracks: dict[str, list[date]] = {}
|
||||
moon = float(context["planet_longitudes"]["Moon"])
|
||||
vim_key = (*_vim_cache_key(birth_date, moon, lo, hi), include_pratyantar)
|
||||
if vim_key not in vim_cache:
|
||||
vim_cache[vim_key] = _vim_start_dates(
|
||||
birth_date, moon, lo, hi, include_pratyantar=include_pratyantar,
|
||||
)
|
||||
tracks["vim"] = vim_cache[vim_key]
|
||||
natal_key = (*_narayana_cache_key(
|
||||
int(context["ascendant_index"]), context["planet_longitudes"], birth_date, lo, hi,
|
||||
), None)
|
||||
if natal_key not in narayana_cache:
|
||||
narayana_cache[natal_key] = _narayana_start_dates(
|
||||
int(context["ascendant_index"]), context["planet_longitudes"], birth_date, lo, hi,
|
||||
)
|
||||
natal = narayana_cache[natal_key]
|
||||
if natal is not None:
|
||||
tracks["nara"] = natal
|
||||
if not varga_narayana:
|
||||
return tracks
|
||||
for layer in ("d9", "d10"):
|
||||
raw = _layer_value(context, layer)
|
||||
if not isinstance(raw, int):
|
||||
continue
|
||||
layer_key = (*_narayana_cache_key(
|
||||
raw, context["planet_longitudes"], birth_date, lo, hi,
|
||||
), layer)
|
||||
if layer_key not in narayana_cache:
|
||||
narayana_cache[layer_key] = _narayana_start_dates(
|
||||
raw, context["planet_longitudes"], birth_date, lo, hi,
|
||||
)
|
||||
dates = narayana_cache[layer_key]
|
||||
if dates is not None:
|
||||
tracks[f"nara:{layer}"] = dates
|
||||
return tracks
|
||||
|
||||
|
||||
def _guided_year_windows(dates: Sequence[date]) -> list[tuple[int, int, int]]:
|
||||
by_year: dict[int, list[date]] = {}
|
||||
for item in dates:
|
||||
by_year.setdefault(item.year, []).append(item)
|
||||
windows: list[tuple[int, int, int]] = []
|
||||
for year in sorted(by_year):
|
||||
months = [item.month for item in by_year[year]]
|
||||
windows.append((year, min(months), max(months)))
|
||||
return windows
|
||||
|
||||
|
||||
def guided_collect_windows(
|
||||
request: dict[str, Any],
|
||||
built: dict[str, Any],
|
||||
*,
|
||||
candidate_times: Sequence[str],
|
||||
today: date | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Boundary windows for guided collect. Does not change auto probe generation."""
|
||||
birth_date = str(request.get("birth_date") or "").strip()
|
||||
birth_year = _birth_year(birth_date)
|
||||
if birth_year is None:
|
||||
return []
|
||||
try:
|
||||
datetime.strptime(birth_date, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return []
|
||||
remaining = _remaining_contexts(built, candidate_times)
|
||||
if len(remaining) < 2:
|
||||
remaining = _static_contexts(built)
|
||||
work = [item for item in remaining if _scoreable(item)]
|
||||
if len(work) < 2:
|
||||
return []
|
||||
declined = {
|
||||
str(item).strip()
|
||||
for item in (request.get("declined_domains") or [])
|
||||
if str(item).strip() in GUIDED_DOMAIN_ORDER
|
||||
}
|
||||
now = today or date.today()
|
||||
lo, hi = birth_year + 5, min(now.year, birth_year + 80)
|
||||
remaining_layers = _differing_layers(work)
|
||||
fallback = [
|
||||
domain
|
||||
for domain in _probe_domains(
|
||||
remaining_layers,
|
||||
[item for item in (request.get("events") or []) if isinstance(item, dict)],
|
||||
d1_differs="d1" in remaining_layers,
|
||||
)
|
||||
if domain not in declined
|
||||
]
|
||||
if not fallback:
|
||||
fallback = [domain for domain in GUIDED_DOMAIN_ORDER if domain not in declined]
|
||||
if not fallback:
|
||||
return []
|
||||
vim_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
|
||||
starts_by_time: dict[str, dict[str, list[date]]] = {}
|
||||
for context in work:
|
||||
time = _context_time(context)
|
||||
if not time:
|
||||
continue
|
||||
starts_by_time[time] = _guided_track_starts(
|
||||
context,
|
||||
birth_date=birth_date,
|
||||
lo=lo,
|
||||
hi=hi,
|
||||
include_pratyantar=True,
|
||||
varga_narayana=True,
|
||||
vim_cache=vim_cache,
|
||||
narayana_cache=narayana_cache,
|
||||
)
|
||||
rows: list[dict[str, Any]] = []
|
||||
seen: set[tuple[int, int, int, str]] = set()
|
||||
unlayered_index = 0
|
||||
for left, right in _representative_pairs(work):
|
||||
left_time = str(_context_time(left) or "")
|
||||
right_time = str(_context_time(right) or "")
|
||||
left_tracks = starts_by_time.get(left_time) or {}
|
||||
right_tracks = starts_by_time.get(right_time) or {}
|
||||
for track in ("vim", "nara", "nara:d9", "nara:d10"):
|
||||
left_dates = left_tracks.get(track) or []
|
||||
right_dates = right_tracks.get(track) or []
|
||||
if not left_dates or not right_dates:
|
||||
continue
|
||||
for index, (one, two) in enumerate(zip(left_dates, right_dates)):
|
||||
if one == two:
|
||||
continue
|
||||
gathered: list[date] = []
|
||||
for context in work:
|
||||
time = _context_time(context)
|
||||
dates = (starts_by_time.get(time or "") or {}).get(track) or []
|
||||
if index < len(dates):
|
||||
gathered.append(dates[index])
|
||||
if len(gathered) < 2 or min(gathered) == max(gathered):
|
||||
continue
|
||||
ordered = sorted(gathered)
|
||||
cut = ordered[0] + (ordered[-1] - ordered[0]) / 2
|
||||
left_n = sum(1 for item in gathered if item <= cut)
|
||||
right_n = len(gathered) - left_n
|
||||
if left_n < 1 or right_n < 1:
|
||||
continue
|
||||
track_domain = _GUIDED_TRACK_DOMAIN.get(track)
|
||||
if track_domain and track_domain not in declined:
|
||||
domain = track_domain
|
||||
else:
|
||||
domain = fallback[unlayered_index % len(fallback)]
|
||||
unlayered_index += 1
|
||||
if domain in declined:
|
||||
continue
|
||||
for year, month_lo, month_hi in _guided_year_windows(gathered):
|
||||
if year < lo or year > hi:
|
||||
continue
|
||||
key = (year, month_lo, month_hi, domain)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
rows.append({
|
||||
"year": year,
|
||||
"month_lo": month_lo,
|
||||
"month_hi": month_hi,
|
||||
"domain": domain,
|
||||
"split": {"left": left_n, "right": right_n},
|
||||
})
|
||||
rows.sort(key=lambda row: (
|
||||
abs(row["split"]["left"] - row["split"]["right"]),
|
||||
abs(now.year - int(row["year"])),
|
||||
-int(row["year"]),
|
||||
int(row["month_lo"]),
|
||||
str(row["domain"]),
|
||||
))
|
||||
return rows[:GUIDED_COLLECT_LIMIT]
|
||||
|
||||
|
||||
def prospective_windows_for_time(
|
||||
request: dict[str, Any],
|
||||
built: dict[str, Any],
|
||||
|
||||
@@ -688,6 +688,7 @@ def build_refinement_packet(
|
||||
"precision_stage": {"current": "block_scan"},
|
||||
"oos_blind_prompts": [],
|
||||
"discriminating_event_probes": [],
|
||||
"guided_collect_windows": [],
|
||||
"event_clarification_probes": [],
|
||||
"evidence_collection_probes": [],
|
||||
"candidate_contrast_opportunities": [],
|
||||
@@ -708,6 +709,7 @@ def build_refinement_packet(
|
||||
discriminating_event_probe_set,
|
||||
event_clarification_probes,
|
||||
evidence_collection_probes,
|
||||
guided_collect_windows,
|
||||
prospective_event_windows,
|
||||
)
|
||||
grid_times = list(built.get("candidate_times") or candidate_times)
|
||||
@@ -731,6 +733,11 @@ def build_refinement_packet(
|
||||
)
|
||||
probes = bundle["probes"]
|
||||
dropped = list(bundle["dropped"])
|
||||
windows = guided_collect_windows(
|
||||
request,
|
||||
built,
|
||||
candidate_times=probe_times,
|
||||
)
|
||||
clarification = event_clarification_probes(request)
|
||||
collection = evidence_collection_probes(request)
|
||||
if probe_times == grid_times:
|
||||
@@ -783,6 +790,7 @@ def build_refinement_packet(
|
||||
"precision_stage": stage,
|
||||
"oos_blind_prompts": oos_blind_prompts(request),
|
||||
"discriminating_event_probes": probes,
|
||||
"guided_collect_windows": windows,
|
||||
"event_clarification_probes": clarification,
|
||||
"evidence_collection_probes": collection,
|
||||
"candidate_contrast_opportunities": opportunities,
|
||||
|
||||
@@ -16,3 +16,4 @@ MAX_EXTERNAL_VALIDATION_WIDTH_MINUTES: Final = int(POLICY["maxExternalValidation
|
||||
MAX_CONFIRMATION_WIDTH_MINUTES: Final = int(POLICY["maxConfirmationWidthMinutes"])
|
||||
MIN_CONFIRMATION_MARGIN_PERCENT: Final = int(POLICY["minConfirmationMarginPercent"])
|
||||
MAX_PLATEAU_ROUNDS: Final = int(POLICY["maxPlateauRounds"])
|
||||
DELIVERY_MAX_WIDTH_MINUTES: Final = int(POLICY["deliveryMaxWidthMinutes"])
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline T2 replay: after six probes, inject guided_collect_windows events.
|
||||
|
||||
Does not change production defaults. Writes a compact JSON summary for the
|
||||
progress note. Not a merge gate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from calendar import monthrange
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any, Sequence
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.active_rectification_event_engine import ( # noqa: E402
|
||||
AYANAMSA,
|
||||
NODE_MODE,
|
||||
compute_candidate_static_contexts,
|
||||
)
|
||||
from scripts.rectification.event_probes import ( # noqa: E402
|
||||
discriminating_event_probes,
|
||||
guided_collect_windows,
|
||||
)
|
||||
from scripts.rectification.refinement_packet import window_scan # noqa: E402
|
||||
from scripts.rectification.scoring_service import ( # noqa: E402
|
||||
build_event_contribution_matrix,
|
||||
score_from_matrix,
|
||||
scoreable_request,
|
||||
)
|
||||
from scripts.rectification_policy import DELIVERY_MAX_WIDTH_MINUTES # noqa: E402
|
||||
from scripts.research.cluster_width_lib import ( # noqa: E402
|
||||
SEPARATION_LEAD,
|
||||
delivery_from_public,
|
||||
merge_adjacent_traced,
|
||||
public_from_clusters,
|
||||
raw_signature_clusters,
|
||||
still_valid_public,
|
||||
)
|
||||
from scripts.research.cluster_width_probe import replay_public # noqa: E402
|
||||
from scripts.research.minute_resolution_sweep import MINUTE_STEP, scoring_request_for # noqa: E402
|
||||
from scripts.research.probe_supply_after_six import ASK_COUNT, KIND_BY_DOMAIN # noqa: E402
|
||||
|
||||
HOLDOUT = ROOT / "references" / "real_case_calibration" / "minute_rectification_holdout_v4.json"
|
||||
REPORT_JSON = ROOT / "docs" / "research" / "guided_collect_holdout_2026_09_16.json"
|
||||
TODAY = date(2026, 9, 16)
|
||||
RADII = (10, 30, 60)
|
||||
RANGE_DELIVERY_TIE_PERCENT = 3
|
||||
|
||||
|
||||
def _hhmm(value: object) -> str | None:
|
||||
text = str(value or "")[:5]
|
||||
return text if len(text) == 5 and text[2] == ":" else None
|
||||
|
||||
|
||||
def load_cases() -> list[dict[str, Any]]:
|
||||
payload = json.loads(HOLDOUT.read_text(encoding="utf-8"))
|
||||
return list(payload.get("cases") or [])
|
||||
|
||||
|
||||
def synthetic_event(window: dict[str, Any], index: int) -> dict[str, Any]:
|
||||
year = int(window["year"])
|
||||
month = int(window["month_lo"])
|
||||
last = monthrange(year, month)[1]
|
||||
domain = str(window.get("domain") or "career")
|
||||
kind = KIND_BY_DOMAIN.get(domain, "career_change")
|
||||
stamp = f"{year:04d}-{month:02d}"
|
||||
return {
|
||||
"id": str(uuid5(NAMESPACE_URL, f"guided-collect:{index}:{domain}:{stamp}")),
|
||||
"domain": domain,
|
||||
"event_kind": kind,
|
||||
"date_start": f"{stamp}-01",
|
||||
"date_end": f"{stamp}-{last:02d}",
|
||||
"precision": "month",
|
||||
"summary": f"guided {domain} {stamp}",
|
||||
}
|
||||
|
||||
|
||||
def posterior_state(
|
||||
*,
|
||||
rows: Sequence[dict[str, Any]],
|
||||
contexts: Sequence[dict[str, Any]],
|
||||
probes: Sequence[dict[str, Any]],
|
||||
true_time: str,
|
||||
) -> dict[str, Any]:
|
||||
raw = raw_signature_clusters(contexts)
|
||||
by_time = {stamp: row for row in rows if (stamp := _hhmm(row.get("time")))}
|
||||
merged, _trace = merge_adjacent_traced(raw, by_time)
|
||||
public = public_from_clusters(merged, rows)
|
||||
prior = {stamp: float(row.get("score") or 0) for row in public if (stamp := _hhmm(row.get("time")))}
|
||||
replay = replay_public(probes=probes, public=public, prior=prior, true_time=true_time)
|
||||
posterior = []
|
||||
eliminated = set(replay["eliminated"])
|
||||
scores = dict(replay["scores"])
|
||||
for row in public:
|
||||
stamp = _hhmm(row.get("time"))
|
||||
if not stamp:
|
||||
continue
|
||||
posterior.append({**row, "score": scores.get(stamp, row.get("score") or 0)})
|
||||
valid = still_valid_public(posterior, scores, eliminated, lead=SEPARATION_LEAD)
|
||||
delivery = delivery_from_public(valid)
|
||||
return {
|
||||
"scores": scores,
|
||||
"eliminated": eliminated,
|
||||
"public": posterior,
|
||||
"valid": valid,
|
||||
"delivery": delivery,
|
||||
"merged": merged,
|
||||
}
|
||||
|
||||
|
||||
def precision_gate(valid: Sequence[dict[str, Any]], scores: dict[str, float]) -> dict[str, Any]:
|
||||
delivery = delivery_from_public(valid)
|
||||
width = delivery.get("width")
|
||||
ranked = sorted(
|
||||
valid,
|
||||
key=lambda row: (
|
||||
-float(scores.get(_hhmm(row.get("time")) or "", row.get("score") or 0)),
|
||||
str(row.get("time") or ""),
|
||||
),
|
||||
)
|
||||
if not ranked:
|
||||
return {
|
||||
"met": False,
|
||||
"width": width,
|
||||
"tied_for_first": False,
|
||||
"gap": None,
|
||||
"percents": [],
|
||||
}
|
||||
if len(ranked) == 1:
|
||||
return {
|
||||
"met": width is not None and width <= DELIVERY_MAX_WIDTH_MINUTES,
|
||||
"width": width,
|
||||
"tied_for_first": False,
|
||||
"gap": None,
|
||||
"percents": [100],
|
||||
}
|
||||
s0 = float(scores.get(_hhmm(ranked[0].get("time")) or "", ranked[0].get("score") or 0))
|
||||
s1 = float(scores.get(_hhmm(ranked[1].get("time")) or "", ranked[1].get("score") or 0))
|
||||
tied = s0 == s1
|
||||
total = sum(
|
||||
max(float(scores.get(_hhmm(row.get("time")) or "", row.get("score") or 0)), 0.0)
|
||||
for row in ranked
|
||||
)
|
||||
percents = []
|
||||
for row in ranked[:3]:
|
||||
score = max(float(scores.get(_hhmm(row.get("time")) or "", row.get("score") or 0)), 0.0)
|
||||
percents.append(round(score / total * 100) if total > 0 else 0)
|
||||
gap = percents[0] - percents[1]
|
||||
met = (
|
||||
width is not None
|
||||
and width <= DELIVERY_MAX_WIDTH_MINUTES
|
||||
and gap > RANGE_DELIVERY_TIE_PERCENT
|
||||
and not tied
|
||||
)
|
||||
return {
|
||||
"met": met,
|
||||
"width": width,
|
||||
"tied_for_first": tied,
|
||||
"gap": gap,
|
||||
"percents": percents,
|
||||
}
|
||||
|
||||
|
||||
def remaining_times(state: dict[str, Any]) -> list[str]:
|
||||
times: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for row in state["valid"]:
|
||||
stamp = _hhmm(row.get("time"))
|
||||
if stamp and stamp not in seen and stamp not in state["eliminated"]:
|
||||
seen.add(stamp)
|
||||
times.append(stamp)
|
||||
for member in row.get("cluster_times") or []:
|
||||
clock = _hhmm(member)
|
||||
if clock and clock not in seen and clock not in state["eliminated"]:
|
||||
seen.add(clock)
|
||||
times.append(clock)
|
||||
return times
|
||||
|
||||
|
||||
def evaluate_case(
|
||||
case: dict[str, Any],
|
||||
radius: int,
|
||||
) -> dict[str, Any]:
|
||||
true_time = str(case["birth"]["time"])[:5]
|
||||
request = scoring_request_for(case, radius)
|
||||
request["ayanamsa"] = AYANAMSA
|
||||
request["node_mode"] = NODE_MODE
|
||||
request["minute_step"] = MINUTE_STEP
|
||||
static_contexts = compute_candidate_static_contexts(request)
|
||||
built = build_event_contribution_matrix(request, static_contexts=static_contexts)
|
||||
rows = score_from_matrix(request, built)
|
||||
times = [stamp for row in rows if (stamp := _hhmm(row.get("time")))]
|
||||
probes = discriminating_event_probes(
|
||||
{**request, "refresh_probes": False, "asked_probe_keys": []},
|
||||
built,
|
||||
scan=window_scan(built),
|
||||
candidate_times=times,
|
||||
representative_time=true_time,
|
||||
today=TODAY,
|
||||
)
|
||||
state = posterior_state(rows=rows, contexts=static_contexts, probes=probes[:ASK_COUNT], true_time=true_time)
|
||||
gate = precision_gate(state["valid"], state["scores"])
|
||||
if gate["met"]:
|
||||
return {
|
||||
"case_id": case.get("case_id"),
|
||||
"radius": radius,
|
||||
"events_needed": 0,
|
||||
"windows": 0,
|
||||
"met": True,
|
||||
"after_six": gate,
|
||||
}
|
||||
remaining = remaining_times(state)
|
||||
windows = guided_collect_windows(
|
||||
request,
|
||||
built,
|
||||
candidate_times=remaining or times,
|
||||
today=TODAY,
|
||||
)
|
||||
extras: list[dict[str, Any]] = []
|
||||
last_gate = gate
|
||||
for index, window in enumerate(windows):
|
||||
extras.append(synthetic_event(window, index))
|
||||
injected = {**request, "events": list(request["events"]) + extras}
|
||||
rebuilt = build_event_contribution_matrix(
|
||||
scoreable_request(injected),
|
||||
static_contexts=static_contexts,
|
||||
)
|
||||
new_rows = score_from_matrix(scoreable_request(injected), rebuilt)
|
||||
state = posterior_state(
|
||||
rows=new_rows,
|
||||
contexts=static_contexts,
|
||||
probes=probes[:ASK_COUNT],
|
||||
true_time=true_time,
|
||||
)
|
||||
last_gate = precision_gate(state["valid"], state["scores"])
|
||||
if last_gate["met"]:
|
||||
return {
|
||||
"case_id": case.get("case_id"),
|
||||
"radius": radius,
|
||||
"events_needed": len(extras),
|
||||
"windows": len(windows),
|
||||
"met": True,
|
||||
"after_six": gate,
|
||||
"final": last_gate,
|
||||
}
|
||||
return {
|
||||
"case_id": case.get("case_id"),
|
||||
"radius": radius,
|
||||
"events_needed": None,
|
||||
"windows": len(windows),
|
||||
"met": False,
|
||||
"after_six": gate,
|
||||
"final": last_gate,
|
||||
}
|
||||
|
||||
|
||||
def summarize(rows: Sequence[dict[str, Any]], radius: int) -> dict[str, Any]:
|
||||
subset = [row for row in rows if row.get("radius") == radius and not row.get("error")]
|
||||
needed = [int(row["events_needed"]) for row in subset if row.get("events_needed") is not None]
|
||||
met = sum(1 for row in subset if row.get("met"))
|
||||
return {
|
||||
"radius": radius,
|
||||
"n": len(subset),
|
||||
"met": met,
|
||||
"median_events_to_gate": statistics.median(needed) if needed else None,
|
||||
"unmet": sum(1 for row in subset if not row.get("met")),
|
||||
"errors": sum(1 for row in rows if row.get("radius") == radius and row.get("error")),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
parser.add_argument("--radii", default="10,30,60")
|
||||
parser.add_argument("--json-out", default=str(REPORT_JSON))
|
||||
args = parser.parse_args()
|
||||
radii = tuple(int(item) for item in str(args.radii).split(",") if item.strip())
|
||||
cases = load_cases()
|
||||
if args.limit:
|
||||
cases = cases[: args.limit]
|
||||
started = time.perf_counter()
|
||||
rows: list[dict[str, Any]] = []
|
||||
for case in cases:
|
||||
for radius in radii:
|
||||
label = f"{case.get('case_id')} ±{radius}"
|
||||
try:
|
||||
result = evaluate_case(case, radius)
|
||||
rows.append(result)
|
||||
print(
|
||||
f"{label} needed={result.get('events_needed')} "
|
||||
f"met={result.get('met')} windows={result.get('windows')}",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
rows.append({
|
||||
"case_id": case.get("case_id"),
|
||||
"radius": radius,
|
||||
"error": f"{type(exc).__name__}: {exc}",
|
||||
"trace": traceback.format_exc(limit=8),
|
||||
"met": False,
|
||||
"events_needed": None,
|
||||
})
|
||||
print(f"{label} ERROR {type(exc).__name__}: {exc}", flush=True)
|
||||
summaries = [summarize(rows, radius) for radius in radii]
|
||||
payload = {
|
||||
"generated_at": TODAY.isoformat(),
|
||||
"holdout": str(HOLDOUT.relative_to(ROOT)).replace("\\", "/"),
|
||||
"ask_count": ASK_COUNT,
|
||||
"delivery_max_width_minutes": DELIVERY_MAX_WIDTH_MINUTES,
|
||||
"tie_percent": RANGE_DELIVERY_TIE_PERCENT,
|
||||
"elapsed_s": round(time.perf_counter() - started, 1),
|
||||
"summaries": summaries,
|
||||
"rows": [
|
||||
{key: value for key, value in row.items() if key != "trace"}
|
||||
for row in rows
|
||||
],
|
||||
"errors": [row for row in rows if row.get("error")],
|
||||
}
|
||||
out = Path(args.json_out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(summaries, ensure_ascii=False, indent=2), flush=True)
|
||||
print(f"wrote {out} in {payload['elapsed_s']}s", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user