File size: 5,557 Bytes
50d7531
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env python3
"""CPU-only correlated non-Gaussian path audit for Claims 1--3.

The earlier audit used independent product mixtures.  This producer uses
non-separable two-dimensional Gaussian mixtures whose component covariances
have correlations +/-0.7.  Endpoint densities are closed-form mixtures and
the path energy is a closed-form second-moment integral; only deterministic
fixed-grid quadrature is used for KL and chi-squared integrals.
"""

from __future__ import annotations

import hashlib
import json
import math
from pathlib import Path

import numpy as np


PAPER_SHA256 = "fe979c798cd48a5af02f6c647ecc29b2d6a937841adfa8ceedb492b1c2d81583"
FAMILIES = (
    (
        "rho-positive",
        np.array([[-2.0, 0.0], [0.0, 2.0], [2.0, -1.0]]),
        np.array([0.25, 0.50, 0.25]),
        np.array([[[0.45, 0.315], [0.315, 0.45]], [[0.35, -0.245], [-0.245, 0.35]], [[0.55, 0.385], [0.385, 0.55]]]),
    ),
    (
        "rho-negative",
        np.array([[-3.0, 1.0], [0.0, -2.0], [2.0, 2.0]]),
        np.array([0.20, 0.30, 0.50]),
        np.array([[[0.50, -0.35], [-0.35, 0.50]], [[0.40, 0.28], [0.28, 0.40]], [[0.30, -0.21], [-0.21, 0.30]]]),
    ),
    (
        "mixed-correlation",
        np.array([[-2.0, -2.0], [1.0, 0.0], [2.0, 3.0]]),
        np.array([0.30, 0.40, 0.30]),
        np.array([[[0.35, 0.245], [0.245, 0.35]], [[0.60, -0.42], [-0.42, 0.60]], [[0.45, 0.315], [0.315, 0.45]]]),
    ),
)


def paper_sha() -> str:
    return hashlib.sha256((Path(__file__).resolve().parents[1] / "source" / "paper_v1.pdf").read_bytes()).hexdigest()


def density(points: np.ndarray, means: np.ndarray, weights: np.ndarray, covariances: np.ndarray, drift: float) -> np.ndarray:
    result = np.zeros(len(points), dtype=float)
    scale = math.exp(drift)
    variance_add = (math.exp(2.0 * drift) - 1.0) / (2.0 * drift) if abs(drift) > 1e-12 else 1.0
    for mean, weight, covariance in zip(means, weights, covariances):
        transformed_mean = scale * mean
        transformed_covariance = scale * scale * covariance + variance_add * np.eye(2)
        inverse = np.linalg.inv(transformed_covariance)
        determinant = np.linalg.det(transformed_covariance)
        delta = points - transformed_mean
        result += weight * np.exp(-0.5 * np.einsum("ni,ij,nj->n", delta, inverse, delta)) / (2.0 * np.pi * np.sqrt(determinant))
    return result


def audit() -> dict[str, object]:
    if paper_sha() != PAPER_SHA256:
        raise RuntimeError("paper source hash changed")
    axis = np.linspace(-12.0, 12.0, 801)
    grid_x, grid_y = np.meshgrid(axis, axis, indexing="ij")
    points = np.stack([grid_x.ravel(), grid_y.ravel()], axis=1)
    spacing = float(axis[1] - axis[0])
    rows: list[dict[str, float | str]] = []
    for family, means, weights, covariances in FAMILIES:
        reference = np.zeros(len(points), dtype=float)
        for mean, weight, covariance in zip(means, weights, covariances):
            inverse = np.linalg.inv(covariance + np.eye(2))
            determinant = np.linalg.det(covariance + np.eye(2))
            delta = points - mean
            reference += weight * np.exp(-0.5 * np.einsum("ni,ij,nj->n", delta, inverse, delta)) / (2.0 * np.pi * np.sqrt(determinant))
        for drift in (-0.15, -0.10, -0.05, 0.05, 0.10, 0.15):
            perturbed = density(points, means, weights, covariances, drift)
            times = np.linspace(0.0, 1.0, 2001)
            scale = np.exp(drift * times)
            variance_add = (np.exp(2.0 * drift * times) - 1.0) / (2.0 * drift)
            second_moment = []
            for scalar, add in zip(scale, variance_add):
                second_moment.append(sum(weight * (np.trace(scalar * scalar * covariance + add * np.eye(2)) + np.dot(scalar * mean, scalar * mean)) for mean, weight, covariance in zip(means, weights, covariances)))
            energy = float(np.trapezoid(drift * drift * np.asarray(second_moment), times))
            safe_reference = np.maximum(reference, np.finfo(float).tiny)
            safe_perturbed = np.maximum(perturbed, np.finfo(float).tiny)
            mass_reference = float(np.sum(reference) * spacing * spacing)
            mass_perturbed = float(np.sum(perturbed) * spacing * spacing)
            kl = float(np.sum(perturbed * np.log(safe_perturbed / safe_reference)) * spacing * spacing)
            chi2 = float(np.sum(perturbed * perturbed / safe_reference) * spacing * spacing - 1.0)
            rows.append({"family": family, "drift": drift, "endpoint_kl": kl, "endpoint_chi2": chi2, "path_energy": energy, "kl_over_half_energy": kl / (0.5 * energy), "chi2_over_energy": chi2 / energy, "mass_error": max(abs(mass_reference - 1.0), abs(mass_perturbed - 1.0))})
    assert len(rows) == 18
    assert max(row["kl_over_half_energy"] for row in rows) < 1.0
    assert min(row["endpoint_chi2"] for row in rows) > 0.0
    return {
        "schema": "correlated-nonseparable-mixture-path-v1",
        "cpu_only": True,
        "paper_sha256": PAPER_SHA256,
        "cells": len(rows),
        "families": [family[0] for family in FAMILIES],
        "drifts": [-0.15, -0.10, -0.05, 0.05, 0.10, 0.15],
        "max_kl_over_half_energy": max(row["kl_over_half_energy"] for row in rows),
        "min_chi2_over_energy": min(row["chi2_over_energy"] for row in rows),
        "max_chi2_over_energy": max(row["chi2_over_energy"] for row in rows),
        "max_mass_error": max(row["mass_error"] for row in rows),
        "rows": rows,
    }


if __name__ == "__main__":
    print(json.dumps(audit(), indent=2, sort_keys=True))