"""Timestep-bin TENP aggregation. Default apply path still sums all bins (plain TENP).""" from __future__ import annotations import torch from .scoring import fuse_modality_scores DEFAULT_NUM_BINS = 5 DEFAULT_N_MIN = 32 DEFAULT_TEMPORAL_BETA = 0.3 DEFAULT_TEMPORAL_TOPK = 2 TEMPORAL_MODES = ("none", "mean", "ta", "st") T_MAX_DEFAULT = 1000.0 def raw_signed_projection_score(table: torch.Tensor) -> torch.Tensor: """Sum routed signed projections without z-score changing their sign. Accepts either ``[3, d_int]`` or ``[K, 3, d_int]`` and sums only the modality axis. This score is deliberately separate from the normalized TENP ranking score. """ if table.ndim == 2 and table.shape[0] == 3: return table.detach().float().sum(dim=0) if table.ndim == 3 and table.shape[1] == 3: return table.detach().float().sum(dim=1) raise ValueError(f"projection table must be [3,d] or [K,3,d], got {tuple(table.shape)}") def _positive_percentile_rank( importance_bin: torch.Tensor, signed_bin: torch.Tensor, ) -> torch.Tensor: """Rank neurons within each bin, while raw non-positive neurons score zero.""" if importance_bin.ndim != 2 or signed_bin.shape != importance_bin.shape: raise ValueError( f"importance/signed bins must share [K,d], got " f"{tuple(importance_bin.shape)} and {tuple(signed_bin.shape)}" ) ranks = torch.zeros_like(importance_bin, dtype=torch.float32) for bin_id in range(int(importance_bin.shape[0])): positive = torch.nonzero(signed_bin[bin_id] > 0, as_tuple=False).reshape(-1) n_pos = int(positive.numel()) if n_pos == 0: continue order = torch.argsort(importance_bin[bin_id, positive], stable=True) percentile = torch.empty(n_pos, device=importance_bin.device, dtype=torch.float32) percentile[order] = ( torch.arange(1, n_pos + 1, device=importance_bin.device, dtype=torch.float32) / float(n_pos) ) ranks[bin_id, positive] = percentile return ranks def temporal_criticality( importance_bin: torch.Tensor, signed_bin: torch.Tensor, topq: int = DEFAULT_TEMPORAL_TOPK, adjacent: bool = False, ) -> torch.Tensor: """Top-Q mean of per-bin positive percentile ranks for each neuron.""" ranks = _positive_percentile_rank(importance_bin, signed_bin) k = int(ranks.shape[0]) q = min(max(int(topq), 1), k) if not adjacent or q == 1: return torch.topk(ranks, q, dim=0).values.mean(dim=0) windows = torch.stack( [ranks[start : start + q].mean(dim=0) for start in range(k - q + 1)], dim=0, ) return windows.max(dim=0).values def temporal_coverage( importance_bin: torch.Tensor, keep_idx: torch.Tensor, eps: float = 1e-8, ) -> torch.Tensor: """Fraction of positive per-bin importance retained by ``keep_idx``.""" if importance_bin.ndim != 2: raise ValueError(f"importance_bin must be [K,d], got {tuple(importance_bin.shape)}") keep = keep_idx.detach().long().reshape(-1) positive = importance_bin.detach().float().clamp_min(0) retained = positive[:, keep].sum(dim=1) return retained / positive.sum(dim=1).clamp_min(eps) def timestep_to_bin( timestep: float | None, num_bins: int, step_index: int | None = None, num_steps: int | None = None, t_max: float = T_MAX_DEFAULT, ) -> int: """Map a diffusion step onto `num_bins` equal bins. Prefer inference `step_index` so each bin gets a similar number of solver steps. Fall back to the scheduler value in `[0, t_max)`. Missing timestep lands in bin 0, which keeps legacy dumps identical to plain TENP. """ k = max(int(num_bins), 1) if k == 1: return 0 if step_index is not None and num_steps: n = max(int(num_steps), 1) return min(k - 1, max(0, int(step_index) * k // n)) if timestep is None: return 0 t = float(timestep) if t_max <= 0: return 0 return min(k - 1, max(0, int(t / float(t_max) * k))) def merge_sparse_bins( n_tokens_bin: torch.Tensor, n_min: int = DEFAULT_N_MIN, ) -> torch.Tensor: """Keep-mask over bins. Sparse bins are folded into the nearest kept neighbor.""" counts = n_tokens_bin.detach().long().reshape(-1) k = int(counts.numel()) keep = counts >= int(n_min) if bool(keep.any()) is False: keep = counts > 0 if bool(keep.any()) is False: keep = torch.ones(k, dtype=torch.bool, device=counts.device) keep[1:] = False return keep def fold_sparse_bin_mass( n_tokens_bin: torch.Tensor, values: torch.Tensor | None = None, n_min: int = DEFAULT_N_MIN, ) -> tuple[torch.Tensor, torch.Tensor]: """Move sparse-bin token mass (and optional values) onto the nearest kept bin. Returns (folded_counts, folded_values). `values` is typically D_{e,k}; when omitted, only counts are folded. """ n = n_tokens_bin.detach().float().reshape(-1).clone() keep = merge_sparse_bins(n, n_min=n_min) if values is None: v = torch.zeros_like(n) has_v = False else: v = values.detach().float().reshape(-1).clone() if v.numel() != n.numel(): raise ValueError(f"values {tuple(v.shape)} != n_tokens_bin {tuple(n.shape)}") has_v = True kept_idx = torch.nonzero(keep, as_tuple=False).reshape(-1) for i in range(int(n.numel())): if bool(keep[i]): continue if float(n[i]) <= 0: continue nearest = int(kept_idx[torch.argmin((kept_idx - i).abs())].item()) if has_v: mass = float(n[nearest]) + float(n[i]) if mass > 0: v[nearest] = (v[nearest] * n[nearest] + v[i] * n[i]) / mass v[i] = 0 n[nearest] = n[nearest] + n[i] n[i] = 0 return n, v def routing_sensitivity_alpha( n_tokens_bin: torch.Tensor, sensitivity: torch.Tensor, n_min: int = DEFAULT_N_MIN, eps: float = 1e-8, layer_sensitivity: torch.Tensor | None = None, ) -> torch.Tensor: """α_{e,k} = π_{e,k} D_{e,k} / sum_j(π D). Sparse bins fold into the nearest kept bin.""" n = n_tokens_bin.detach().float().reshape(-1) d = sensitivity.detach().float().reshape(-1) if d.numel() != n.numel(): raise ValueError(f"sensitivity {tuple(d.shape)} != n_tokens_bin {tuple(n.shape)}") if layer_sensitivity is not None: d_layer = layer_sensitivity.detach().float().reshape(-1) if d_layer.numel() == d.numel(): keep = merge_sparse_bins(n, n_min=n_min) d = torch.where(~keep, d_layer, d) n, d = fold_sparse_bin_mass(n, d, n_min=n_min) pi = n / n.sum().clamp_min(1.0) raw = pi * d.clamp_min(0) if float(raw.sum()) <= 0: raw = pi return raw / raw.sum().clamp_min(eps) def mean_alpha(n_tokens_bin: torch.Tensor, n_min: int = DEFAULT_N_MIN) -> torch.Tensor: keep = merge_sparse_bins(n_tokens_bin, n_min=n_min) n_fold, _ = fold_sparse_bin_mass(n_tokens_bin, n_min=n_min) w = torch.where(n_fold > 0, torch.ones_like(n_fold), torch.zeros_like(n_fold)) if float(w.sum()) <= 0: w = keep.float() return w / w.sum().clamp_min(1.0) def fuse_bins_to_importance( table_bin: torch.Tensor, n_tokens_bin: torch.Tensor, lambdas: tuple[float, float, float], n_min: int = DEFAULT_N_MIN, zscore: bool = True, ) -> torch.Tensor: """Per-bin modality fusion. `table_bin`: [K, 3, d_int] -> [K, d_int].""" if table_bin.ndim != 3 or table_bin.shape[1] != 3: raise ValueError(f"table_bin must be [K, 3, d_int], got {tuple(table_bin.shape)}") k, _, d_int = table_bin.shape folded = table_bin.detach().float().clone() keep = merge_sparse_bins(n_tokens_bin, n_min=n_min) kept_idx = torch.nonzero(keep, as_tuple=False).reshape(-1) for bin_id in range(k): if bool(keep[bin_id]): continue if float(folded[bin_id].abs().sum()) == 0: continue nearest = int(kept_idx[torch.argmin((kept_idx - bin_id).abs())].item()) folded[nearest] = folded[nearest] + folded[bin_id] folded[bin_id] = 0 out = torch.zeros(k, d_int, device=table_bin.device, dtype=torch.float32) for bin_id in range(k): if not bool(keep[bin_id]): continue row = folded[bin_id] if float(row.abs().sum()) == 0: continue out[bin_id] = fuse_modality_scores(row, lambdas=lambdas, zscore=zscore) return out def aggregate_temporal( importance_bin: torch.Tensor, alpha: torch.Tensor | None = None, mode: str = "st", beta: float = DEFAULT_TEMPORAL_BETA, temporal_topk: int = DEFAULT_TEMPORAL_TOPK, ) -> torch.Tensor: """Reduce [K, d_int] bin scores to one neuron vector.""" if mode not in TEMPORAL_MODES: raise ValueError(f"unknown temporal mode {mode!r}; expected one of {TEMPORAL_MODES}") if importance_bin.ndim != 2: raise ValueError(f"importance_bin must be [K, d_int], got {tuple(importance_bin.shape)}") k = int(importance_bin.shape[0]) if mode == "none": raise ValueError("mode 'none' is handled by the summed projection table, not here") live = importance_bin.abs().sum(dim=1) > 0 if not bool(live.any()): return importance_bin.sum(dim=0) stacked = importance_bin[live] if mode == "mean": if alpha is None: return stacked.mean(dim=0) w = alpha.detach().float().reshape(-1)[live] w = w / w.sum().clamp_min(1e-8) return (w.unsqueeze(-1) * stacked).sum(dim=0) if mode == "ta": mean = stacked.mean(dim=0) topk = min(max(int(temporal_topk), 1), int(stacked.shape[0])) peak = stacked.topk(topk, dim=0).values.mean(dim=0) return mean + float(beta) * peak if alpha is None: raise ValueError("ST-TENP needs alpha") w = alpha.detach().float().reshape(-1) if w.numel() != k: raise ValueError(f"alpha {tuple(w.shape)} != K={k}") w = torch.where(live, w, torch.zeros_like(w)) w = w / w.sum().clamp_min(1e-8) return (w.unsqueeze(-1) * importance_bin).sum(dim=0) def bin_coefficient_of_variation(importance_bin: torch.Tensor, eps: float = 1e-8) -> torch.Tensor: """Per-neuron CV across timestep bins. Empty bins are ignored.""" live = importance_bin.abs().sum(dim=1) > 0 stacked = importance_bin[live] if bool(live.any()) else importance_bin mu = stacked.mean(dim=0) sigma = stacked.std(dim=0, unbiased=False) return sigma / (mu.abs() + eps) def topk_jaccard(importance_bin: torch.Tensor, frac: float = 0.20) -> torch.Tensor: """Pairwise Jaccard of per-bin top-(frac) neurons. Diagonal is 1.""" k, d_int = importance_bin.shape n_keep = max(1, int(round(float(frac) * d_int))) n_keep = min(n_keep, d_int) sets: list[set[int]] = [] live = importance_bin.abs().sum(dim=1) > 0 for bin_id in range(k): if not bool(live[bin_id]): sets.append(set()) continue _, idx = torch.topk(importance_bin[bin_id], n_keep) sets.append(set(int(x) for x in idx.tolist())) jac = torch.zeros(k, k, dtype=torch.float32) for i in range(k): for j in range(k): if not sets[i] or not sets[j]: jac[i, j] = 0.0 if i != j else 1.0 continue inter = len(sets[i] & sets[j]) union = len(sets[i] | sets[j]) jac[i, j] = float(inter) / float(union) if union else 0.0 return jac