| """ |
| Zero-Noise Extrapolation (ZNE) |
| ------------------------------- |
| Standard error-mitigation entry points, named the way the field already |
| names them (Richardson extrapolation, noise factors, zero-noise |
| extrapolation -- same vocabulary as e.g. Mitiq's `zne` API), so callers |
| and tooling can find "ZNE" without first learning Dense-Evolution's |
| internal healing vocabulary. |
| |
| This module composes `dense_evolution.healing`'s existing primitives |
| (`calculate_delta_preemp`, ...) -- it does not rename or replace them. |
| """ |
| import jax.numpy as jnp |
|
|
| from .healing import calculate_delta_preemp |
|
|
| __all__ = ["richardson_extrapolate", "zero_noise_extrapolation"] |
|
|
|
|
| def richardson_extrapolate(expectation_values, noise_factors) -> jnp.ndarray: |
| """Polynomial (Lagrange) Richardson extrapolation to zero noise. |
| |
| `expectation_values[i]` is the value measured/simulated at noise scale |
| `noise_factors[i]` (e.g. 1x, 2x, 3x folded/scaled noise) -- a scalar, |
| or itself an array (e.g. a full probability distribution sampled at |
| that noise scale; extrapolated elementwise). Returns the extrapolated |
| zero-noise estimate, same shape as one `expectation_values[i]`. Works |
| for any number of points and any (not necessarily equally spaced) |
| noise factors; for the common 3-point case at noise_factors=(1,2,3) |
| this reduces exactly to the textbook coefficients (3, -3, 1). |
| """ |
| lambdas = jnp.asarray(noise_factors, dtype=jnp.float64) |
| values = jnp.asarray(expectation_values, dtype=jnp.float64) |
| n = lambdas.shape[0] |
|
|
| def lagrange_coeff(i): |
| others = jnp.concatenate([lambdas[:i], lambdas[i + 1:]]) |
| return jnp.prod((0.0 - others) / (lambdas[i] - others)) |
|
|
| coeffs = jnp.stack([lagrange_coeff(i) for i in range(n)]) |
| |
| |
| |
| |
| |
| |
| coeffs = coeffs.reshape((n,) + (1,) * (values.ndim - 1)) |
| return jnp.sum(coeffs * values, axis=0) |
|
|
|
|
| def zero_noise_extrapolation(expectation_values, noise_factors, |
| sigma_at_base_noise=None, |
| target_sigma_ideal: float = 10.0) -> jnp.ndarray: |
| """Zero-Noise Extrapolation -- plain, or healing-adapted when a |
| coherence signal is available. |
| |
| Without `sigma_at_base_noise`: standard Richardson ZNE |
| (`richardson_extrapolate`). |
| |
| With `sigma_at_base_noise` (the measured/simulated coherence sigma at |
| the base, unscaled noise level): the 3 Richardson coefficients are |
| perturbed by `dense_evolution.healing.calculate_delta_preemp` -- the |
| normalized deviation between the observed sigma and the ideal target |
| -- then renormalized to sum to 1. This is Dense-Evolution's |
| "predictive healing" ZNE variant: when the observed coherence is off |
| the ideal target, the extrapolation is nudged accordingly instead of |
| trusting the 3 raw noise-scaled points equally. |
| |
| The healing-adapted path currently only supports exactly 3 noise |
| factors (the case it has been derived and tested against); passing |
| `sigma_at_base_noise` with any other point count raises |
| NotImplementedError rather than silently generalizing an unverified |
| formula. |
| """ |
| if sigma_at_base_noise is None: |
| return richardson_extrapolate(expectation_values, noise_factors) |
|
|
| lambdas = jnp.asarray(noise_factors, dtype=jnp.float64) |
| if lambdas.shape[0] != 3: |
| raise NotImplementedError( |
| "Healing-adapted ZNE is only defined for exactly 3 noise factors; " |
| "call richardson_extrapolate(...) directly for the plain N-point case." |
| ) |
| values = jnp.asarray(expectation_values, dtype=jnp.float64) |
| e_l1, e_l2, e_l3 = values[0], values[1], values[2] |
|
|
| delta_p = calculate_delta_preemp(jnp.asarray(sigma_at_base_noise, dtype=jnp.float64), |
| target_sigma_ideal) |
| c1 = 3.0 - 0.01 * delta_p |
| c2 = -3.0 + 0.02 * delta_p |
| c3 = 1.0 - 0.01 * delta_p |
| return (c1 * e_l1 + c2 * e_l2 + c3 * e_l3) / (c1 + c2 + c3) |
|
|