Buckets:
| #!/usr/bin/env python3 | |
| """Exact finite-sample numerical audits for Claims 1--5. | |
| This script reproduces the two-component Gaussian-mixture model from | |
| "The Catastrophic Failure of the k-Means Algorithm in High Dimensions, | |
| and How Hartigan's Algorithm Avoids It" without depending on the authors' | |
| implementation. | |
| The key reduction is exact. Conditional on fixed ground-truth labels, each | |
| feature column of X is Gaussian with covariance | |
| Sigma = tau**2 * Z Z.T + sigma**2 * I. | |
| Every Lloyd/Hartigan fixed-point comparison depends on X only through the | |
| Gram matrix G = X X.T, and therefore G is Wishart(d, Sigma). Bartlett draws | |
| make exhaustive partition checks inexpensive even when d is very large. | |
| Only the Python standard library and NumPy are required. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import html | |
| import json | |
| import math | |
| import os | |
| import platform | |
| import sys | |
| import time | |
| from dataclasses import asdict, dataclass | |
| from pathlib import Path | |
| from typing import Any, Iterable, Sequence | |
| import numpy as np | |
| PAPER_URL = "https://arxiv.org/abs/2602.09936" | |
| CODE_URL = ( | |
| "https://github.com/Lederman-Group/Catastrophic_Failure_KMeans/" | |
| "tree/e460f6198c42685e75efa7877bb66a55cab4d460" | |
| ) | |
| ZENODO_URL = "https://zenodo.org/records/20435365" | |
| FLOAT_EPS = np.finfo(np.float64).eps | |
| TIE_ULPS = 128.0 | |
| class Profile: | |
| name: str | |
| high_dimensions: tuple[int, ...] | |
| low_dimensions: tuple[int, ...] | |
| high_trials: int | |
| low_trials: int | |
| partition_batch_size: int | |
| claim5_dimensions: tuple[int, ...] | |
| claim5_ratios: tuple[float, ...] | |
| claim5_trials: int | |
| wishart_batch_size: int | |
| PROFILES = { | |
| "smoke": Profile( | |
| name="smoke", | |
| high_dimensions=(64, 1024, 4096, 4245, 4631, 5913, 8192, 32768, 255164, 349345), | |
| low_dimensions=(64, 1024, 8192, 349345), | |
| high_trials=240, | |
| low_trials=160, | |
| partition_batch_size=80, | |
| claim5_dimensions=(8, 128, 1024, 8192, 100000), | |
| claim5_ratios=(0.5, 0.9, 1.0, 1.1, 1.5, 2.0), | |
| claim5_trials=1200, | |
| wishart_batch_size=1200, | |
| ), | |
| "full": Profile( | |
| name="full", | |
| high_dimensions=( | |
| 64, | |
| 256, | |
| 1024, | |
| 4096, | |
| 4245, | |
| 4631, | |
| 5913, | |
| 8192, | |
| 32768, | |
| 131072, | |
| 226812, | |
| 255164, | |
| 349345, | |
| 524288, | |
| ), | |
| low_dimensions=(64, 1024, 8192, 131072, 524288), | |
| high_trials=3000, | |
| low_trials=1200, | |
| partition_batch_size=150, | |
| claim5_dimensions=(8, 32, 128, 512, 1024, 2048, 8192, 32768, 100000), | |
| claim5_ratios=(0.1, 0.5, 0.9, 1.0, 1.1, 1.5, 2.0), | |
| claim5_trials=10000, | |
| wishart_batch_size=2500, | |
| ), | |
| } | |
| def _native(value: Any) -> Any: | |
| """Convert NumPy values recursively into strict JSON-compatible values.""" | |
| if isinstance(value, dict): | |
| return {str(key): _native(val) for key, val in value.items()} | |
| if isinstance(value, (list, tuple)): | |
| return [_native(val) for val in value] | |
| if isinstance(value, np.ndarray): | |
| return _native(value.tolist()) | |
| if isinstance(value, (np.integer,)): | |
| return int(value) | |
| if isinstance(value, (np.floating, float)): | |
| val = float(value) | |
| return val if math.isfinite(val) else None | |
| if isinstance(value, (np.bool_, bool)): | |
| return bool(value) | |
| return value | |
| def wilson_interval(successes: int, trials: int, alpha: float = 0.05) -> tuple[float, float]: | |
| """Two-sided Wilson score interval using the standard-normal quantile. | |
| Python's standard library has no inverse normal CDF, but NormalDist does. | |
| Importing locally keeps compatibility with older minimal Python installs. | |
| """ | |
| if trials <= 0: | |
| raise ValueError("trials must be positive") | |
| if not 0 <= successes <= trials: | |
| raise ValueError("successes must lie in [0, trials]") | |
| if not 0.0 < alpha < 1.0: | |
| raise ValueError("alpha must lie in (0, 1)") | |
| from statistics import NormalDist | |
| z = NormalDist().inv_cdf(1.0 - alpha / 2.0) | |
| phat = successes / trials | |
| z2 = z * z | |
| denominator = 1.0 + z2 / trials | |
| center = (phat + z2 / (2.0 * trials)) / denominator | |
| half_width = ( | |
| z | |
| * math.sqrt(phat * (1.0 - phat) / trials + z2 / (4.0 * trials * trials)) | |
| / denominator | |
| ) | |
| return max(0.0, center - half_width), min(1.0, center + half_width) | |
| def enumerate_bipartitions(n: int) -> np.ndarray: | |
| """Enumerate each nonempty unlabeled bipartition exactly once. | |
| Point zero is fixed in cluster 0, removing complement symmetry. Rows are | |
| binary membership indicators for cluster 1. | |
| """ | |
| if n < 2: | |
| raise ValueError("n must be at least 2") | |
| values = np.arange(1, 1 << (n - 1), dtype=np.uint64) | |
| bit_positions = np.arange(n - 1, dtype=np.uint64) | |
| remaining = ((values[:, None] >> bit_positions[None, :]) & 1).astype(np.float64) | |
| return np.concatenate((np.zeros((remaining.shape[0], 1)), remaining), axis=1) | |
| def q_balanced_mask(partitions: np.ndarray, q: float) -> np.ndarray: | |
| """Definition 2.5, including strict bounds and the |C_k| > 2 clause.""" | |
| if q <= 1.0: | |
| raise ValueError("Corollary 3.8 requires q > 1") | |
| n = partitions.shape[1] | |
| sizes = partitions.sum(axis=1) | |
| lower = n / 2.0 - q * math.sqrt(n / 4.0) | |
| upper = n / 2.0 + q * math.sqrt(n / 4.0) | |
| other_sizes = n - sizes | |
| return ( | |
| (sizes > 2.0) | |
| & (other_sizes > 2.0) | |
| & (sizes > lower) | |
| & (sizes < upper) | |
| & (other_sizes > lower) | |
| & (other_sizes < upper) | |
| ) | |
| def model_covariance(true_labels: np.ndarray, tau2: float, sigma2: float) -> np.ndarray: | |
| """Covariance across observations for one independent feature column.""" | |
| labels = np.asarray(true_labels) | |
| if labels.ndim != 1: | |
| raise ValueError("true_labels must be one-dimensional") | |
| if tau2 <= 0.0 or sigma2 <= 0.0: | |
| raise ValueError("tau2 and sigma2 must be positive") | |
| shared_center = labels[:, None] == labels[None, :] | |
| return tau2 * shared_center.astype(np.float64) + sigma2 * np.eye(labels.size) | |
| def sample_wishart_normalized( | |
| rng: np.random.Generator, | |
| covariance: np.ndarray, | |
| degrees_of_freedom: int, | |
| draws: int, | |
| ) -> np.ndarray: | |
| """Draw Wishart matrices with Bartlett's decomposition and divide by d.""" | |
| covariance = np.asarray(covariance, dtype=np.float64) | |
| p = covariance.shape[0] | |
| if covariance.shape != (p, p): | |
| raise ValueError("covariance must be square") | |
| if degrees_of_freedom < p: | |
| raise ValueError("Bartlett decomposition here requires d >= matrix dimension") | |
| if draws <= 0: | |
| raise ValueError("draws must be positive") | |
| chol = np.linalg.cholesky(covariance) | |
| bartlett = np.zeros((draws, p, p), dtype=np.float64) | |
| row, col = np.tril_indices(p, k=-1) | |
| bartlett[:, row, col] = rng.standard_normal((draws, row.size)) | |
| for idx in range(p): | |
| bartlett[:, idx, idx] = np.sqrt( | |
| rng.chisquare(degrees_of_freedom - idx, size=draws) | |
| ) | |
| transformed = np.einsum("ij,tjk->tik", chol, bartlett, optimize=True) | |
| gram = np.einsum("tik,tjk->tij", transformed, transformed, optimize=True) | |
| gram /= float(degrees_of_freedom) | |
| return gram | |
| def gram_partition_distances( | |
| grams: np.ndarray, partitions: np.ndarray | |
| ) -> tuple[np.ndarray, np.ndarray]: | |
| """Squared distances to both empirical centroids from Gram matrices. | |
| Returned arrays have shape (draw, partition, point). Since input Gram | |
| matrices may be divided by d, distances carry the same harmless scaling. | |
| """ | |
| grams = np.asarray(grams, dtype=np.float64) | |
| partitions = np.asarray(partitions, dtype=np.float64) | |
| if grams.ndim != 3 or grams.shape[1] != grams.shape[2]: | |
| raise ValueError("grams must have shape (draw, n, n)") | |
| if partitions.ndim != 2 or partitions.shape[1] != grams.shape[1]: | |
| raise ValueError("partition width must match Gram dimension") | |
| n = grams.shape[1] | |
| sizes1 = partitions.sum(axis=1) | |
| sizes0 = n - sizes1 | |
| if np.any(sizes1 <= 0.0) or np.any(sizes0 <= 0.0): | |
| raise ValueError("all partitions must contain two nonempty clusters") | |
| complements = 1.0 - partitions | |
| gb1 = np.einsum("tij,mj->tmi", grams, partitions, optimize=True) | |
| gb0 = np.einsum("tij,mj->tmi", grams, complements, optimize=True) | |
| quadratic1 = np.einsum("mi,tmi->tm", partitions, gb1, optimize=True) | |
| quadratic0 = np.einsum("mi,tmi->tm", complements, gb0, optimize=True) | |
| diagonal = np.diagonal(grams, axis1=1, axis2=2)[:, None, :] | |
| distance1 = ( | |
| diagonal | |
| - 2.0 * gb1 / sizes1[None, :, None] | |
| + quadratic1[:, :, None] / (sizes1[None, :, None] ** 2) | |
| ) | |
| distance0 = ( | |
| diagonal | |
| - 2.0 * gb0 / sizes0[None, :, None] | |
| + quadratic0[:, :, None] / (sizes0[None, :, None] ** 2) | |
| ) | |
| return distance0, distance1 | |
| def _near_tie_mask(margin: np.ndarray, current: np.ndarray, alternative: np.ndarray) -> np.ndarray: | |
| scale = np.maximum(np.abs(current) + np.abs(alternative), np.finfo(np.float64).tiny) | |
| return np.abs(margin) <= TIE_ULPS * FLOAT_EPS * scale | |
| def fixed_point_tests( | |
| grams: np.ndarray, partitions: np.ndarray | |
| ) -> dict[str, np.ndarray | int | float]: | |
| """Apply exact Lloyd and Hartigan fixed-point definitions to every row.""" | |
| distance0, distance1 = gram_partition_distances(grams, partitions) | |
| labels = partitions[None, :, :].astype(bool) | |
| current = np.where(labels, distance1, distance0) | |
| alternative = np.where(labels, distance0, distance1) | |
| # A move is accepted only for a strict improvement. Thus a fixed point | |
| # uses current <= alternative (ties occur with probability zero in-model). | |
| lloyd_margin = alternative - current | |
| lloyd_fixed = np.all(lloyd_margin >= 0.0, axis=2) | |
| lloyd_near_ties = _near_tie_mask(lloyd_margin, current, alternative) | |
| n = partitions.shape[1] | |
| sizes1 = partitions.sum(axis=1) | |
| sizes0 = n - sizes1 | |
| current_sizes = np.where(partitions.astype(bool), sizes1[:, None], sizes0[:, None]) | |
| alternative_sizes = n - current_sizes | |
| movable = current_sizes > 1.0 | |
| current_factor = np.zeros_like(current_sizes, dtype=np.float64) | |
| current_factor[movable] = current_sizes[movable] / (current_sizes[movable] - 1.0) | |
| alternative_factor = alternative_sizes / (alternative_sizes + 1.0) | |
| hartigan_current = current * current_factor[None, :, :] | |
| hartigan_alternative = alternative * alternative_factor[None, :, :] | |
| hartigan_margin = hartigan_alternative - hartigan_current | |
| hartigan_fixed = np.all((~movable[None, :, :]) | (hartigan_margin >= 0.0), axis=2) | |
| hartigan_near_ties = movable[None, :, :] & _near_tie_mask( | |
| hartigan_margin, hartigan_current, hartigan_alternative | |
| ) | |
| finite_lloyd_margin = np.abs(lloyd_margin).ravel() | |
| finite_hart_margin = np.abs(hartigan_margin[:, movable]).ravel() | |
| return { | |
| "lloyd_fixed": lloyd_fixed, | |
| "hartigan_fixed": hartigan_fixed, | |
| "lloyd_near_ties": int(lloyd_near_ties.sum()), | |
| "hartigan_near_ties": int(hartigan_near_ties.sum()), | |
| "lloyd_comparisons": int(lloyd_margin.size), | |
| "hartigan_comparisons": int(grams.shape[0] * movable.sum()), | |
| "minimum_abs_lloyd_margin": float(finite_lloyd_margin.min()), | |
| "minimum_abs_hartigan_margin": float(finite_hart_margin.min()), | |
| "negative_distance_count": int(((distance0 < 0.0) | (distance1 < 0.0)).sum()), | |
| } | |
| def corollary_38_parameters(n: int, q: float, beta: float) -> tuple[float, float]: | |
| """Return sigma^2 and rho_q from Theorem 3.6 / Corollary 3.8.""" | |
| if n < 1 or q <= 1.0 or beta <= 1.0: | |
| raise ValueError("requires n >= 1, q > 1, and beta > 1") | |
| root_n = math.sqrt(n) | |
| sigma = beta * (root_n * q + n - 2.0) / ( | |
| math.sqrt(2.0) * math.sqrt(root_n * q + n) | |
| ) | |
| sigma2 = sigma * sigma | |
| numerator = ( | |
| sigma2 | |
| * (root_n * q + n - 2.0) | |
| * (root_n * q + n) | |
| * (root_n * q + n + 2.0) | |
| * (root_n * (sigma2 + 2.0) * (root_n + q) - 4.0) | |
| ) | |
| denominator = ( | |
| n * sigma2 * (root_n + q) ** 2 + (root_n * q + n - 2.0) ** 2 | |
| ) ** 2 | |
| rho_q = numerator / denominator | |
| if not 0.0 < rho_q < 1.0: | |
| raise ArithmeticError(f"rho_q must lie in (0, 1), got {rho_q}") | |
| return sigma2, rho_q | |
| def rho_h_uniform(n: int, tau2: float, sigma2: float, r_star: float) -> float: | |
| term = 4.0 * tau2 * r_star * r_star / n / (3.0 * tau2 + sigma2) | |
| rho = 1.0 - term * term | |
| if not 0.0 < rho < 1.0: | |
| raise ArithmeticError(f"rho_h must lie in (0, 1), got {rho}") | |
| return rho | |
| def probability_bound(log_prefactor: float, rho: float, dimension: int) -> tuple[float, float]: | |
| """Return capped bound and its uncapped natural logarithm.""" | |
| log_bound = log_prefactor + (dimension / 4.0) * math.log(rho) | |
| if log_bound >= 0.0: | |
| return 1.0, log_bound | |
| return math.exp(log_bound), log_bound | |
| def _correct_partition_mask(partitions: np.ndarray, true_labels: np.ndarray) -> np.ndarray: | |
| labels = np.asarray(true_labels, dtype=np.float64) | |
| return np.all(partitions == labels[None, :], axis=1) | np.all( | |
| partitions == (1.0 - labels)[None, :], axis=1 | |
| ) | |
| def run_claims_1_to_4( | |
| rng: np.random.Generator, | |
| profile: Profile, | |
| *, | |
| n: int = 8, | |
| tau2: float = 1.0, | |
| q: float = 1.5, | |
| beta: float = 1.5, | |
| ) -> tuple[list[dict[str, Any]], dict[str, Any]]: | |
| """Exhaustively audit fixed points and both corollary events.""" | |
| if n % 2: | |
| raise ValueError("the default audit expects balanced, even n") | |
| true_labels = np.repeat(np.arange(2), n // 2) | |
| partitions = enumerate_bipartitions(n) | |
| balanced = q_balanced_mask(partitions, q) | |
| correct = _correct_partition_mask(partitions, true_labels) | |
| incorrect = ~correct | |
| high_sigma2, rho_q = corollary_38_parameters(n, q, beta) | |
| low_sigma2 = 1.0 | |
| r_star = 0.5 | |
| settings = [ | |
| ("high_noise", high_sigma2, profile.high_dimensions, profile.high_trials, True), | |
| ("low_noise_control", low_sigma2, profile.low_dimensions, profile.low_trials, False), | |
| ] | |
| rows: list[dict[str, Any]] = [] | |
| aggregate_diagnostics = { | |
| "lloyd_near_ties": 0, | |
| "hartigan_near_ties": 0, | |
| "lloyd_comparisons": 0, | |
| "hartigan_comparisons": 0, | |
| "negative_distance_count": 0, | |
| "hartigan_not_lloyd_violations": 0, | |
| "minimum_abs_lloyd_margin": math.inf, | |
| "minimum_abs_hartigan_margin": math.inf, | |
| } | |
| for regime, sigma2, dimensions, trial_count, cor38_applies in settings: | |
| covariance = model_covariance(true_labels, tau2, sigma2) | |
| rho_h = rho_h_uniform(n, tau2, sigma2, r_star) | |
| for dimension in dimensions: | |
| all_balanced_successes = 0 | |
| any_incorrect_hartigan_successes = 0 | |
| correct_hartigan_successes = 0 | |
| balanced_fixed_fraction_sum = 0.0 | |
| incorrect_hartigan_fraction_sum = 0.0 | |
| all_lloyd_fixed_fraction_sum = 0.0 | |
| trials_done = 0 | |
| while trials_done < trial_count: | |
| batch = min(profile.partition_batch_size, trial_count - trials_done) | |
| grams = sample_wishart_normalized(rng, covariance, dimension, batch) | |
| tested = fixed_point_tests(grams, partitions) | |
| lloyd_fixed = np.asarray(tested["lloyd_fixed"], dtype=bool) | |
| hartigan_fixed = np.asarray(tested["hartigan_fixed"], dtype=bool) | |
| balanced_fixed = lloyd_fixed[:, balanced] | |
| incorrect_hartigan = hartigan_fixed[:, incorrect] | |
| all_balanced_successes += int(np.all(balanced_fixed, axis=1).sum()) | |
| any_incorrect_hartigan_successes += int( | |
| np.any(incorrect_hartigan, axis=1).sum() | |
| ) | |
| correct_hartigan_successes += int(np.all(hartigan_fixed[:, correct], axis=1).sum()) | |
| balanced_fixed_fraction_sum += float(balanced_fixed.mean(axis=1).sum()) | |
| incorrect_hartigan_fraction_sum += float( | |
| incorrect_hartigan.mean(axis=1).sum() | |
| ) | |
| all_lloyd_fixed_fraction_sum += float(lloyd_fixed.mean(axis=1).sum()) | |
| subset_violations = hartigan_fixed & ~lloyd_fixed | |
| aggregate_diagnostics["hartigan_not_lloyd_violations"] += int( | |
| subset_violations.sum() | |
| ) | |
| for key in ( | |
| "lloyd_near_ties", | |
| "hartigan_near_ties", | |
| "lloyd_comparisons", | |
| "hartigan_comparisons", | |
| "negative_distance_count", | |
| ): | |
| aggregate_diagnostics[key] += int(tested[key]) | |
| aggregate_diagnostics["minimum_abs_lloyd_margin"] = min( | |
| aggregate_diagnostics["minimum_abs_lloyd_margin"], | |
| float(tested["minimum_abs_lloyd_margin"]), | |
| ) | |
| aggregate_diagnostics["minimum_abs_hartigan_margin"] = min( | |
| aggregate_diagnostics["minimum_abs_hartigan_margin"], | |
| float(tested["minimum_abs_hartigan_margin"]), | |
| ) | |
| trials_done += batch | |
| exists_not_lloyd_fixed = trial_count - all_balanced_successes | |
| lloyd_event_ci = wilson_interval(exists_not_lloyd_fixed, trial_count) | |
| hartigan_event_ci = wilson_interval( | |
| any_incorrect_hartigan_successes, trial_count | |
| ) | |
| cor38_bound, cor38_log = probability_bound( | |
| n * math.log(2.0) + math.log(n), rho_q, dimension | |
| ) | |
| cor312_bound, cor312_log = probability_bound( | |
| n * math.log(2.0), rho_h, dimension | |
| ) | |
| row = { | |
| "claim_group": "claims_1_to_4", | |
| "regime": regime, | |
| "n": n, | |
| "dimension": dimension, | |
| "tau2": tau2, | |
| "sigma2": sigma2, | |
| "q": q, | |
| "beta": beta, | |
| "trials": trial_count, | |
| "total_unlabeled_partitions": int(partitions.shape[0]), | |
| "q_balanced_partitions": int(balanced.sum()), | |
| "incorrect_partitions": int(incorrect.sum()), | |
| "prob_all_q_balanced_lloyd_fixed": all_balanced_successes / trial_count, | |
| "prob_all_q_balanced_lloyd_fixed_ci_low": wilson_interval( | |
| all_balanced_successes, trial_count | |
| )[0], | |
| "prob_all_q_balanced_lloyd_fixed_ci_high": wilson_interval( | |
| all_balanced_successes, trial_count | |
| )[1], | |
| "mean_fraction_q_balanced_lloyd_fixed": balanced_fixed_fraction_sum | |
| / trial_count, | |
| "mean_fraction_all_lloyd_fixed": all_lloyd_fixed_fraction_sum / trial_count, | |
| "prob_exists_q_balanced_not_lloyd_fixed": exists_not_lloyd_fixed | |
| / trial_count, | |
| "prob_exists_q_balanced_not_lloyd_fixed_ci_low": lloyd_event_ci[0], | |
| "prob_exists_q_balanced_not_lloyd_fixed_ci_high": lloyd_event_ci[1], | |
| "corollary_38_applies": cor38_applies, | |
| "rho_q": rho_q if cor38_applies else None, | |
| "corollary_38_bound": cor38_bound if cor38_applies else None, | |
| "corollary_38_log_uncapped_bound": cor38_log if cor38_applies else None, | |
| "prob_any_incorrect_hartigan_fixed": any_incorrect_hartigan_successes | |
| / trial_count, | |
| "prob_any_incorrect_hartigan_fixed_ci_low": hartigan_event_ci[0], | |
| "prob_any_incorrect_hartigan_fixed_ci_high": hartigan_event_ci[1], | |
| "mean_fraction_incorrect_hartigan_fixed": incorrect_hartigan_fraction_sum | |
| / trial_count, | |
| "prob_correct_partition_hartigan_fixed": correct_hartigan_successes | |
| / trial_count, | |
| "rho_h": rho_h, | |
| "corollary_312_bound": cor312_bound, | |
| "corollary_312_log_uncapped_bound": cor312_log, | |
| } | |
| rows.append(row) | |
| if aggregate_diagnostics["hartigan_not_lloyd_violations"]: | |
| raise AssertionError("Hartigan fixed points must be a subset of Lloyd fixed points") | |
| return rows, { | |
| "true_labels": true_labels.tolist(), | |
| "high_sigma2": high_sigma2, | |
| "low_sigma2": low_sigma2, | |
| "rho_q": rho_q, | |
| "partition_count": int(partitions.shape[0]), | |
| "q_balanced_partition_count": int(balanced.sum()), | |
| "diagnostics": aggregate_diagnostics, | |
| } | |
| def theorem_34_sigma2_threshold(c: int, c_bar: int, tau2: float) -> float: | |
| """Square of Eq. (13): sigma > sqrt(2*c_bar)*tau*(c-1)/sqrt(c(c+c_bar)).""" | |
| return 2.0 * c_bar * tau2 * (c - 1.0) ** 2 / (c * (c + c_bar)) | |
| def theorem_34_rho(sigma2: float, tau2: float, c: int, c_bar: int) -> float: | |
| numerator = ( | |
| 4.0 | |
| * sigma2 | |
| * (c - 1.0) | |
| * c**2 | |
| * c_bar | |
| * (c_bar + 1.0) | |
| * (c * (sigma2 + 2.0 * tau2) - 2.0 * tau2) | |
| ) | |
| denominator = ( | |
| -c * (sigma2 + 4.0 * tau2) * c_bar | |
| + c**2 * (sigma2 + 2.0 * (sigma2 + tau2) * c_bar) | |
| + 2.0 * tau2 * c_bar | |
| ) ** 2 | |
| rho = numerator / denominator | |
| # At the (excluded) equality sigma^2=sigma_0^2, rho is exactly one. | |
| # Permit that control row while retaining a guard against real drift. | |
| if -1e-14 <= rho <= 1.0 + 1e-14: | |
| return min(1.0, max(0.0, rho)) | |
| raise ArithmeticError(f"Theorem 3.4 rho outside [0,1]: {rho}") | |
| def theorem_39_rho( | |
| sigma2: float, | |
| tau2: float, | |
| c: int, | |
| c_bar: int, | |
| purity_current: float, | |
| purity_alternative: float, | |
| ) -> float: | |
| current_term = c / (c - 1.0) * (1.0 - purity_current) ** 2 | |
| alternative_term = c_bar / (c_bar + 1.0) * (1.0 - purity_alternative) ** 2 | |
| ratio = tau2 * (current_term - alternative_term) / ( | |
| tau2 * (current_term + alternative_term) + sigma2 | |
| ) | |
| rho = 1.0 - ratio * ratio | |
| if not 0.0 <= rho < 1.0: | |
| raise ArithmeticError(f"Theorem 3.9 rho outside [0,1): {rho}") | |
| return rho | |
| def _claim5_designs() -> list[dict[str, Any]]: | |
| n = 40 | |
| true_labels = np.repeat(np.arange(2), n // 2) | |
| official = np.concatenate( | |
| ( | |
| np.zeros(5, dtype=int), | |
| np.ones(15, dtype=int), | |
| np.ones(5, dtype=int), | |
| np.zeros(15, dtype=int), | |
| ) | |
| ) | |
| single_error = true_labels.copy() | |
| single_error[0] = 1 | |
| return [ | |
| { | |
| "name": "paper_balanced_purity_025_075", | |
| "true_labels": true_labels, | |
| "current_labels": official, | |
| "point": 0, | |
| }, | |
| { | |
| "name": "single_misclassified_point", | |
| "true_labels": true_labels, | |
| "current_labels": single_error, | |
| "point": 0, | |
| }, | |
| ] | |
| def _single_point_covariance( | |
| true_labels: np.ndarray, | |
| current_labels: np.ndarray, | |
| point: int, | |
| tau2: float, | |
| sigma2: float, | |
| ) -> tuple[np.ndarray, dict[str, Any]]: | |
| current_label = int(current_labels[point]) | |
| alternative_label = 1 - current_label | |
| current_members = current_labels == current_label | |
| alternative_members = current_labels == alternative_label | |
| c = int(current_members.sum()) | |
| c_bar = int(alternative_members.sum()) | |
| if c <= 1 or c_bar <= 0: | |
| raise ValueError("Claim 5 designs require a movable point and nonempty alternative") | |
| coefficients = np.zeros((2, true_labels.size), dtype=np.float64) | |
| coefficients[:, point] = 1.0 | |
| coefficients[0, current_members] -= 1.0 / c | |
| coefficients[1, alternative_members] -= 1.0 / c_bar | |
| covariance = model_covariance(true_labels, tau2, sigma2) | |
| reduced = coefficients @ covariance @ coefficients.T | |
| true_class = true_labels[point] | |
| purity_current = float(np.mean(true_labels[current_members] == true_class)) | |
| purity_alternative = float(np.mean(true_labels[alternative_members] == true_class)) | |
| return reduced, { | |
| "c": c, | |
| "c_bar": c_bar, | |
| "purity_current": purity_current, | |
| "purity_alternative": purity_alternative, | |
| } | |
| def run_claim_5( | |
| rng: np.random.Generator, | |
| profile: Profile, | |
| *, | |
| tau2: float = 1.0, | |
| ) -> tuple[list[dict[str, Any]], dict[str, Any]]: | |
| rows: list[dict[str, Any]] = [] | |
| diagnostics = { | |
| "lloyd_near_ties": 0, | |
| "hartigan_near_ties": 0, | |
| "comparisons": 0, | |
| } | |
| design_summaries: list[dict[str, Any]] = [] | |
| for design in _claim5_designs(): | |
| # Geometry-derived cluster counts/purities do not depend on sigma2. | |
| _, geometry = _single_point_covariance( | |
| design["true_labels"], | |
| design["current_labels"], | |
| design["point"], | |
| tau2, | |
| 1.0, | |
| ) | |
| c = geometry["c"] | |
| c_bar = geometry["c_bar"] | |
| threshold_sigma2 = theorem_34_sigma2_threshold(c, c_bar, tau2) | |
| design_summaries.append( | |
| { | |
| "design": design["name"], | |
| **geometry, | |
| "sigma2_threshold": threshold_sigma2, | |
| } | |
| ) | |
| for ratio in profile.claim5_ratios: | |
| sigma2 = ratio * threshold_sigma2 | |
| reduced_covariance, checked_geometry = _single_point_covariance( | |
| design["true_labels"], | |
| design["current_labels"], | |
| design["point"], | |
| tau2, | |
| sigma2, | |
| ) | |
| if checked_geometry != geometry: | |
| raise AssertionError("cluster geometry unexpectedly changed") | |
| rho_lloyd = theorem_34_rho(sigma2, tau2, c, c_bar) | |
| rho_hartigan = theorem_39_rho( | |
| sigma2, | |
| tau2, | |
| c, | |
| c_bar, | |
| geometry["purity_current"], | |
| geometry["purity_alternative"], | |
| ) | |
| for dimension in profile.claim5_dimensions: | |
| lloyd_stays = 0 | |
| hartigan_stays = 0 | |
| done = 0 | |
| while done < profile.claim5_trials: | |
| batch = min(profile.wishart_batch_size, profile.claim5_trials - done) | |
| wishart2 = sample_wishart_normalized( | |
| rng, reduced_covariance, dimension, batch | |
| ) | |
| distance_current = wishart2[:, 0, 0] | |
| distance_alternative = wishart2[:, 1, 1] | |
| lloyd_margin = distance_alternative - distance_current | |
| hartigan_current = c / (c - 1.0) * distance_current | |
| hartigan_alternative = c_bar / (c_bar + 1.0) * distance_alternative | |
| hartigan_margin = hartigan_alternative - hartigan_current | |
| # Current assignment is wrong in both designs. Staying means | |
| # no strict improvement under the relevant criterion. | |
| lloyd_stays += int((lloyd_margin >= 0.0).sum()) | |
| hartigan_stays += int((hartigan_margin >= 0.0).sum()) | |
| diagnostics["lloyd_near_ties"] += int( | |
| _near_tie_mask( | |
| lloyd_margin, distance_current, distance_alternative | |
| ).sum() | |
| ) | |
| diagnostics["hartigan_near_ties"] += int( | |
| _near_tie_mask( | |
| hartigan_margin, hartigan_current, hartigan_alternative | |
| ).sum() | |
| ) | |
| diagnostics["comparisons"] += 2 * batch | |
| done += batch | |
| lloyd_ci = wilson_interval(lloyd_stays, profile.claim5_trials) | |
| hartigan_ci = wilson_interval(hartigan_stays, profile.claim5_trials) | |
| log_lloyd_tail = (dimension / 4.0) * math.log(rho_lloyd) | |
| lloyd_lower_bound = -math.expm1(log_lloyd_tail) | |
| hartigan_upper_bound = math.exp((dimension / 4.0) * math.log(rho_hartigan)) | |
| rows.append( | |
| { | |
| "claim_group": "claim_5", | |
| "design": design["name"], | |
| "n": int(design["true_labels"].size), | |
| "dimension": dimension, | |
| "tau2": tau2, | |
| "sigma2": sigma2, | |
| "sigma2_threshold": threshold_sigma2, | |
| "sigma2_threshold_ratio": ratio, | |
| "theorem_34_strict_condition": ratio > 1.0, | |
| "c": c, | |
| "c_bar": c_bar, | |
| "purity_current": geometry["purity_current"], | |
| "purity_alternative": geometry["purity_alternative"], | |
| "trials": profile.claim5_trials, | |
| "prob_lloyd_stays_wrong": lloyd_stays / profile.claim5_trials, | |
| "prob_lloyd_stays_wrong_ci_low": lloyd_ci[0], | |
| "prob_lloyd_stays_wrong_ci_high": lloyd_ci[1], | |
| "rho_lloyd": rho_lloyd, | |
| "theorem_34_lloyd_stay_lower_bound": lloyd_lower_bound | |
| if ratio > 1.0 | |
| else None, | |
| "prob_hartigan_stays_wrong": hartigan_stays | |
| / profile.claim5_trials, | |
| "prob_hartigan_stays_wrong_ci_low": hartigan_ci[0], | |
| "prob_hartigan_stays_wrong_ci_high": hartigan_ci[1], | |
| "rho_hartigan": rho_hartigan, | |
| "theorem_39_hartigan_stay_upper_bound": hartigan_upper_bound, | |
| } | |
| ) | |
| return rows, {"designs": design_summaries, "diagnostics": diagnostics} | |
| def _direct_fixed_tests(data: np.ndarray, partitions: np.ndarray) -> tuple[np.ndarray, np.ndarray]: | |
| lloyd_results = [] | |
| hartigan_results = [] | |
| for partition in partitions.astype(int): | |
| centers = np.stack([data[partition == k].mean(axis=0) for k in (0, 1)]) | |
| squared = ((data[:, None, :] - centers[None, :, :]) ** 2).sum(axis=2) | |
| current = squared[np.arange(data.shape[0]), partition] | |
| alternative = squared[np.arange(data.shape[0]), 1 - partition] | |
| lloyd_results.append(bool(np.all(current <= alternative))) | |
| sizes = np.bincount(partition, minlength=2) | |
| current_size = sizes[partition] | |
| alternative_size = sizes[1 - partition] | |
| movable = current_size > 1 | |
| current_weighted = np.zeros_like(current) | |
| current_weighted[movable] = ( | |
| current[movable] * current_size[movable] / (current_size[movable] - 1.0) | |
| ) | |
| alternative_weighted = ( | |
| alternative * alternative_size / (alternative_size + 1.0) | |
| ) | |
| hartigan_results.append( | |
| bool(np.all((~movable) | (current_weighted <= alternative_weighted))) | |
| ) | |
| return np.asarray(lloyd_results), np.asarray(hartigan_results) | |
| def run_self_checks(seed: int = 29589) -> dict[str, Any]: | |
| """Algebraic and numerical checks independent of Monte Carlo outcomes.""" | |
| rng = np.random.default_rng(seed + 991) | |
| n = 8 | |
| d = 64 | |
| tau2 = 1.0 | |
| sigma2 = 9.64053853187796 | |
| true_labels = np.repeat(np.arange(2), n // 2) | |
| means = rng.normal(scale=math.sqrt(tau2), size=(2, d)) | |
| data = means[true_labels] + rng.normal(scale=math.sqrt(sigma2), size=(n, d)) | |
| partitions = enumerate_bipartitions(n) | |
| gram = (data @ data.T / d)[None, :, :] | |
| distance0, distance1 = gram_partition_distances(gram, partitions) | |
| maximum_distance_error = 0.0 | |
| for pidx, partition in enumerate(partitions.astype(int)): | |
| for cluster in (0, 1): | |
| center = data[partition == cluster].mean(axis=0) | |
| direct = ((data - center) ** 2).sum(axis=1) / d | |
| formula = distance0[0, pidx] if cluster == 0 else distance1[0, pidx] | |
| maximum_distance_error = max( | |
| maximum_distance_error, float(np.max(np.abs(direct - formula))) | |
| ) | |
| if maximum_distance_error > 2e-12: | |
| raise AssertionError(f"Gram/direct centroid mismatch: {maximum_distance_error}") | |
| gram_fixed = fixed_point_tests(gram, partitions) | |
| direct_lloyd, direct_hartigan = _direct_fixed_tests(data, partitions) | |
| if not np.array_equal(np.asarray(gram_fixed["lloyd_fixed"])[0], direct_lloyd): | |
| raise AssertionError("direct and Gram Lloyd fixed-point tests disagree") | |
| if not np.array_equal(np.asarray(gram_fixed["hartigan_fixed"])[0], direct_hartigan): | |
| raise AssertionError("direct and Gram Hartigan fixed-point tests disagree") | |
| # Check the Hartigan weighted-distance identity against direct WCSS delta. | |
| partition = partitions[37].astype(int) | |
| point = next(i for i in range(n) if np.sum(partition == partition[i]) > 1) | |
| current_label = partition[point] | |
| alternative_label = 1 - current_label | |
| sizes = np.bincount(partition, minlength=2) | |
| centers = np.stack([data[partition == k].mean(axis=0) for k in (0, 1)]) | |
| before = float(((data - centers[partition]) ** 2).sum()) | |
| current_distance = float(((data[point] - centers[current_label]) ** 2).sum()) | |
| alternative_distance = float(((data[point] - centers[alternative_label]) ** 2).sum()) | |
| expected_delta = ( | |
| sizes[alternative_label] / (sizes[alternative_label] + 1.0) * alternative_distance | |
| - sizes[current_label] / (sizes[current_label] - 1.0) * current_distance | |
| ) | |
| moved = partition.copy() | |
| moved[point] = alternative_label | |
| moved_centers = np.stack([data[moved == k].mean(axis=0) for k in (0, 1)]) | |
| after = float(((data - moved_centers[moved]) ** 2).sum()) | |
| wcss_delta_error = abs((after - before) - expected_delta) | |
| if wcss_delta_error > 2e-10: | |
| raise AssertionError(f"Hartigan WCSS identity mismatch: {wcss_delta_error}") | |
| # Regression constants calculated from the paper's displayed equations. | |
| calculated_sigma2, rho_q = corollary_38_parameters(8, 1.5, 1.5) | |
| if not math.isclose(calculated_sigma2, 9.64053853187796, rel_tol=2e-14): | |
| raise AssertionError("Corollary 3.8 sigma regression failed") | |
| if not math.isclose(rho_q, 0.9928404727106878, rel_tol=2e-14): | |
| raise AssertionError("Corollary 3.8 rho regression failed") | |
| rho_h = rho_h_uniform(8, 1.0, calculated_sigma2, 0.5) | |
| if not math.isclose(rho_h, 0.9999022112550369, rel_tol=2e-14): | |
| raise AssertionError("Corollary 3.12 rho regression failed") | |
| balanced_count = int(q_balanced_mask(partitions, 1.5).sum()) | |
| if partitions.shape != (127, 8) or balanced_count != 91: | |
| raise AssertionError( | |
| f"partition enumeration regression failed: {partitions.shape}, {balanced_count}" | |
| ) | |
| lo, hi = wilson_interval(0, 100) | |
| if lo != 0.0 or not (0.036 < hi < 0.038): | |
| raise AssertionError("Wilson interval regression failed") | |
| return { | |
| "status": "passed", | |
| "direct_x_dimension": d, | |
| "partitions_checked": int(partitions.shape[0]), | |
| "maximum_distance_error": maximum_distance_error, | |
| "wcss_delta_error": wcss_delta_error, | |
| "rho_q_regression": rho_q, | |
| "rho_h_regression": rho_h, | |
| "q_balanced_partitions": balanced_count, | |
| } | |
| def write_csv(path: Path, rows: Sequence[dict[str, Any]]) -> None: | |
| if not rows: | |
| raise ValueError("cannot write an empty CSV") | |
| fieldnames: list[str] = [] | |
| seen = set() | |
| for row in rows: | |
| for key in row: | |
| if key not in seen: | |
| fieldnames.append(key) | |
| seen.add(key) | |
| with path.open("w", newline="", encoding="utf-8") as handle: | |
| writer = csv.DictWriter(handle, fieldnames=fieldnames) | |
| writer.writeheader() | |
| writer.writerows(rows) | |
| def _svg_polyline( | |
| points: Sequence[tuple[float, float]], color: str, *, dashed: bool = False | |
| ) -> str: | |
| encoded = " ".join(f"{x:.2f},{y:.2f}" for x, y in points) | |
| dash = ' stroke-dasharray="7 5"' if dashed else "" | |
| circles = "".join( | |
| f'<circle cx="{x:.2f}" cy="{y:.2f}" r="3.0" fill="{color}"/>' | |
| for x, y in points | |
| ) | |
| return ( | |
| f'<polyline points="{encoded}" fill="none" stroke="{color}" ' | |
| f'stroke-width="2.3"{dash}/>{circles}' | |
| ) | |
| def _panel_svg( | |
| *, | |
| x: float, | |
| y: float, | |
| width: float, | |
| height: float, | |
| title: str, | |
| series: Sequence[dict[str, Any]], | |
| x_label: str = "dimension d (log scale)", | |
| y_label: str = "probability", | |
| ) -> str: | |
| left, right, top, bottom = 62.0, 18.0, 42.0, 52.0 | |
| plot_x = x + left | |
| plot_y = y + top | |
| plot_w = width - left - right | |
| plot_h = height - top - bottom | |
| all_x = [point[0] for item in series for point in item["values"]] | |
| if not all_x: | |
| return "" | |
| log_min = math.log10(min(all_x)) | |
| log_max = math.log10(max(all_x)) | |
| if log_max == log_min: | |
| log_max += 1.0 | |
| def sx(value: float) -> float: | |
| return plot_x + (math.log10(value) - log_min) / (log_max - log_min) * plot_w | |
| def sy(value: float) -> float: | |
| return plot_y + (1.0 - min(1.0, max(0.0, value))) * plot_h | |
| parts = [ | |
| f'<g><rect x="{x}" y="{y}" width="{width}" height="{height}" rx="8" ' | |
| 'fill="#ffffff" stroke="#d8dee9"/>', | |
| f'<text x="{x + width / 2}" y="{y + 25}" text-anchor="middle" ' | |
| f'font-size="15" font-weight="700">{html.escape(title)}</text>', | |
| ] | |
| for tick in (0.0, 0.25, 0.5, 0.75, 1.0): | |
| py = sy(tick) | |
| parts.append( | |
| f'<line x1="{plot_x}" y1="{py}" x2="{plot_x + plot_w}" y2="{py}" ' | |
| 'stroke="#e5e9f0"/>' | |
| ) | |
| parts.append( | |
| f'<text x="{plot_x - 8}" y="{py + 4}" text-anchor="end" ' | |
| f'font-size="11">{tick:g}</text>' | |
| ) | |
| tick_powers = range(math.floor(log_min), math.ceil(log_max) + 1) | |
| for power in tick_powers: | |
| value = 10.0**power | |
| if min(all_x) <= value <= max(all_x): | |
| px = sx(value) | |
| parts.append( | |
| f'<line x1="{px}" y1="{plot_y}" x2="{px}" y2="{plot_y + plot_h}" ' | |
| 'stroke="#edf0f5"/>' | |
| ) | |
| parts.append( | |
| f'<text x="{px}" y="{plot_y + plot_h + 18}" text-anchor="middle" ' | |
| f'font-size="11">10^{power}</text>' | |
| ) | |
| for item in series: | |
| plotted = [(sx(px), sy(py)) for px, py in item["values"]] | |
| parts.append(_svg_polyline(plotted, item["color"], dashed=item.get("dashed", False))) | |
| legend_x = plot_x + 5 | |
| legend_y = plot_y + 15 | |
| for index, item in enumerate(series): | |
| ly = legend_y + index * 17 | |
| dash = ' stroke-dasharray="6 4"' if item.get("dashed", False) else "" | |
| parts.append( | |
| f'<line x1="{legend_x}" y1="{ly}" x2="{legend_x + 22}" y2="{ly}" ' | |
| f'stroke="{item["color"]}" stroke-width="2.3"{dash}/>' | |
| ) | |
| parts.append( | |
| f'<text x="{legend_x + 28}" y="{ly + 4}" font-size="10">' | |
| f'{html.escape(item["label"])}</text>' | |
| ) | |
| parts.extend( | |
| [ | |
| f'<text x="{plot_x + plot_w / 2}" y="{y + height - 12}" ' | |
| f'text-anchor="middle" font-size="12">{html.escape(x_label)}</text>', | |
| f'<text x="{x + 15}" y="{plot_y + plot_h / 2}" text-anchor="middle" ' | |
| f'font-size="12" transform="rotate(-90 {x + 15} {plot_y + plot_h / 2})">' | |
| f'{html.escape(y_label)}</text>', | |
| "</g>", | |
| ] | |
| ) | |
| return "".join(parts) | |
| def build_claims_1_4_svg(rows: Sequence[dict[str, Any]]) -> str: | |
| high = sorted( | |
| (row for row in rows if row["regime"] == "high_noise"), | |
| key=lambda row: row["dimension"], | |
| ) | |
| low = sorted( | |
| (row for row in rows if row["regime"] == "low_noise_control"), | |
| key=lambda row: row["dimension"], | |
| ) | |
| panel1 = [ | |
| { | |
| "label": "high noise: empirical all fixed", | |
| "color": "#2563eb", | |
| "values": [(row["dimension"], row["prob_all_q_balanced_lloyd_fixed"]) for row in high], | |
| }, | |
| { | |
| "label": "Cor. 3.8 guaranteed lower bound", | |
| "color": "#1d4ed8", | |
| "dashed": True, | |
| "values": [(row["dimension"], 1.0 - row["corollary_38_bound"]) for row in high], | |
| }, | |
| { | |
| "label": "low-noise control", | |
| "color": "#94a3b8", | |
| "values": [(row["dimension"], row["prob_all_q_balanced_lloyd_fixed"]) for row in low], | |
| }, | |
| ] | |
| panel2 = [ | |
| { | |
| "label": "high noise: empirical any incorrect", | |
| "color": "#ea580c", | |
| "values": [(row["dimension"], row["prob_any_incorrect_hartigan_fixed"]) for row in high], | |
| }, | |
| { | |
| "label": "Cor. 3.12 upper bound", | |
| "color": "#c2410c", | |
| "dashed": True, | |
| "values": [(row["dimension"], row["corollary_312_bound"]) for row in high], | |
| }, | |
| { | |
| "label": "low-noise control", | |
| "color": "#94a3b8", | |
| "values": [(row["dimension"], row["prob_any_incorrect_hartigan_fixed"]) for row in low], | |
| }, | |
| ] | |
| body = _panel_svg( | |
| x=18, y=55, width=555, height=350, title="Claims 1 & 3 — Lloyd fixed-point proliferation", series=panel1 | |
| ) + _panel_svg( | |
| x=587, y=55, width=555, height=350, title="Claims 2 & 4 — incorrect Hartigan fixed points", series=panel2 | |
| ) | |
| return ( | |
| '<svg xmlns="http://www.w3.org/2000/svg" width="1160" height="425" ' | |
| 'viewBox="0 0 1160 425" role="img" aria-label="Claims 1 to 4 numerical audit">' | |
| '<rect width="1160" height="425" fill="#f8fafc"/>' | |
| '<text x="580" y="30" text-anchor="middle" font-family="system-ui,sans-serif" ' | |
| 'font-size="20" font-weight="700">Exact exhaustive Wishart/Gram audit</text>' | |
| f'<g font-family="system-ui,sans-serif" fill="#172033">{body}</g></svg>' | |
| ) | |
| def build_claim_5_svg(rows: Sequence[dict[str, Any]]) -> str: | |
| design = "paper_balanced_purity_025_075" | |
| selected_ratios = sorted( | |
| set(row["sigma2_threshold_ratio"] for row in rows if row["design"] == design) | |
| ) | |
| if len(selected_ratios) > 4: | |
| selected_ratios = [selected_ratios[0], 1.0, selected_ratios[-2], selected_ratios[-1]] | |
| palette = ["#64748b", "#7c3aed", "#2563eb", "#dc2626"] | |
| lloyd_series = [] | |
| hartigan_series = [] | |
| for color, ratio in zip(palette, selected_ratios): | |
| subset = sorted( | |
| ( | |
| row | |
| for row in rows | |
| if row["design"] == design | |
| and row["sigma2_threshold_ratio"] == ratio | |
| ), | |
| key=lambda row: row["dimension"], | |
| ) | |
| lloyd_series.append( | |
| { | |
| "label": f"sigma²/sigma0²={ratio:g}", | |
| "color": color, | |
| "values": [(row["dimension"], row["prob_lloyd_stays_wrong"]) for row in subset], | |
| } | |
| ) | |
| hartigan_series.append( | |
| { | |
| "label": f"sigma²/sigma0²={ratio:g}", | |
| "color": color, | |
| "values": [(row["dimension"], row["prob_hartigan_stays_wrong"]) for row in subset], | |
| } | |
| ) | |
| body = _panel_svg( | |
| x=18, | |
| y=55, | |
| width=555, | |
| height=350, | |
| title="Claim 5 — Lloyd keeps the wrong assignment", | |
| series=lloyd_series, | |
| ) + _panel_svg( | |
| x=587, | |
| y=55, | |
| width=555, | |
| height=350, | |
| title="Control — Hartigan keeps the wrong assignment", | |
| series=hartigan_series, | |
| ) | |
| return ( | |
| '<svg xmlns="http://www.w3.org/2000/svg" width="1160" height="425" ' | |
| 'viewBox="0 0 1160 425" role="img" aria-label="Claim 5 numerical audit">' | |
| '<rect width="1160" height="425" fill="#f8fafc"/>' | |
| '<text x="580" y="30" text-anchor="middle" font-family="system-ui,sans-serif" ' | |
| 'font-size="20" font-weight="700">Single-sample persistence across dimension</text>' | |
| f'<g font-family="system-ui,sans-serif" fill="#172033">{body}</g></svg>' | |
| ) | |
| def _html_table(rows: Sequence[dict[str, Any]], columns: Sequence[str], limit: int | None = None) -> str: | |
| shown = rows if limit is None else rows[:limit] | |
| header = "".join(f"<th>{html.escape(column)}</th>" for column in columns) | |
| body = [] | |
| for row in shown: | |
| cells = [] | |
| for column in columns: | |
| value = row.get(column) | |
| if isinstance(value, float): | |
| rendered = f"{value:.6g}" | |
| elif value is None: | |
| rendered = "—" | |
| else: | |
| rendered = str(value) | |
| cells.append(f"<td>{html.escape(rendered)}</td>") | |
| body.append("<tr>" + "".join(cells) + "</tr>") | |
| return f"<table><thead><tr>{header}</tr></thead><tbody>{''.join(body)}</tbody></table>" | |
| def build_html_report( | |
| summary: dict[str, Any], | |
| claims_1_4_rows: Sequence[dict[str, Any]], | |
| claim5_rows: Sequence[dict[str, Any]], | |
| claims_svg: str, | |
| claim5_svg: str, | |
| ) -> str: | |
| high = [row for row in claims_1_4_rows if row["regime"] == "high_noise"] | |
| claim5_paper = [ | |
| row for row in claim5_rows if row["design"] == "paper_balanced_purity_025_075" | |
| ] | |
| columns_14 = ( | |
| "regime", | |
| "dimension", | |
| "trials", | |
| "prob_all_q_balanced_lloyd_fixed", | |
| "corollary_38_bound", | |
| "prob_any_incorrect_hartigan_fixed", | |
| "corollary_312_bound", | |
| ) | |
| columns_5 = ( | |
| "design", | |
| "sigma2_threshold_ratio", | |
| "dimension", | |
| "trials", | |
| "prob_lloyd_stays_wrong", | |
| "theorem_34_lloyd_stay_lower_bound", | |
| "prob_hartigan_stays_wrong", | |
| "theorem_39_hartigan_stay_upper_bound", | |
| ) | |
| diagnostics = summary["claims_1_to_4"]["diagnostics"] | |
| return f"""<!doctype html> | |
| <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Claims 1–5 exact theory audit</title> | |
| <style> | |
| body{{font:15px/1.5 system-ui,sans-serif;color:#172033;background:#f1f5f9;margin:0}} | |
| main{{max-width:1200px;margin:auto;padding:28px}} section{{background:white;border-radius:12px;padding:22px;margin:18px 0;box-shadow:0 1px 3px #0001}} | |
| h1,h2{{line-height:1.2}} code{{background:#eef2f7;padding:2px 5px;border-radius:4px}} table{{border-collapse:collapse;width:100%;font-size:12px}} | |
| th,td{{border:1px solid #d8dee9;padding:6px;text-align:right}} th:first-child,td:first-child{{text-align:left}} th{{background:#eef2f7;position:sticky;top:0}} | |
| .scroll{{overflow:auto;max-height:480px}} .note{{border-left:4px solid #2563eb;padding:8px 12px;background:#eff6ff}} a{{color:#1d4ed8}} | |
| svg{{max-width:100%;height:auto}} | |
| </style></head><body><main> | |
| <h1>Exact Wishart/Gram numerical audit: Claims 1–5</h1> | |
| <p>Profile <code>{html.escape(summary['profile'])}</code>, seed <code>{summary['seed']}</code>, NumPy {html.escape(summary['environment']['numpy'])}, runtime {summary['runtime_seconds']:.2f}s.</p> | |
| <p><a href="{PAPER_URL}">Paper</a> · <a href="{CODE_URL}">author code (audited commit)</a> · <a href="{ZENODO_URL}">author archive</a></p> | |
| <section><h2>Method and scope</h2> | |
| <p>For fixed labels, each feature column is Gaussian with covariance <code>Σ=τ²ZZᵀ+σ²I</code>, so <code>G=XXᵀ ~ Wishart(d,Σ)</code>. Since both fixed-point criteria use only distances to empirical centroids, exhaustive Gram-space tests are distributionally exact, not a proxy. Complement-symmetric partitions are enumerated once.</p> | |
| <p class="note">A numerical audit is not a replacement for a proof. Corollary bounds are probabilities over independent datasets. The exact event is evaluated once per dataset; candidate partitions are not treated as independent trials.</p> | |
| <p>Built-in self-check: <strong>{html.escape(summary['self_check']['status'])}</strong>; {summary['self_check']['partitions_checked']} direct-X partitions checked, maximum Gram/direct distance error {summary['self_check']['maximum_distance_error']:.3g}.</p> | |
| </section> | |
| <section><h2>Claims 1–4</h2>{claims_svg} | |
| <p>High-noise parameters: n=8, τ²=1, q=1.5, β=1.5, σ²={summary['claims_1_to_4']['high_sigma2']:.8g}. The q-balanced filter uses strict Definition 2.5 inequalities and cluster sizes >2.</p> | |
| <p>Near-tie diagnostics: {diagnostics['lloyd_near_ties']} Lloyd and {diagnostics['hartigan_near_ties']} Hartigan comparisons within {TIE_ULPS:g} scaled ulps; {diagnostics['negative_distance_count']} negative computed distances. Hartigan-fixed/not-Lloyd-fixed violations: {diagnostics['hartigan_not_lloyd_violations']}.</p> | |
| <div class="scroll">{_html_table(claims_1_4_rows, columns_14)}</div></section> | |
| <section><h2>Claim 5</h2>{claim5_svg} | |
| <p>The paper-matched design uses n=40, τ²=1, c=c̄=20 and purities 0.25/0.75, giving σ₀²=18.05. A second design contains exactly one misclassified point. The Theorem 3.4 lower bound is shown only when the strict condition σ²>σ₀² holds.</p> | |
| <div class="scroll">{_html_table(claim5_paper, columns_5)}</div></section> | |
| <section><h2>Artifacts</h2><p>Machine-readable values are in <code>claims_1_4_results.csv</code>, <code>claim_5_results.csv</code>, and <code>summary.json</code>. Standalone figures are <code>claims_1_4.svg</code> and <code>claim_5.svg</code>.</p></section> | |
| </main></body></html>""" | |
| def _summary_findings(rows_14: Sequence[dict[str, Any]], rows_5: Sequence[dict[str, Any]]) -> dict[str, Any]: | |
| high = sorted( | |
| (row for row in rows_14 if row["regime"] == "high_noise"), | |
| key=lambda row: row["dimension"], | |
| ) | |
| high_last = high[-1] | |
| claim5_above = [ | |
| row | |
| for row in rows_5 | |
| if row["design"] == "paper_balanced_purity_025_075" | |
| and row["sigma2_threshold_ratio"] > 1.0 | |
| ] | |
| claim5_last = max(claim5_above, key=lambda row: row["dimension"]) | |
| return { | |
| "claim_1_highest_dimension_prob_all_balanced_fixed": high_last[ | |
| "prob_all_q_balanced_lloyd_fixed" | |
| ], | |
| "claim_2_highest_dimension_prob_any_incorrect_hartigan_fixed": high_last[ | |
| "prob_any_incorrect_hartigan_fixed" | |
| ], | |
| "claim_3_highest_dimension_empirical_bad_event": high_last[ | |
| "prob_exists_q_balanced_not_lloyd_fixed" | |
| ], | |
| "claim_3_highest_dimension_bound": high_last["corollary_38_bound"], | |
| "claim_4_highest_dimension_empirical_bad_event": high_last[ | |
| "prob_any_incorrect_hartigan_fixed" | |
| ], | |
| "claim_4_highest_dimension_bound": high_last["corollary_312_bound"], | |
| "claim_5_high_dimension_lloyd_stays_wrong": claim5_last[ | |
| "prob_lloyd_stays_wrong" | |
| ], | |
| "claim_5_high_dimension_hartigan_stays_wrong": claim5_last[ | |
| "prob_hartigan_stays_wrong" | |
| ], | |
| } | |
| def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--profile", choices=sorted(PROFILES), default="smoke") | |
| parser.add_argument("--output-dir", type=Path, default=Path("outputs/theory")) | |
| parser.add_argument("--seed", type=int, default=29589) | |
| parser.add_argument( | |
| "--self-check-only", | |
| action="store_true", | |
| help="run algebra/regression checks without creating result files", | |
| ) | |
| return parser.parse_args(argv) | |
| def main(argv: Sequence[str] | None = None) -> int: | |
| args = parse_args(argv) | |
| started = time.perf_counter() | |
| self_check = run_self_checks(args.seed) | |
| if args.self_check_only: | |
| print(json.dumps(_native(self_check), indent=2, allow_nan=False)) | |
| return 0 | |
| profile = PROFILES[args.profile] | |
| # Independent deterministic streams keep one claim's profile changes from | |
| # silently changing another claim's samples. | |
| seed_sequence = np.random.SeedSequence(args.seed) | |
| stream_14, stream_5 = seed_sequence.spawn(2) | |
| rows_14, metadata_14 = run_claims_1_to_4( | |
| np.random.default_rng(stream_14), profile | |
| ) | |
| rows_5, metadata_5 = run_claim_5(np.random.default_rng(stream_5), profile) | |
| runtime = time.perf_counter() - started | |
| args.output_dir.mkdir(parents=True, exist_ok=True) | |
| claims_svg = build_claims_1_4_svg(rows_14) | |
| claim5_svg = build_claim_5_svg(rows_5) | |
| summary = { | |
| "schema_version": 1, | |
| "paper": PAPER_URL, | |
| "author_code": CODE_URL, | |
| "author_archive": ZENODO_URL, | |
| "profile": profile.name, | |
| "profile_parameters": asdict(profile), | |
| "seed": args.seed, | |
| "runtime_seconds": runtime, | |
| "environment": { | |
| "python": platform.python_version(), | |
| "numpy": np.__version__, | |
| "platform": platform.platform(), | |
| "pid": os.getpid(), | |
| }, | |
| "self_check": self_check, | |
| "claims_1_to_4": metadata_14, | |
| "claim_5": metadata_5, | |
| "findings": _summary_findings(rows_14, rows_5), | |
| "definitions": { | |
| "lloyd_fixed": "every point has current squared distance <= alternative squared distance", | |
| "hartigan_fixed": "every allowed source-nonsingleton move has weighted current distance <= weighted alternative distance", | |
| "corollary_38_event": "exists q-approximately balanced partition that is not Lloyd-fixed", | |
| "corollary_312_event": "exists nonempty incorrect partition that is Hartigan-fixed", | |
| "ties": "strict improvements move; exact ties stay and near-ties are diagnosed without altering classification", | |
| }, | |
| } | |
| report = build_html_report(summary, rows_14, rows_5, claims_svg, claim5_svg) | |
| write_csv(args.output_dir / "claims_1_4_results.csv", rows_14) | |
| write_csv(args.output_dir / "claim_5_results.csv", rows_5) | |
| (args.output_dir / "claims_1_4.svg").write_text(claims_svg, encoding="utf-8") | |
| (args.output_dir / "claim_5.svg").write_text(claim5_svg, encoding="utf-8") | |
| (args.output_dir / "theory_reproduction.html").write_text(report, encoding="utf-8") | |
| (args.output_dir / "summary.json").write_text( | |
| json.dumps(_native(summary), indent=2, sort_keys=True, allow_nan=False) + "\n", | |
| encoding="utf-8", | |
| ) | |
| print(f"Self-check: {self_check['status']}") | |
| print(f"Profile: {profile.name}; seed: {args.seed}; runtime: {runtime:.2f}s") | |
| print(f"Results: {args.output_dir.resolve()}") | |
| print(json.dumps(_native(summary["findings"]), indent=2, allow_nan=False)) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |
Xet Storage Details
- Size:
- 54.7 kB
- Xet hash:
- 8ac025e259f5cd8dc74191d018e86b9ca73b02fb5800b4a0346058979472a215
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.