"""Pure helpers for the minute-resolution scoring sweep. No production defaults.""" from __future__ import annotations import math from collections.abc import Iterable, Sequence from typing import Any from scripts.active_rectification_event_engine import DOMAIN_CONFIG from scripts.rectification.candidate_contrast import ( SIGNATURE_LAYERS, cluster_contexts_by_signature, context_time, layer_value, select_signature_representatives, ) from scripts.rectification.scoring_service import PRECISION_WEIGHTS, _event_kind_factor VARGA_RULE_WEIGHT = { "vim_md_domain_varga": 2.0, "vim_ad_domain_varga": 1.5, "vim_pd_domain_varga": 0.75, } KP_MATCH = { "md": (0.50, 0.25), "ad": (0.35, 0.15), "pd": (0.20, 0.10), } DOMAIN_SIGNATURE_LAYERS = { "education": ("d24", "d5"), "relocation": ("d4",), "relationship": ("d9",), "career": ("d10",), "occupation": ("d10",), "finance": ("d2", "d11"), "health_pressure": ("d30",), "family": ("d12", "d7", "d3"), } EXTRA_LAYER_VARGA = { "d2": "D2", "d3": "D3", "d5": "D5", "d7": "D7", "d11": "D11", "d30": "D30", } NARROW_WIDTH = 5 def clock(value: str) -> int: stamp = str(value)[:5] return int(stamp[:2]) * 60 + int(stamp[3:5]) def hhmm_from_minutes(value: int) -> str: wrapped = value % 1440 return f"{wrapped // 60:02d}:{wrapped % 60:02d}" def shift_clock(value: str, delta: int) -> str: return hhmm_from_minutes(clock(value) + delta) def range_width(times: Sequence[str]) -> int | None: clocks = sorted(clock(item) for item in times if str(item)[:5]) if not clocks: return None return clocks[-1] - clocks[0] + 1 def varga_divisor(count: int, mode: str) -> float: n = max(int(count), 1) if mode == "len": return float(n) if mode == "sqrt": return math.sqrt(n) if mode == "fixed2": return 2.0 return 2.0 * n def rescale_varga_points( points: float, rule_ids: Sequence[str], domain: str, event_kind: str, precision: str, mode: str, ) -> float: if mode == "2len": return float(points) prefixes, _houses = DOMAIN_CONFIG[domain] n = max(len(prefixes), 1) old_div = varga_divisor(n, "2len") new_div = varga_divisor(n, mode) if abs(old_div - new_div) < 1e-12: return float(points) delta_inner = 0.0 for rule in rule_ids: weight = VARGA_RULE_WEIGHT.get(str(rule)) if weight is None: continue delta_inner += weight * (1.0 / new_div - 1.0 / old_div) precision_w = PRECISION_WEIGHTS.get(precision, 1.0) kind = _event_kind_factor(event_kind, rule_ids) return round(float(points) + delta_inner * kind * (precision_w ** 2), 4) def aggregate_samples(samples: Sequence[float], mode: str, temperature: float = 1.0) -> float: values = [float(item) for item in samples] if not values: return 0.0 if mode == "max": return round(max(values), 4) if mode == "lse": temp = max(float(temperature), 1e-6) peak = max(values) mean_exp = sum(math.exp((item - peak) / temp) for item in values) / len(values) return round(temp * math.log(mean_exp) + peak, 4) return round(sum(values) / len(values), 4) def subtract_event_floor(by_time: dict[str, float]) -> dict[str, float]: if not by_time: return {} floor = min(by_time.values()) return {time: round(score - floor, 4) for time, score in by_time.items()} def shannon_entropy(weights: Iterable[float]) -> float: values = [max(float(item), 0.0) for item in weights] total = sum(values) if total <= 0: return 0.0 entropy = 0.0 for value in values: if value <= 0: continue share = value / total entropy -= share * math.log(share, 2) return round(entropy, 6) def kp_event_points( context: dict[str, Any], domain: str, vim_lords: tuple[str, str, str], weight: float, ) -> float: feature = context.get("feature") if isinstance(context.get("feature"), dict) else {} snapshot = feature.get("kp_cusps") if isinstance(feature.get("kp_cusps"), dict) else {} if snapshot.get("status") != "executed": return 0.0 houses = snapshot.get("houses") if isinstance(snapshot.get("houses"), dict) else {} _prefixes, target_houses = DOMAIN_CONFIG[domain] points = 0.0 md, ad, pd = vim_lords for house in target_houses: row = houses.get(str(house)) if not isinstance(row, dict): continue sub = str(row.get("sub_lord") or "") sub_sub = str(row.get("sub_sub_lord") or "") for lord, (sub_w, sub_sub_w) in ( (md, KP_MATCH["md"]), (ad, KP_MATCH["ad"]), (pd, KP_MATCH["pd"]), ): if lord and lord == sub: points += sub_w if lord and lord == sub_sub: points += sub_sub_w return round(points * float(weight), 4) def kp_sub_lord_key(context: dict[str, Any], houses: Sequence[int]) -> tuple[str, ...]: feature = context.get("feature") if isinstance(context.get("feature"), dict) else {} snapshot = feature.get("kp_cusps") if isinstance(feature.get("kp_cusps"), dict) else {} table = snapshot.get("houses") if isinstance(snapshot.get("houses"), dict) else {} keys = [] for house in houses: row = table.get(str(house)) if isinstance(table.get(str(house)), dict) else {} keys.append(str(row.get("sub_lord") or "")) return tuple(keys) def kp_changes_in_window(contexts: Sequence[dict[str, Any]], domain: str) -> int: _prefixes, houses = DOMAIN_CONFIG[domain] seen: list[tuple[str, ...]] = [] for context in contexts: key = kp_sub_lord_key(context, houses) if not seen or seen[-1] != key: seen.append(key) return max(len(seen) - 1, 0) def dynamic_signature_layers(domains: Sequence[str]) -> tuple[str, ...]: layers = ["d1"] for domain in domains: for layer in DOMAIN_SIGNATURE_LAYERS.get(domain, ()): if layer not in layers: layers.append(layer) return tuple(layers) def layer_value_extended(context: dict[str, Any], layer: str) -> int | None: if layer in SIGNATURE_LAYERS or layer == "md" or layer == "d1": return layer_value(context, layer) name = EXTRA_LAYER_VARGA.get(layer) if not name: return None feature = context.get("feature") if isinstance(context.get("feature"), dict) else {} vargas = feature.get("varga_ascendants") if isinstance(feature.get("varga_ascendants"), dict) else {} raw = vargas.get(name) return raw if isinstance(raw, int) else None def cluster_contexts( contexts: Sequence[dict[str, Any]], layers: Sequence[str] | None = None, ) -> list[dict[str, Any]]: if not layers or tuple(layers) == SIGNATURE_LAYERS: return cluster_contexts_by_signature(contexts) buckets: dict[tuple[int | None, ...], list[dict[str, Any]]] = {} for context in contexts: if not isinstance(context, dict): continue time = context_time(context) if not time: continue signature = tuple(layer_value_extended(context, layer) for layer in layers) buckets.setdefault(signature, []).append(context) clusters: list[dict[str, Any]] = [] for signature, members in buckets.items(): ordered = sorted(members, key=lambda item: clock(str(context_time(item)))) times = [str(context_time(item)) for item in ordered] clusters.append({ "signature": signature, "signature_key": ",".join("x" if value is None else str(value) for value in signature), "contexts": ordered, "times": times, "representative_time": times[len(times) // 2], "representative": ordered[len(ordered) // 2], }) clusters.sort(key=lambda item: clock(item["representative_time"])) return clusters def public_rows( rows: Sequence[dict[str, Any]], contexts: Sequence[dict[str, Any]], layers: Sequence[str] | None = None, ) -> list[dict[str, Any]]: if not layers or tuple(layers) == SIGNATURE_LAYERS: return select_signature_representatives(rows, contexts) by_time = {str(row.get("time"))[:5]: row for row in rows if str(row.get("time"))} clusters = cluster_contexts(contexts, layers) from scripts.rectification.candidate_contrast import cap_clusters_by_adjacent_merge clusters = cap_clusters_by_adjacent_merge(clusters, by_time) representatives: list[dict[str, Any]] = [] for cluster in clusters: members = [by_time[time] for time in cluster["times"] if time in by_time] if not members: continue best = max(members, key=lambda row: (float(row.get("score") or 0), str(row.get("time")))) representatives.append({ **best, "cluster_times": [time for time in cluster["times"] if time in by_time], }) representatives.sort(key=lambda row: (-float(row.get("score") or 0), str(row.get("time")))) return representatives or list(rows)[:1] def cluster_of(true_time: str, clusters: Sequence[dict[str, Any]]) -> dict[str, Any] | None: stamp = str(true_time)[:5] for cluster in clusters: times = [str(item)[:5] for item in cluster.get("times") or []] if stamp in times: return cluster return None def metrics_from_public( public: Sequence[dict[str, Any]], true_time: str, window_times: Sequence[str], ) -> dict[str, Any]: if not public: return { "top1_hit": False, "coverage": False, "width": None, "tie": False, "entropy": 0.0, "truth_squeezed": True, "too_narrow": False, "leader_count": 0, "public_count": 0, "true_cluster_rank": None, "true_cluster_size": 0, } scores = [float(row.get("score") or 0) for row in public] best = max(scores) leaders = [row for row in public if abs(float(row.get("score") or 0) - best) <= 1e-9] delivered = [] seen: set[str] = set() for row in public: for time in row.get("cluster_times") or [str(row.get("time"))[:5]]: stamp = str(time)[:5] if stamp not in seen: seen.add(stamp) delivered.append(stamp) true = str(true_time)[:5] true_in_leaders = any(true in (row.get("cluster_times") or [str(row.get("time"))[:5]]) for row in leaders) if not true_in_leaders: true_in_leaders = any(str(row.get("time"))[:5] == true for row in leaders) rank = None size = 0 ordered = sorted(public, key=lambda row: (-float(row.get("score") or 0), str(row.get("time")))) for index, row in enumerate(ordered, start=1): members = [str(item)[:5] for item in (row.get("cluster_times") or [str(row.get("time"))[:5]])] if true in members: rank = index size = len(members) break width = range_width(delivered) second = sorted({round(float(row.get("score") or 0), 4) for row in public}, reverse=True) tied = len(leaders) >= 2 or (len(second) >= 2 and abs(second[0] - second[1]) <= 1e-9) return { "top1_hit": true_in_leaders, "coverage": true in seen, "width": width, "tie": tied, "entropy": shannon_entropy(max(score, 0.0) for score in scores), "truth_squeezed": true not in seen, "too_narrow": width is not None and width <= NARROW_WIDTH and true in seen, "leader_count": len(leaders), "public_count": len(public), "true_cluster_rank": rank, "true_cluster_size": size, "window_width": range_width(list(window_times)), }