"""Reconstruction of the two ingredients Theorem 2.10's bound is built from. Theorem 2.10 is a *sufficiency* statement quantified over every model and every ``(eps, delta)``, so measuring one algorithm's sample requirement can corroborate it but never establish it. What can be established is the derivation, and it factors into two independently measurable pieces: (S) **Sensitivity.** ``comp(Sigma, A)`` is an explicit quadratic polynomial in the entries of ``Sigma``, so its exact gradient is available in closed form. Write ``L(Sigma, A)`` for the ``l1`` norm of that gradient, i.e. the largest first-order change in ``comp`` caused by an entrywise perturbation of ``Sigma`` of size 1. To get ``|comp(Sigmahat) - comp(Sigma)| <= eps`` it suffices to estimate every entry of ``Sigma`` to accuracy ``t = eps / L``. (C) **Concentration.** For a centered Gaussian, ``Sigmahat_ab`` is an average of ``N`` iid sub-exponential variables with variance ``(Sigma_aa Sigma_bb + Sigma_ab^2)/N <= 2V^2/N``. A union bound over the ``n(n+1)/2`` entries gives ``max_ab |Sigmahat_ab - Sigma_ab| <= t`` with probability ``1 - delta`` once ``N >= C (V/t)^2 log(n^2/delta)``. Composing them, ``N >= C V^2 L^2 / eps^2 * log(n^2/delta)``. This has the theorem's shape exactly when ``L <= K n^2 (1+a+b)^2 V``, since then V^2 L^2 = K^2 n^4 (1+a+b)^4 V^4. So the theorem's four exponents are *predictions* about two measurable quantities, and neither measurement uses the theorem's formula: exponent of L in n = 2 (=> exponent 4 of N* in n) exponent of L in (1+a+b) = 2 (=> exponent 4 in (1+a+b)) exponent of L in V = 1 (=> exponent 4 in V, with the V^2 above) exponent of t in 1/sqrt(N) = 1 (=> exponent 2 in 1/eps) exponent of t in sqrt(log(n^2/delta)) = 1 (=> exponent 1 in log(n/delta)) ``L`` is deterministic -- no Monte-Carlo noise at all -- so its exponents come out sharply, which is exactly where the end-to-end ``N*`` measurement is weakest. """ from __future__ import annotations import numpy as np from .linear import disjoint_path_pair_sums, gamma_from_A def comp_gradient(Sigma: np.ndarray, A: np.ndarray) -> np.ndarray: """Exact gradient of ``comp`` w.r.t. the free parameters of a symmetric Sigma. Returns an upper-triangular array ``g`` with ``g[a, b] = d comp / d theta_ab`` where ``theta_ab`` is the shared value of ``Sigma_ab`` and ``Sigma_ba``. ``comp = sum_{i float: """``L = ||grad comp||_1`` -- worst first-order change per unit sup-perturbation.""" return float(np.abs(comp_gradient(Sigma, A)).sum()) def curvature(Sigma: np.ndarray, A: np.ndarray) -> float: """``Q``: the second-order coefficient, so that for every perturbation D ``|comp(Sigma+D) - comp(Sigma)| <= L ||D||_inf + Q ||D||_inf^2``. ``comp`` is exactly quadratic in Sigma, so this two-term bound is exact -- there is no higher-order remainder to neglect. ``Q`` is the ``l1`` norm of the quadratic form ``sum_{i np.ndarray: """Upper quantiles of ``max_{a<=b} |Sigmahat_ab - Sigma_ab|`` over ``reps`` draws. ``probs`` are *exceedance* probabilities (i.e. ``delta``), so the returned value at ``delta`` is the smallest ``t`` with an empirical ``P(max dev > t) <= delta``. Computed in blocks to bound memory. """ L = np.linalg.cholesky(Sigma) iu = np.triu_indices(n) devs = np.empty(reps) done = 0 while done < reps: r = min(block, reps - done) z = rng.standard_normal((r, N, n)) x = z @ L.T S = np.einsum("rna,rnb->rab", x, x) / N devs[done:done + r] = np.abs(S - Sigma)[:, iu[0], iu[1]].max(axis=1) done += r return np.quantile(devs, 1.0 - np.asarray(probs, dtype=float))