File size: 12,458 Bytes
2a87ede
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
"""Classical-shadows-based estimator for `magic_entropy` (see

`magic_entropy.py` in this same subpackage) -- estimates the same

single-qubit non-stabilizerness quantity from randomized measurement

snapshots instead of requiring the exact density matrix.



Originated from a Colab proposal for a `dense_evolution/circuits/shadows.py`

module (following Huang, Kueng, Preskill 2020, "Predicting Many Properties

of a Quantum System from Very Few Measurements") with a real bug in its

purity estimator (a missing transpose in a U-statistic einsum contraction,

silent whenever every snapshot happened to be real-valued). Fixed, then

extended -- using the same multi-copy U-statistic trick the paper says

"readily generalizes to higher order polynomials" -- to estimate

`magic_entropy`'s reduced convolution matrix from shadow snapshots instead

of the exact rho. Matured across Dense-Evolution-Discovery Experiment 31

(three real gaps found and closed there before promotion: the purity

estimator bug, missing median-of-means robustness, and no

sample-complexity guidance):

https://tatopenn-cell.github.io/Dense-Evolution-Discovery/quantum_shadows_magic_entropy/



API SHAPE differs from every other function in this subpackage: sampling

(`sample_classical_shadow`) and estimation (`magic_entropy_from_shadows`)

are separate steps, matching how classical shadows work in general -- the

snapshot data can come from this simulator (`sample_classical_shadow` uses

oracle access to `rho`'s exact Born-rule probabilities, something only a

simulator has) or, in principle, from real hardware measurement outcomes

reconstructed the same way (`rho_hat = 3 U^dagger |b><b| U - I` per

snapshot, from a recorded basis+outcome).



Restricted to SINGLE-QUBIT density matrices, matching `magic_entropy`'s

own scope.

"""
import jax
import jax.numpy as jnp
import numpy as np

from .magic_entropy import _KEY_UNITARY_K3

__all__ = [
    "sample_classical_shadow", "magic_entropy_from_shadows",
    "approx_shadow_std", "fit_shadow_sample_complexity",
]

# Single-qubit random-Pauli classical shadow protocol (Huang, Kueng,
# Preskill 2020, eq. 2-3): the three diagonalizing unitaries for X, Y, Z.
_H = jnp.array([[1.0, 1.0], [1.0, -1.0]], dtype=jnp.complex128) / jnp.sqrt(2.0)
_SDAG = jnp.array([[1.0, 0.0], [0.0, -1j]], dtype=jnp.complex128)
_BASIS_U = jnp.stack([_H, _H @ _SDAG, jnp.eye(2, dtype=jnp.complex128)])


def sample_classical_shadow(rho: jnp.ndarray, n_snapshots: int, seed: int = 0) -> jnp.ndarray:
    """Simulates the real single-qubit random-Pauli classical-shadow

    measurement protocol against a known `rho` (2x2, pure or mixed): for

    each of `n_snapshots` independent draws, picks a random Pauli basis

    uniformly, samples a computational-basis outcome from the true

    Born-rule probability under that basis (this simulator has oracle

    access to `rho`, unlike real hardware), then reconstructs the

    classical snapshot `rho_hat = 3 U^dagger |b><b| U - I`.



    Returns an `(n_snapshots, 2, 2)` complex128 array -- feed this

    directly into `magic_entropy_from_shadows`.



    Each individual `rho_hat` is NOT a valid density matrix on its own

    (can have negative eigenvalues) -- only the average over many

    snapshots converges to the true `rho`. Verified directly in

    Dense-Evolution-Discovery Experiment 31: the empirical mean over

    200,000 snapshots matched the true state to within 0.004.

    """
    rho = jnp.asarray(rho, dtype=jnp.complex128)
    key = jax.random.PRNGKey(seed)
    key_basis, key_bit = jax.random.split(key)
    bases = jax.random.randint(key_basis, (n_snapshots,), 0, 3)

    def prob0(basis_idx):
        u = _BASIS_U[basis_idx]
        rotated = u @ rho @ jnp.conj(u).T
        return jnp.clip(jnp.real(rotated[0, 0]), 0.0, 1.0)

    probs0 = jax.vmap(prob0)(bases)
    uniforms = jax.random.uniform(key_bit, (n_snapshots,))
    bits = (uniforms > probs0).astype(jnp.int32)

    def snapshot_matrix(basis_idx, bit):
        u = _BASIS_U[basis_idx]
        b_ket = jnp.array([1.0, 0.0], dtype=jnp.complex128) * (1 - bit) + \
            jnp.array([0.0, 1.0], dtype=jnp.complex128) * bit
        proj = jnp.outer(b_ket, jnp.conj(b_ket))
        return 3.0 * (jnp.conj(u).T @ proj @ u) - jnp.eye(2, dtype=jnp.complex128)

    return jax.vmap(snapshot_matrix)(bases, bits)


def _o_operators():
    """O_ab = V^dagger (|b><a| (x) I_4) V such that R_ab = Tr[O_ab . rho^{(x)3}]

    -- the same construction `magic_entropy.py`'s `_self_convolve_3_core`

    applies to exact `rho`, here turned into fixed operators so each entry

    is a LINEAR functional of `rho^{(x)3}` (the shape a shadow-snapshot

    U-statistic estimator needs). Reuses this package's own

    `_KEY_UNITARY_K3` directly rather than duplicating it (unlike

    Dense-Evolution-Discovery's per-script self-containment convention,

    this library's internal modules import from each other freely). The

    `|b><a|` (not `|a><b|`) projector is deliberate: a direct index-expansion

    check (not just the cyclic-trace derivation, which looks right on paper

    but hides the swap) showed `Tr[(|a><b| (x) I) M] = R_ba`, not `R_ab` --

    caught in Discovery Experiment 31 by a unit test checking matrix

    entries directly, not just the downstream entropy."""
    i4 = jnp.eye(4, dtype=jnp.complex128)
    v = _KEY_UNITARY_K3
    ops = {}
    for a in range(2):
        for b in range(2):
            proj_ba = jnp.zeros((2, 2), dtype=jnp.complex128).at[b, a].set(1.0)
            ops[(a, b)] = jnp.conj(v).T @ jnp.kron(proj_ba, i4) @ v
    return ops


_O_AB = _o_operators()


def _median_of_means(values: np.ndarray, n_groups: int) -> float:
    """Split real-valued `values` into `n_groups` contiguous batches,

    average each batch, then return the median of those batch means --

    Huang et al.'s standard robustification for shadow-estimator

    U-statistics. Unlike a single overall mean, the median tolerates up

    to `n_groups // 2` entirely corrupted/outlier batches (e.g. a

    systematic calibration fault affecting one contiguous stretch of a

    measurement run) without being dragged toward them -- verified

    directly in Dense-Evolution-Discovery Experiment 31: stays within 0.5

    of the true value at 40% of samples corrupted, while a plain mean is

    dragged from 1.0 to -19.4 under the same corruption."""
    values = np.asarray(values, dtype=float)
    n = len(values)
    n_groups = max(1, min(n_groups, n))
    group_size = n // n_groups
    trimmed = values[: group_size * n_groups]
    group_means = trimmed.reshape(n_groups, group_size).mean(axis=1)
    return float(np.median(group_means))


def magic_entropy_from_shadows(shadow_snapshots: jnp.ndarray, n_groups: int = 20) -> float:
    """Estimates `magic_entropy(rho)` from classical shadow snapshots of

    `rho` (from `sample_classical_shadow`, or real hardware measurement

    data reconstructed the same way) instead of the exact density matrix.



    Groups the snapshots into disjoint triples, estimates each entry of

    the 3-copy self-convolution's reduced matrix `R` via median-of-means

    over `Tr[O_ab . (rho_hat_i (x) rho_hat_j (x) rho_hat_k)]` (unbiased,

    since each triple's three snapshots are independent unbiased

    estimators of `rho`; real and imaginary parts of each entry are

    median-of-means'd separately, the standard practical choice since a

    complex median has no single definition), then computes the von

    Neumann entropy of the (Hermitized, eigenvalue-clipped, trace-

    renormalized) ESTIMATED `R` classically -- entropy itself is never

    shadow-estimated directly, matching how Huang et al. handle their own

    Renyi-2 entanglement entropy example.



    Not `jax.jit`-compatible (unlike every other function in this

    subpackage): median-of-means uses `numpy.median`, which has no

    equivalent JAX primitive at this scale.



    See `approx_shadow_std`/`fit_shadow_sample_complexity` for how many

    snapshots this needs for a given error tolerance.

    """
    n = shadow_snapshots.shape[0]
    n_triples = n // 3
    if n_triples < 1:
        raise ValueError(f"need at least 3 shadow snapshots to form one triple, got {n}")
    triples = shadow_snapshots[: n_triples * 3].reshape(n_triples, 3, 2, 2)

    def triple_kron(t):
        return jnp.kron(jnp.kron(t[0], t[1]), t[2])

    rho3_batch = jax.vmap(triple_kron)(triples)

    r_hat = jnp.zeros((2, 2), dtype=jnp.complex128)
    for (a, b), o_ab in _O_AB.items():
        vals = np.array(jnp.einsum("ij,tji->t", o_ab, rho3_batch))
        real_part = _median_of_means(vals.real, n_groups)
        imag_part = _median_of_means(vals.imag, n_groups)
        r_hat = r_hat.at[a, b].set(real_part + 1j * imag_part)

    r_hat = 0.5 * (r_hat + jnp.conj(r_hat).T)  # enforce Hermiticity
    ev = jnp.linalg.eigvalsh(r_hat)
    safe_ev = jnp.clip(ev.real, 1e-9, None)
    safe_ev = safe_ev / jnp.sum(safe_ev)  # renormalize (estimation noise can shift trace off 1)
    return float(-jnp.sum(safe_ev * jnp.log2(safe_ev)))


# Empirically fitted in Dense-Evolution-Discovery Experiment 31 (20 trials
# per snapshot count on a |T> state, n_groups=20): std(n_snapshots) ~
# _FIT_C / n_snapshots ** _FIT_P. The T-state is a standard maximally-magic
# single-qubit state, a reasonable but NOT rigorously proven proxy for a
# "hard" case -- this is a rough built-in guide, not a formal guarantee for
# an arbitrary rho.
_FIT_C = 11.751
_FIT_P = 0.546


def approx_shadow_std(n_snapshots: int) -> float:
    """Rough approximate standard deviation (bits) of

    `magic_entropy_from_shadows`'s estimate at a given snapshot count,

    from an empirical fit (not a formal theorem) calibrated on a `|T>`

    state in Dense-Evolution-Discovery Experiment 31: 20 independent

    trials at each of 4 snapshot counts (3,000-100,000), a log-log linear

    regression gave `std(n) ~ 11.75 / n^0.546` -- the fitted exponent

    (0.546) is close to the ~0.5 ("error shrinks like 1/sqrt(n)") standard

    shadow/median-of-means theory predicts.



    This is a quick sanity-check fallback, not a guarantee for an

    arbitrary state -- call `fit_shadow_sample_complexity` on YOUR

    specific state if you need a real, state-calibrated error bound.

    """
    return _FIT_C / float(n_snapshots) ** _FIT_P


def fit_shadow_sample_complexity(rho: jnp.ndarray, exact_value: float, n_snapshots_list, n_trials: int, seed_base: int = 0):
    """Empirically measures `magic_entropy_from_shadows`'s standard

    deviation across `n_trials` independent shadow samplings at each

    snapshot count in `n_snapshots_list`, for the SPECIFIC state `rho`

    (rather than trusting `approx_shadow_std`'s built-in T-state-derived

    fallback), then fits `std(n) ~ C / n^p` via log-log linear regression

    -- the same method used to derive `approx_shadow_std`'s constants in

    the first place (Dense-Evolution-Discovery Experiment 31).



    `exact_value` should be `magic_entropy(rho)` -- used only to also

    report each snapshot count's mean estimation bias alongside the

    fitted curve, not part of the fit itself.



    Returns `(rows, fit_c, fit_p)`: `rows` is a list of per-snapshot-count

    dicts (`n_snapshots`, `mean_estimate`, `std_estimate`,

    `mean_abs_error`); `fit_c`/`fit_p` are the fitted constants for

    `C / n^p`, usable the same way as `approx_shadow_std` (or pass them to

    `approx_shadow_std`'s formula directly: `fit_c / n ** fit_p`).

    """
    rows = []
    for n_snap in n_snapshots_list:
        estimates = []
        for trial in range(n_trials):
            snaps = sample_classical_shadow(rho, n_snap, seed=seed_base + trial)
            estimates.append(magic_entropy_from_shadows(snaps))
        estimates = np.array(estimates)
        rows.append({
            "n_snapshots": n_snap, "n_trials": n_trials,
            "mean_estimate": float(estimates.mean()), "std_estimate": float(estimates.std()),
            "mean_abs_error": float(abs(estimates.mean() - exact_value)),
        })
    log_n = np.log([r["n_snapshots"] for r in rows])
    log_std = np.log([r["std_estimate"] for r in rows])
    slope, intercept = np.polyfit(log_n, log_std, 1)
    return rows, float(np.exp(intercept)), float(-slope)