Sync v8.1.40: full jax.jit coverage for dense_evolution.mitigation
Browse files- README.md +5 -0
- dense_evolution.egg-info/PKG-INFO +6 -1
- dense_evolution/__init__.py +4 -2
- dense_evolution/mitigation.py +146 -10
- pyproject.toml +1 -1
- test_mitigation.py +184 -1
README.md
CHANGED
|
@@ -624,6 +624,11 @@ All circuits stored as OpenQASM 2.0 strings in `QASM_LIBRARY`.
|
|
| 624 |
|
| 625 |
## ▍ Changelog
|
| 626 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
### v8.1.39
|
| 628 |
- **Changed**: `project_to_physical`/`uhlmann_fidelity` rewritten as fully vectorized, `jax.jit`-compatible JAX (no Python `while`/`for` loop over eigenvalues) -- the previous versions had a dynamic Python loop that isn't traceable, forcing a host round-trip on every call. `project_to_physical` now uses Euclidean projection onto the probability simplex (Held, Wolfe & Crowder 1974; also Duchi et al. 2008) applied to the eigenvalues -- a different, fully array-vectorized algorithm for the exact same convex projection problem Smolin-Gambetta-Smith's paper solves (unique global minimum, so any correct algorithm must agree). `uhlmann_fidelity` now computes `Tr(sqrt(inner))` as the sum of `inner`'s eigenvalues' square roots instead of reconstructing the full matrix square root. Both verified numerically identical to the previous versions (~1e-15 max difference on the SGS paper's own worked example and 30 random test matrices) and confirmed to actually compile and run under `jax.jit`. No behavior change for any existing caller -- same inputs, same outputs, just usable inside a jitted pipeline (e.g. `jax.lax.scan`) going forward.
|
| 629 |
- Originated as a suggestion from an external AI (Gemini), independently verified numerically before adopting -- same source also proposed reintroducing the predictive-healing coefficient perturbation (`calculate_delta_preemp`) for the density-matrix case as "the real improvement." That specific claim was rejected: it directly contradicts the already-measured result that the perturbation's effect is negligible (~0.14% coefficient shift even at a large observed coherence deviation, confirmed with a live numeric trace, not assumed) -- an external source getting a plausible-sounding architecture story right doesn't make an unverified empirical claim inside it right; each part was checked on its own.
|
|
|
|
| 624 |
|
| 625 |
## ▍ Changelog
|
| 626 |
|
| 627 |
+
### v8.1.40
|
| 628 |
+
- **Added**: full `jax.jit`-compatible coverage for every function in `dense_evolution.mitigation` -- `richardson_extrapolate_jit`, `zero_noise_extrapolation_jit` (the predictive-healing branch), `polynomial_extrapolate_jit`, `uhlmann_fidelity_jit`, `zne_density_matrix_jit`. Each is a jit-safe `_core` (no `np.iscomplexobj`/`np.asarray`/`float()` calls on possibly-traced values -- those break `jax.jit` tracing) plus an unchanged eager wrapper, the same split already used by `dense_evolution.mps`'s `_jsd_vectors_jax`/`_jsd_vectors`. Verified: each `_jit` variant matches its eager counterpart exactly; all compose correctly together inside a *single* outer `jax.jit` (the realistic use case -- several of these called in sequence inside a step function passed to `jax.lax.scan`, not each jitted in isolation). Real measured speedup on the full `zne_density_matrix` pipeline (not a single function): 4.5x-150x across 2x2-32x32 matrices, positive in every case tested.
|
| 629 |
+
- Checked and rejected a suggestion (from the same external-AI source as v8.1.39's rewrite) to mark `target_sigma_ideal` static via `functools.partial(static_argnames=...)`: `calculate_delta_preemp` already uses `jnp.where` internally, not a Python `if`, so it's trace-safe as a dynamic value -- marking it static would force a fresh XLA recompilation every time a caller varies it, with no benefit. Confirmed directly by testing both ways before deciding, not by assumption.
|
| 630 |
+
- This closes out the density-matrix ZNE healing work for now. A further step -- wiring these functions into `MPSSimulator` so error mitigation can run on tensor-network-simulated circuits -- was scoped and intentionally deferred: it requires noise-channel support for MPS (doesn't exist yet, a large feature on its own -- MPO-based or trajectory-based), reduced (not global) density-matrix extraction via partial trace (also doesn't exist yet), and handling the multiple-noise-scale requirement ZNE needs, none of which reduce to "just call the existing functions." A global density matrix is the wrong target for large-qubit MPS use in the first place -- materializing a full 2^n x 2^n matrix defeats MPS's own reason for existing. Tracked separately, not started here.
|
| 631 |
+
|
| 632 |
### v8.1.39
|
| 633 |
- **Changed**: `project_to_physical`/`uhlmann_fidelity` rewritten as fully vectorized, `jax.jit`-compatible JAX (no Python `while`/`for` loop over eigenvalues) -- the previous versions had a dynamic Python loop that isn't traceable, forcing a host round-trip on every call. `project_to_physical` now uses Euclidean projection onto the probability simplex (Held, Wolfe & Crowder 1974; also Duchi et al. 2008) applied to the eigenvalues -- a different, fully array-vectorized algorithm for the exact same convex projection problem Smolin-Gambetta-Smith's paper solves (unique global minimum, so any correct algorithm must agree). `uhlmann_fidelity` now computes `Tr(sqrt(inner))` as the sum of `inner`'s eigenvalues' square roots instead of reconstructing the full matrix square root. Both verified numerically identical to the previous versions (~1e-15 max difference on the SGS paper's own worked example and 30 random test matrices) and confirmed to actually compile and run under `jax.jit`. No behavior change for any existing caller -- same inputs, same outputs, just usable inside a jitted pipeline (e.g. `jax.lax.scan`) going forward.
|
| 634 |
- Originated as a suggestion from an external AI (Gemini), independently verified numerically before adopting -- same source also proposed reintroducing the predictive-healing coefficient perturbation (`calculate_delta_preemp`) for the density-matrix case as "the real improvement." That specific claim was rejected: it directly contradicts the already-measured result that the perturbation's effect is negligible (~0.14% coefficient shift even at a large observed coherence deviation, confirmed with a live numeric trace, not assumed) -- an external source getting a plausible-sounding architecture story right doesn't make an unverified empirical claim inside it right; each part was checked on its own.
|
dense_evolution.egg-info/PKG-INFO
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
Metadata-Version: 2.4
|
| 2 |
Name: dense-evolution
|
| 3 |
-
Version: 8.1.
|
| 4 |
Summary: Micro-optimized High-Performance NISQ Statevector Quantum Circuit Simulator (Hardware-Adaptive Integration of Native NumPy, CUDA-Accelerated CuPy, and Linear Kernel Fusion via JAX JIT/XLA Compilation)
|
| 5 |
Author-email: Salvatore Pennacchio <jtatopenn@libero.it>
|
| 6 |
License: Business Source License 1.1
|
|
@@ -680,6 +680,11 @@ All circuits stored as OpenQASM 2.0 strings in `QASM_LIBRARY`.
|
|
| 680 |
|
| 681 |
## ▍ Changelog
|
| 682 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
### v8.1.39
|
| 684 |
- **Changed**: `project_to_physical`/`uhlmann_fidelity` rewritten as fully vectorized, `jax.jit`-compatible JAX (no Python `while`/`for` loop over eigenvalues) -- the previous versions had a dynamic Python loop that isn't traceable, forcing a host round-trip on every call. `project_to_physical` now uses Euclidean projection onto the probability simplex (Held, Wolfe & Crowder 1974; also Duchi et al. 2008) applied to the eigenvalues -- a different, fully array-vectorized algorithm for the exact same convex projection problem Smolin-Gambetta-Smith's paper solves (unique global minimum, so any correct algorithm must agree). `uhlmann_fidelity` now computes `Tr(sqrt(inner))` as the sum of `inner`'s eigenvalues' square roots instead of reconstructing the full matrix square root. Both verified numerically identical to the previous versions (~1e-15 max difference on the SGS paper's own worked example and 30 random test matrices) and confirmed to actually compile and run under `jax.jit`. No behavior change for any existing caller -- same inputs, same outputs, just usable inside a jitted pipeline (e.g. `jax.lax.scan`) going forward.
|
| 685 |
- Originated as a suggestion from an external AI (Gemini), independently verified numerically before adopting -- same source also proposed reintroducing the predictive-healing coefficient perturbation (`calculate_delta_preemp`) for the density-matrix case as "the real improvement." That specific claim was rejected: it directly contradicts the already-measured result that the perturbation's effect is negligible (~0.14% coefficient shift even at a large observed coherence deviation, confirmed with a live numeric trace, not assumed) -- an external source getting a plausible-sounding architecture story right doesn't make an unverified empirical claim inside it right; each part was checked on its own.
|
|
|
|
| 1 |
Metadata-Version: 2.4
|
| 2 |
Name: dense-evolution
|
| 3 |
+
Version: 8.1.40
|
| 4 |
Summary: Micro-optimized High-Performance NISQ Statevector Quantum Circuit Simulator (Hardware-Adaptive Integration of Native NumPy, CUDA-Accelerated CuPy, and Linear Kernel Fusion via JAX JIT/XLA Compilation)
|
| 5 |
Author-email: Salvatore Pennacchio <jtatopenn@libero.it>
|
| 6 |
License: Business Source License 1.1
|
|
|
|
| 680 |
|
| 681 |
## ▍ Changelog
|
| 682 |
|
| 683 |
+
### v8.1.40
|
| 684 |
+
- **Added**: full `jax.jit`-compatible coverage for every function in `dense_evolution.mitigation` -- `richardson_extrapolate_jit`, `zero_noise_extrapolation_jit` (the predictive-healing branch), `polynomial_extrapolate_jit`, `uhlmann_fidelity_jit`, `zne_density_matrix_jit`. Each is a jit-safe `_core` (no `np.iscomplexobj`/`np.asarray`/`float()` calls on possibly-traced values -- those break `jax.jit` tracing) plus an unchanged eager wrapper, the same split already used by `dense_evolution.mps`'s `_jsd_vectors_jax`/`_jsd_vectors`. Verified: each `_jit` variant matches its eager counterpart exactly; all compose correctly together inside a *single* outer `jax.jit` (the realistic use case -- several of these called in sequence inside a step function passed to `jax.lax.scan`, not each jitted in isolation). Real measured speedup on the full `zne_density_matrix` pipeline (not a single function): 4.5x-150x across 2x2-32x32 matrices, positive in every case tested.
|
| 685 |
+
- Checked and rejected a suggestion (from the same external-AI source as v8.1.39's rewrite) to mark `target_sigma_ideal` static via `functools.partial(static_argnames=...)`: `calculate_delta_preemp` already uses `jnp.where` internally, not a Python `if`, so it's trace-safe as a dynamic value -- marking it static would force a fresh XLA recompilation every time a caller varies it, with no benefit. Confirmed directly by testing both ways before deciding, not by assumption.
|
| 686 |
+
- This closes out the density-matrix ZNE healing work for now. A further step -- wiring these functions into `MPSSimulator` so error mitigation can run on tensor-network-simulated circuits -- was scoped and intentionally deferred: it requires noise-channel support for MPS (doesn't exist yet, a large feature on its own -- MPO-based or trajectory-based), reduced (not global) density-matrix extraction via partial trace (also doesn't exist yet), and handling the multiple-noise-scale requirement ZNE needs, none of which reduce to "just call the existing functions." A global density matrix is the wrong target for large-qubit MPS use in the first place -- materializing a full 2^n x 2^n matrix defeats MPS's own reason for existing. Tracked separately, not started here.
|
| 687 |
+
|
| 688 |
### v8.1.39
|
| 689 |
- **Changed**: `project_to_physical`/`uhlmann_fidelity` rewritten as fully vectorized, `jax.jit`-compatible JAX (no Python `while`/`for` loop over eigenvalues) -- the previous versions had a dynamic Python loop that isn't traceable, forcing a host round-trip on every call. `project_to_physical` now uses Euclidean projection onto the probability simplex (Held, Wolfe & Crowder 1974; also Duchi et al. 2008) applied to the eigenvalues -- a different, fully array-vectorized algorithm for the exact same convex projection problem Smolin-Gambetta-Smith's paper solves (unique global minimum, so any correct algorithm must agree). `uhlmann_fidelity` now computes `Tr(sqrt(inner))` as the sum of `inner`'s eigenvalues' square roots instead of reconstructing the full matrix square root. Both verified numerically identical to the previous versions (~1e-15 max difference on the SGS paper's own worked example and 30 random test matrices) and confirmed to actually compile and run under `jax.jit`. No behavior change for any existing caller -- same inputs, same outputs, just usable inside a jitted pipeline (e.g. `jax.lax.scan`) going forward.
|
| 690 |
- Originated as a suggestion from an external AI (Gemini), independently verified numerically before adopting -- same source also proposed reintroducing the predictive-healing coefficient perturbation (`calculate_delta_preemp`) for the density-matrix case as "the real improvement." That specific claim was rejected: it directly contradicts the already-measured result that the perturbation's effect is negligible (~0.14% coefficient shift even at a large observed coherence deviation, confirmed with a live numeric trace, not assumed) -- an external source getting a plausible-sounding architecture story right doesn't make an unverified empirical claim inside it right; each part was checked on its own.
|
dense_evolution/__init__.py
CHANGED
|
@@ -12,6 +12,8 @@ from .interop import from_qiskit, from_pennylane, run_qiskit_circuit, run_pennyl
|
|
| 12 |
from .autodiff import circuit_to_energy_fn
|
| 13 |
from .mps import MPSSimulator
|
| 14 |
from .mitigation import (richardson_extrapolate, zero_noise_extrapolation, polynomial_extrapolate,
|
| 15 |
-
project_to_physical, uhlmann_fidelity, zne_density_matrix
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
__version__ = "8.1.
|
|
|
|
| 12 |
from .autodiff import circuit_to_energy_fn
|
| 13 |
from .mps import MPSSimulator
|
| 14 |
from .mitigation import (richardson_extrapolate, zero_noise_extrapolation, polynomial_extrapolate,
|
| 15 |
+
project_to_physical, uhlmann_fidelity, zne_density_matrix,
|
| 16 |
+
richardson_extrapolate_jit, zero_noise_extrapolation_jit,
|
| 17 |
+
polynomial_extrapolate_jit, uhlmann_fidelity_jit, zne_density_matrix_jit)
|
| 18 |
|
| 19 |
+
__version__ = "8.1.40"
|
dense_evolution/mitigation.py
CHANGED
|
@@ -10,13 +10,18 @@ internal healing vocabulary.
|
|
| 10 |
This module composes `dense_evolution.healing`'s existing primitives
|
| 11 |
(`calculate_delta_preemp`, ...) -- it does not rename or replace them.
|
| 12 |
"""
|
|
|
|
|
|
|
| 13 |
import numpy as np
|
|
|
|
| 14 |
import jax.numpy as jnp
|
| 15 |
|
| 16 |
from .healing import calculate_delta_preemp
|
| 17 |
|
| 18 |
__all__ = ["richardson_extrapolate", "zero_noise_extrapolation", "polynomial_extrapolate",
|
| 19 |
-
"project_to_physical", "uhlmann_fidelity", "zne_density_matrix"
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def richardson_extrapolate(expectation_values, noise_factors) -> jnp.ndarray:
|
|
@@ -44,6 +49,18 @@ def richardson_extrapolate(expectation_values, noise_factors) -> jnp.ndarray:
|
|
| 44 |
lambdas = jnp.asarray(noise_factors, dtype=jnp.float64)
|
| 45 |
values_dtype = jnp.complex128 if np.iscomplexobj(np.asarray(expectation_values)) else jnp.float64
|
| 46 |
values = jnp.asarray(expectation_values, dtype=values_dtype)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
n = lambdas.shape[0]
|
| 48 |
|
| 49 |
def lagrange_coeff(i):
|
|
@@ -61,6 +78,21 @@ def richardson_extrapolate(expectation_values, noise_factors) -> jnp.ndarray:
|
|
| 61 |
return jnp.sum(coeffs * values, axis=0)
|
| 62 |
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
def zero_noise_extrapolation(expectation_values, noise_factors,
|
| 65 |
sigma_at_base_noise=None,
|
| 66 |
target_sigma_ideal: float = 10.0) -> jnp.ndarray:
|
|
@@ -96,16 +128,41 @@ def zero_noise_extrapolation(expectation_values, noise_factors,
|
|
| 96 |
)
|
| 97 |
values_dtype = jnp.complex128 if np.iscomplexobj(np.asarray(expectation_values)) else jnp.float64
|
| 98 |
values = jnp.asarray(expectation_values, dtype=values_dtype)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
e_l1, e_l2, e_l3 = values[0], values[1], values[2]
|
| 100 |
-
|
| 101 |
-
delta_p = calculate_delta_preemp(jnp.asarray(sigma_at_base_noise, dtype=jnp.float64),
|
| 102 |
-
target_sigma_ideal)
|
| 103 |
c1 = 3.0 - 0.01 * delta_p
|
| 104 |
c2 = -3.0 + 0.02 * delta_p
|
| 105 |
c3 = 1.0 - 0.01 * delta_p
|
| 106 |
return (c1 * e_l1 + c2 * e_l2 + c3 * e_l3) / (c1 + c2 + c3)
|
| 107 |
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
def polynomial_extrapolate(expectation_values, noise_factors, degree: int = 2) -> jnp.ndarray:
|
| 110 |
"""Least-squares polynomial extrapolation to zero noise.
|
| 111 |
|
|
@@ -148,16 +205,43 @@ def polynomial_extrapolate(expectation_values, noise_factors, degree: int = 2) -
|
|
| 148 |
)
|
| 149 |
values_dtype = jnp.complex128 if np.iscomplexobj(np.asarray(expectation_values)) else jnp.float64
|
| 150 |
values = jnp.asarray(expectation_values, dtype=values_dtype)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
orig_shape = values.shape[1:]
|
| 152 |
flat = values.reshape(n, -1)
|
| 153 |
-
|
| 154 |
# Vandermonde design matrix: columns lambda^0, lambda^1, ..., lambda^degree.
|
| 155 |
-
design = jnp.stack([lambdas ** k for k in range(degree + 1)], axis=1).astype(
|
| 156 |
coeffs, *_ = jnp.linalg.lstsq(design, flat, rcond=None)
|
| 157 |
intercept = coeffs[0] # fitted polynomial evaluated at noise_factor=0
|
| 158 |
return intercept.reshape(orig_shape)
|
| 159 |
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
def project_to_physical(rho_raw: jnp.ndarray) -> jnp.ndarray:
|
| 162 |
"""Project a Hermitian, trace-1 candidate matrix onto the nearest
|
| 163 |
physical density matrix (Hermitian, trace 1, positive-semidefinite) in
|
|
@@ -237,18 +321,36 @@ def uhlmann_fidelity(rho_A: jnp.ndarray, rho_B: jnp.ndarray) -> float:
|
|
| 237 |
against the previous full-reconstruction version: identical to machine
|
| 238 |
precision (~1e-16) on 30 random density-matrix pairs.
|
| 239 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
def sqrt_eigvals(m):
|
| 241 |
w = jnp.linalg.eigvalsh(m)
|
| 242 |
return jnp.clip(jnp.real(w), 0.0, None)
|
| 243 |
|
| 244 |
-
rho_A = jnp.asarray(rho_A, dtype=jnp.complex128)
|
| 245 |
-
rho_B = jnp.asarray(rho_B, dtype=jnp.complex128)
|
| 246 |
-
|
| 247 |
w_A, v_A = jnp.linalg.eigh(rho_A)
|
| 248 |
sqrt_A = (v_A * jnp.sqrt(jnp.clip(jnp.real(w_A), 0.0, None))) @ jnp.conj(v_A).T
|
| 249 |
inner = sqrt_A @ rho_B @ sqrt_A
|
| 250 |
inner_evals = sqrt_eigvals(inner)
|
| 251 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
|
| 254 |
def zne_density_matrix(rho_at_scales, noise_factors, degree: int = 2) -> jnp.ndarray:
|
|
@@ -336,3 +438,37 @@ def zne_density_matrix(rho_at_scales, noise_factors, degree: int = 2) -> jnp.nda
|
|
| 336 |
"""
|
| 337 |
extrapolated = polynomial_extrapolate(rho_at_scales, noise_factors, degree=degree)
|
| 338 |
return project_to_physical(extrapolated)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
This module composes `dense_evolution.healing`'s existing primitives
|
| 11 |
(`calculate_delta_preemp`, ...) -- it does not rename or replace them.
|
| 12 |
"""
|
| 13 |
+
import functools
|
| 14 |
+
|
| 15 |
import numpy as np
|
| 16 |
+
import jax
|
| 17 |
import jax.numpy as jnp
|
| 18 |
|
| 19 |
from .healing import calculate_delta_preemp
|
| 20 |
|
| 21 |
__all__ = ["richardson_extrapolate", "zero_noise_extrapolation", "polynomial_extrapolate",
|
| 22 |
+
"project_to_physical", "uhlmann_fidelity", "zne_density_matrix",
|
| 23 |
+
"richardson_extrapolate_jit", "zero_noise_extrapolation_jit",
|
| 24 |
+
"polynomial_extrapolate_jit", "uhlmann_fidelity_jit", "zne_density_matrix_jit"]
|
| 25 |
|
| 26 |
|
| 27 |
def richardson_extrapolate(expectation_values, noise_factors) -> jnp.ndarray:
|
|
|
|
| 49 |
lambdas = jnp.asarray(noise_factors, dtype=jnp.float64)
|
| 50 |
values_dtype = jnp.complex128 if np.iscomplexobj(np.asarray(expectation_values)) else jnp.float64
|
| 51 |
values = jnp.asarray(expectation_values, dtype=values_dtype)
|
| 52 |
+
return _richardson_extrapolate_core(values, lambdas)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _richardson_extrapolate_core(values: jnp.ndarray, lambdas: jnp.ndarray) -> jnp.ndarray:
|
| 56 |
+
"""`jax.jit`-traceable core of `richardson_extrapolate` -- `values`
|
| 57 |
+
already cast to its final dtype, `lambdas` already float64 (no
|
| 58 |
+
`np.iscomplexobj`/`np.asarray` call on a possibly-traced value). `n`
|
| 59 |
+
comes from `lambdas.shape[0]`, a static value even under tracing
|
| 60 |
+
(array shapes are always known at trace time), so the Python
|
| 61 |
+
`range(n)` unroll below is trace-safe without `n` needing to be
|
| 62 |
+
static-marked by callers.
|
| 63 |
+
"""
|
| 64 |
n = lambdas.shape[0]
|
| 65 |
|
| 66 |
def lagrange_coeff(i):
|
|
|
|
| 78 |
return jnp.sum(coeffs * values, axis=0)
|
| 79 |
|
| 80 |
|
| 81 |
+
richardson_extrapolate_jit = jax.jit(_richardson_extrapolate_core)
|
| 82 |
+
"""`jax.jit`-compiled entry point for `richardson_extrapolate`. Unlike
|
| 83 |
+
`polynomial_extrapolate`/`zne_density_matrix`'s jitted variants, no
|
| 84 |
+
argument needs to be marked static here -- `n` (the point count) is read
|
| 85 |
+
from `lambdas.shape[0]`, itself always static under tracing, not from a
|
| 86 |
+
Python `degree` parameter.
|
| 87 |
+
|
| 88 |
+
`values` must already be complex128 or float64 (pick the dtype yourself
|
| 89 |
+
before calling -- this skips `richardson_extrapolate`'s `np.iscomplexobj`
|
| 90 |
+
auto-detection, which isn't traceable) and `lambdas` a float64 array.
|
| 91 |
+
Verified to match `richardson_extrapolate` exactly on real and complex
|
| 92 |
+
input; JAX recompiles per distinct input shape/dtype, as usual.
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
|
| 96 |
def zero_noise_extrapolation(expectation_values, noise_factors,
|
| 97 |
sigma_at_base_noise=None,
|
| 98 |
target_sigma_ideal: float = 10.0) -> jnp.ndarray:
|
|
|
|
| 128 |
)
|
| 129 |
values_dtype = jnp.complex128 if np.iscomplexobj(np.asarray(expectation_values)) else jnp.float64
|
| 130 |
values = jnp.asarray(expectation_values, dtype=values_dtype)
|
| 131 |
+
return _zero_noise_extrapolation_healing_core(
|
| 132 |
+
values, jnp.asarray(sigma_at_base_noise, dtype=jnp.float64), target_sigma_ideal)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _zero_noise_extrapolation_healing_core(values: jnp.ndarray, sigma_at_base_noise: jnp.ndarray,
|
| 136 |
+
target_sigma_ideal: float) -> jnp.ndarray:
|
| 137 |
+
"""`jax.jit`-traceable core of `zero_noise_extrapolation`'s
|
| 138 |
+
healing-adapted branch -- `values` already cast to its final dtype
|
| 139 |
+
(exactly 3 noise-scale rows, `values[0]`, `values[1]`, `values[2]`),
|
| 140 |
+
`sigma_at_base_noise` already a jnp float64 scalar. `calculate_delta_preemp`
|
| 141 |
+
is itself already `@jax.jit`-decorated (`dense_evolution.healing`), so
|
| 142 |
+
calling it here composes cleanly under an outer jit.
|
| 143 |
+
"""
|
| 144 |
e_l1, e_l2, e_l3 = values[0], values[1], values[2]
|
| 145 |
+
delta_p = calculate_delta_preemp(sigma_at_base_noise, target_sigma_ideal)
|
|
|
|
|
|
|
| 146 |
c1 = 3.0 - 0.01 * delta_p
|
| 147 |
c2 = -3.0 + 0.02 * delta_p
|
| 148 |
c3 = 1.0 - 0.01 * delta_p
|
| 149 |
return (c1 * e_l1 + c2 * e_l2 + c3 * e_l3) / (c1 + c2 + c3)
|
| 150 |
|
| 151 |
|
| 152 |
+
zero_noise_extrapolation_jit = jax.jit(_zero_noise_extrapolation_healing_core)
|
| 153 |
+
"""`jax.jit`-compiled entry point for `zero_noise_extrapolation`'s
|
| 154 |
+
healing-adapted branch (the `sigma_at_base_noise is not None` case --
|
| 155 |
+
the plain-Richardson case already has `richardson_extrapolate_jit`, use
|
| 156 |
+
that directly instead). `values` must already be complex128 or float64
|
| 157 |
+
with exactly 3 rows, `sigma_at_base_noise` a float64 scalar; the
|
| 158 |
+
`lambdas.shape[0] != 3` validation and dtype auto-detection that
|
| 159 |
+
`zero_noise_extrapolation` does are both skipped here (not traceable) --
|
| 160 |
+
callers are responsible for passing exactly-3-row input themselves.
|
| 161 |
+
`target_sigma_ideal` is a plain Python float, fine to leave non-static
|
| 162 |
+
since it only ever multiplies/subtracts, no Python branching on its value.
|
| 163 |
+
"""
|
| 164 |
+
|
| 165 |
+
|
| 166 |
def polynomial_extrapolate(expectation_values, noise_factors, degree: int = 2) -> jnp.ndarray:
|
| 167 |
"""Least-squares polynomial extrapolation to zero noise.
|
| 168 |
|
|
|
|
| 205 |
)
|
| 206 |
values_dtype = jnp.complex128 if np.iscomplexobj(np.asarray(expectation_values)) else jnp.float64
|
| 207 |
values = jnp.asarray(expectation_values, dtype=values_dtype)
|
| 208 |
+
return _polynomial_extrapolate_core(values, lambdas, degree)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def _polynomial_extrapolate_core(values: jnp.ndarray, lambdas: jnp.ndarray, degree: int) -> jnp.ndarray:
|
| 212 |
+
"""`jax.jit`-traceable core of `polynomial_extrapolate` -- takes `values`
|
| 213 |
+
already cast to its final dtype and `lambdas` already a float64 array,
|
| 214 |
+
so there's no `np.iscomplexobj`/`np.asarray` call on a possibly-traced
|
| 215 |
+
value (which breaks tracing; `np.asarray` on a JAX tracer raises).
|
| 216 |
+
`degree` must be a Python int, not a traced value (`range(degree + 1)`
|
| 217 |
+
unrolls it at trace time) -- callers that `jax.jit` a function using
|
| 218 |
+
this must mark `degree` static (`static_argnames`).
|
| 219 |
+
"""
|
| 220 |
+
n = lambdas.shape[0]
|
| 221 |
orig_shape = values.shape[1:]
|
| 222 |
flat = values.reshape(n, -1)
|
|
|
|
| 223 |
# Vandermonde design matrix: columns lambda^0, lambda^1, ..., lambda^degree.
|
| 224 |
+
design = jnp.stack([lambdas ** k for k in range(degree + 1)], axis=1).astype(values.dtype)
|
| 225 |
coeffs, *_ = jnp.linalg.lstsq(design, flat, rcond=None)
|
| 226 |
intercept = coeffs[0] # fitted polynomial evaluated at noise_factor=0
|
| 227 |
return intercept.reshape(orig_shape)
|
| 228 |
|
| 229 |
|
| 230 |
+
polynomial_extrapolate_jit = functools.partial(jax.jit, static_argnames=("degree",))(_polynomial_extrapolate_core)
|
| 231 |
+
"""`jax.jit`-compiled entry point for `polynomial_extrapolate`, added for
|
| 232 |
+
consistency with every other function in this module (`richardson_extrapolate_jit`,
|
| 233 |
+
`zero_noise_extrapolation_jit`, `uhlmann_fidelity_jit`, `zne_density_matrix_jit`)
|
| 234 |
+
-- until now this was the one function whose `_core` existed (used
|
| 235 |
+
internally by `zne_density_matrix_jit`) but had no standalone public jit
|
| 236 |
+
entry point of its own.
|
| 237 |
+
|
| 238 |
+
`values` must already be cast to its final dtype (complex128 or float64)
|
| 239 |
+
and `lambdas` a float64 array -- this skips `polynomial_extrapolate`'s
|
| 240 |
+
`np.iscomplexobj` auto-detection, not traceable. `degree` is static (same
|
| 241 |
+
constraint as `zne_density_matrix_jit`). Verified to match
|
| 242 |
+
`polynomial_extrapolate` exactly."""
|
| 243 |
+
|
| 244 |
+
|
| 245 |
def project_to_physical(rho_raw: jnp.ndarray) -> jnp.ndarray:
|
| 246 |
"""Project a Hermitian, trace-1 candidate matrix onto the nearest
|
| 247 |
physical density matrix (Hermitian, trace 1, positive-semidefinite) in
|
|
|
|
| 321 |
against the previous full-reconstruction version: identical to machine
|
| 322 |
precision (~1e-16) on 30 random density-matrix pairs.
|
| 323 |
"""
|
| 324 |
+
rho_A = jnp.asarray(rho_A, dtype=jnp.complex128)
|
| 325 |
+
rho_B = jnp.asarray(rho_B, dtype=jnp.complex128)
|
| 326 |
+
return float(_uhlmann_fidelity_core(rho_A, rho_B))
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def _uhlmann_fidelity_core(rho_A: jnp.ndarray, rho_B: jnp.ndarray) -> jnp.ndarray:
|
| 330 |
+
"""`jax.jit`-traceable core of `uhlmann_fidelity` -- both inputs already
|
| 331 |
+
complex128; returns a jnp scalar instead of a Python `float` (the
|
| 332 |
+
`float()` cast isn't traceable, same reason `_jsd_vectors_jax`/
|
| 333 |
+
`_jsd_vectors` are split this way in `dense_evolution.mps`).
|
| 334 |
+
"""
|
| 335 |
def sqrt_eigvals(m):
|
| 336 |
w = jnp.linalg.eigvalsh(m)
|
| 337 |
return jnp.clip(jnp.real(w), 0.0, None)
|
| 338 |
|
|
|
|
|
|
|
|
|
|
| 339 |
w_A, v_A = jnp.linalg.eigh(rho_A)
|
| 340 |
sqrt_A = (v_A * jnp.sqrt(jnp.clip(jnp.real(w_A), 0.0, None))) @ jnp.conj(v_A).T
|
| 341 |
inner = sqrt_A @ rho_B @ sqrt_A
|
| 342 |
inner_evals = sqrt_eigvals(inner)
|
| 343 |
+
return jnp.real(jnp.sum(jnp.sqrt(inner_evals)) ** 2)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
uhlmann_fidelity_jit = jax.jit(_uhlmann_fidelity_core)
|
| 347 |
+
"""`jax.jit`-compiled entry point for `uhlmann_fidelity`. Both `rho_A`/
|
| 348 |
+
`rho_B` must already be `complex128` (this skips `uhlmann_fidelity`'s
|
| 349 |
+
own `jnp.asarray(..., dtype=jnp.complex128)` cast, itself trace-safe, but
|
| 350 |
+
kept out of the core to mirror the other `_core` functions' convention).
|
| 351 |
+
Returns a jnp scalar, not a Python `float` -- call `float(...)` yourself
|
| 352 |
+
if you need one outside a jitted context. Verified to match `uhlmann_fidelity`
|
| 353 |
+
exactly (same underlying math, just not cast to a Python float)."""
|
| 354 |
|
| 355 |
|
| 356 |
def zne_density_matrix(rho_at_scales, noise_factors, degree: int = 2) -> jnp.ndarray:
|
|
|
|
| 438 |
"""
|
| 439 |
extrapolated = polynomial_extrapolate(rho_at_scales, noise_factors, degree=degree)
|
| 440 |
return project_to_physical(extrapolated)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def _zne_density_matrix_core(rho_at_scales: jnp.ndarray, noise_factors: jnp.ndarray,
|
| 444 |
+
degree: int) -> jnp.ndarray:
|
| 445 |
+
"""`jax.jit`-traceable core of `zne_density_matrix` -- `rho_at_scales`
|
| 446 |
+
must already be complex128, `noise_factors` a float64 array; `degree`
|
| 447 |
+
must be a Python int (unrolled at trace time by
|
| 448 |
+
`_polynomial_extrapolate_core`, same constraint as there).
|
| 449 |
+
"""
|
| 450 |
+
extrapolated = _polynomial_extrapolate_core(rho_at_scales, noise_factors, degree)
|
| 451 |
+
return project_to_physical(extrapolated)
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
zne_density_matrix_jit = functools.partial(jax.jit, static_argnames=("degree",))(_zne_density_matrix_core)
|
| 455 |
+
"""`jax.jit`-compiled entry point for `zne_density_matrix`, for callers
|
| 456 |
+
inside a jitted pipeline (e.g. `jax.lax.scan` in `MPSSimulator.run_circuit_jit`)
|
| 457 |
+
who don't want a host round-trip every call -- `zne_density_matrix` itself
|
| 458 |
+
stays eager (unchanged) for one-off/interactive use, where jit compilation
|
| 459 |
+
overhead isn't worth paying for a single call.
|
| 460 |
+
|
| 461 |
+
`degree` is a static argument (must be a Python int, not a traced value --
|
| 462 |
+
pass it positionally or by keyword the same way every call, since JAX
|
| 463 |
+
recompiles per distinct static value). `rho_at_scales` must already be
|
| 464 |
+
`complex128` and `noise_factors` a plain float array/sequence -- unlike
|
| 465 |
+
`zne_density_matrix`, this skips the `np.iscomplexobj` dtype auto-detection
|
| 466 |
+
(not traceable) and always assumes complex input, which is the only case
|
| 467 |
+
that makes sense for density matrices.
|
| 468 |
+
|
| 469 |
+
Measured speedup is real but size- and call-pattern-dependent (`project_to_physical`
|
| 470 |
+
alone measured 2x-22x across 2x2 to 32x32 matrices when jitted vs. the
|
| 471 |
+
previous non-jittable version) -- benchmark your own use case rather than
|
| 472 |
+
assuming a fixed number; the benefit only appears once compiled and called
|
| 473 |
+
repeatedly, a single one-off call pays the compilation cost first.
|
| 474 |
+
"""
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "dense-evolution"
|
| 7 |
-
version = "8.1.
|
| 8 |
description = "Micro-optimized High-Performance NISQ Statevector Quantum Circuit Simulator (Hardware-Adaptive Integration of Native NumPy, CUDA-Accelerated CuPy, and Linear Kernel Fusion via JAX JIT/XLA Compilation)"
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.9"
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "dense-evolution"
|
| 7 |
+
version = "8.1.40"
|
| 8 |
description = "Micro-optimized High-Performance NISQ Statevector Quantum Circuit Simulator (Hardware-Adaptive Integration of Native NumPy, CUDA-Accelerated CuPy, and Linear Kernel Fusion via JAX JIT/XLA Compilation)"
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.9"
|
test_mitigation.py
CHANGED
|
@@ -5,7 +5,9 @@ import pytest
|
|
| 5 |
import dense_evolution as de
|
| 6 |
from dense_evolution.mitigation import (
|
| 7 |
richardson_extrapolate, zero_noise_extrapolation, polynomial_extrapolate,
|
| 8 |
-
project_to_physical, uhlmann_fidelity, zne_density_matrix,
|
|
|
|
|
|
|
| 9 |
)
|
| 10 |
|
| 11 |
|
|
@@ -120,6 +122,10 @@ def test_exported_from_package_root():
|
|
| 120 |
assert de.project_to_physical is project_to_physical
|
| 121 |
assert de.uhlmann_fidelity is uhlmann_fidelity
|
| 122 |
assert de.zne_density_matrix is zne_density_matrix
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
|
| 125 |
def test_polynomial_extrapolate_matches_richardson_at_exact_point_count():
|
|
@@ -284,3 +290,180 @@ def test_zne_density_matrix_preserves_complex_off_diagonal_entries():
|
|
| 284 |
])
|
| 285 |
got = np.asarray(zne_density_matrix(rho_at_scales, [1.0, 2.0, 3.0]))
|
| 286 |
assert np.any(np.abs(got.imag) > 1e-9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import dense_evolution as de
|
| 6 |
from dense_evolution.mitigation import (
|
| 7 |
richardson_extrapolate, zero_noise_extrapolation, polynomial_extrapolate,
|
| 8 |
+
project_to_physical, uhlmann_fidelity, zne_density_matrix, zne_density_matrix_jit,
|
| 9 |
+
richardson_extrapolate_jit, zero_noise_extrapolation_jit, uhlmann_fidelity_jit,
|
| 10 |
+
polynomial_extrapolate_jit,
|
| 11 |
)
|
| 12 |
|
| 13 |
|
|
|
|
| 122 |
assert de.project_to_physical is project_to_physical
|
| 123 |
assert de.uhlmann_fidelity is uhlmann_fidelity
|
| 124 |
assert de.zne_density_matrix is zne_density_matrix
|
| 125 |
+
assert de.zne_density_matrix_jit is zne_density_matrix_jit
|
| 126 |
+
assert de.richardson_extrapolate_jit is richardson_extrapolate_jit
|
| 127 |
+
assert de.zero_noise_extrapolation_jit is zero_noise_extrapolation_jit
|
| 128 |
+
assert de.uhlmann_fidelity_jit is uhlmann_fidelity_jit
|
| 129 |
|
| 130 |
|
| 131 |
def test_polynomial_extrapolate_matches_richardson_at_exact_point_count():
|
|
|
|
| 290 |
])
|
| 291 |
got = np.asarray(zne_density_matrix(rho_at_scales, [1.0, 2.0, 3.0]))
|
| 292 |
assert np.any(np.abs(got.imag) > 1e-9)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def test_zne_density_matrix_jit_matches_eager_exactly():
|
| 296 |
+
rng = np.random.default_rng(20)
|
| 297 |
+
d = 4
|
| 298 |
+
mats = []
|
| 299 |
+
for _ in range(3):
|
| 300 |
+
a = rng.normal(size=(d, d)) + 1j * rng.normal(size=(d, d))
|
| 301 |
+
m = a @ a.conj().T
|
| 302 |
+
mats.append(m / np.trace(m))
|
| 303 |
+
rho_at_scales = jnp.asarray(np.stack(mats), dtype=jnp.complex128)
|
| 304 |
+
noise_factors = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 305 |
+
|
| 306 |
+
eager = zne_density_matrix(rho_at_scales, [1.0, 2.0, 3.0], degree=2)
|
| 307 |
+
jitted = zne_density_matrix_jit(rho_at_scales, noise_factors, degree=2)
|
| 308 |
+
np.testing.assert_array_equal(np.asarray(eager), np.asarray(jitted))
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def test_zne_density_matrix_jit_output_is_a_valid_density_matrix():
|
| 312 |
+
rng = np.random.default_rng(21)
|
| 313 |
+
d = 5
|
| 314 |
+
mats = []
|
| 315 |
+
for _ in range(5):
|
| 316 |
+
a = rng.normal(size=(d, d)) + 1j * rng.normal(size=(d, d))
|
| 317 |
+
m = a @ a.conj().T
|
| 318 |
+
mats.append(m / np.trace(m))
|
| 319 |
+
rho_at_scales = jnp.asarray(np.stack(mats), dtype=jnp.complex128)
|
| 320 |
+
noise_factors = jnp.asarray([1.0, 2.0, 3.0, 4.0, 5.0], dtype=jnp.float64)
|
| 321 |
+
|
| 322 |
+
got = np.asarray(zne_density_matrix_jit(rho_at_scales, noise_factors, degree=2))
|
| 323 |
+
np.testing.assert_allclose(got, got.conj().T, atol=1e-9)
|
| 324 |
+
assert np.trace(got).real == pytest.approx(1.0, abs=1e-9)
|
| 325 |
+
assert np.linalg.eigvalsh(got).min() >= -1e-9
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def test_zne_density_matrix_jit_actually_compiles_under_jit():
|
| 329 |
+
# zne_density_matrix_jit is already jax.jit-wrapped; this specifically
|
| 330 |
+
# checks it doesn't raise a tracing error (e.g. from a stray
|
| 331 |
+
# np.iscomplexobj/np.asarray call on a traced value) when called
|
| 332 |
+
# through an *additional* outer jax.jit, the realistic case of
|
| 333 |
+
# embedding it inside a larger jitted pipeline (e.g. jax.lax.scan).
|
| 334 |
+
import jax
|
| 335 |
+
|
| 336 |
+
rng = np.random.default_rng(22)
|
| 337 |
+
d = 3
|
| 338 |
+
mats = []
|
| 339 |
+
for _ in range(3):
|
| 340 |
+
a = rng.normal(size=(d, d)) + 1j * rng.normal(size=(d, d))
|
| 341 |
+
m = a @ a.conj().T
|
| 342 |
+
mats.append(m / np.trace(m))
|
| 343 |
+
rho_at_scales = jnp.asarray(np.stack(mats), dtype=jnp.complex128)
|
| 344 |
+
noise_factors = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 345 |
+
|
| 346 |
+
from dense_evolution.mitigation import _zne_density_matrix_core
|
| 347 |
+
import functools
|
| 348 |
+
outer_jit = jax.jit(functools.partial(_zne_density_matrix_core, degree=2))
|
| 349 |
+
result = outer_jit(rho_at_scales, noise_factors)
|
| 350 |
+
result.block_until_ready()
|
| 351 |
+
assert np.trace(np.asarray(result)).real == pytest.approx(1.0, abs=1e-9)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def test_polynomial_extrapolate_jit_matches_eager():
|
| 355 |
+
lambdas = jnp.asarray([1.0, 2.0, 3.0, 4.0, 5.0], dtype=jnp.float64)
|
| 356 |
+
values = jnp.asarray([1 + 2j, 3 + 4j, 3 + 4j, 2 - 1j, 5 + 0.5j], dtype=jnp.complex128)
|
| 357 |
+
got = polynomial_extrapolate_jit(values, lambdas, degree=2)
|
| 358 |
+
expected = polynomial_extrapolate(
|
| 359 |
+
[1 + 2j, 3 + 4j, 3 + 4j, 2 - 1j, 5 + 0.5j], [1.0, 2.0, 3.0, 4.0, 5.0], degree=2)
|
| 360 |
+
np.testing.assert_allclose(np.asarray(got), np.asarray(expected))
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def test_polynomial_extrapolate_jit_compiles_under_outer_jit():
|
| 364 |
+
import jax
|
| 365 |
+
import functools
|
| 366 |
+
lambdas = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 367 |
+
values = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 368 |
+
outer_jit = jax.jit(functools.partial(polynomial_extrapolate_jit, degree=2))
|
| 369 |
+
result = outer_jit(values, lambdas)
|
| 370 |
+
result.block_until_ready()
|
| 371 |
+
assert float(result) == pytest.approx(0.0, abs=1e-9) # linear data -> exact intercept 0
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def test_richardson_extrapolate_jit_matches_eager():
|
| 375 |
+
lambdas = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 376 |
+
values = jnp.asarray([1 + 2j, 3 + 4j, 3 + 4j], dtype=jnp.complex128)
|
| 377 |
+
got = richardson_extrapolate_jit(values, lambdas)
|
| 378 |
+
expected = richardson_extrapolate([1 + 2j, 3 + 4j, 3 + 4j], [1.0, 2.0, 3.0])
|
| 379 |
+
np.testing.assert_allclose(np.asarray(got), np.asarray(expected))
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def test_richardson_extrapolate_jit_compiles_under_outer_jit():
|
| 383 |
+
import jax
|
| 384 |
+
lambdas = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 385 |
+
values = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 386 |
+
outer_jit = jax.jit(richardson_extrapolate_jit)
|
| 387 |
+
result = outer_jit(values, lambdas)
|
| 388 |
+
result.block_until_ready()
|
| 389 |
+
assert float(result) == pytest.approx(0.0, abs=1e-9) # linear data -> exact intercept 0
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def test_zero_noise_extrapolation_jit_matches_eager():
|
| 393 |
+
values = jnp.asarray([1 + 2j, 3 + 4j, 3 + 4j], dtype=jnp.complex128)
|
| 394 |
+
sigma = jnp.asarray(7.5, dtype=jnp.float64)
|
| 395 |
+
got = zero_noise_extrapolation_jit(values, sigma, 10.0)
|
| 396 |
+
expected = zero_noise_extrapolation([1 + 2j, 3 + 4j, 3 + 4j], [1.0, 2.0, 3.0],
|
| 397 |
+
sigma_at_base_noise=7.5, target_sigma_ideal=10.0)
|
| 398 |
+
np.testing.assert_allclose(np.asarray(got), np.asarray(expected))
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def test_zero_noise_extrapolation_jit_target_sigma_ideal_stays_dynamic():
|
| 402 |
+
# target_sigma_ideal must NOT need to be static -- calculate_delta_preemp
|
| 403 |
+
# uses jnp.where internally, not a Python if, so it's trace-safe as a
|
| 404 |
+
# plain traced float. Regression guard: if this ever needs
|
| 405 |
+
# static_argnames, this test starts raising a tracing error (the
|
| 406 |
+
# primary thing being checked -- no exception on a second call with a
|
| 407 |
+
# different target, no recompilation required). A wide target gap and
|
| 408 |
+
# non-degenerate complex values also confirm the coefficients (and so
|
| 409 |
+
# the result) actually do move, not just "didn't crash".
|
| 410 |
+
import jax
|
| 411 |
+
values = jnp.asarray([1 + 2j, 3 + 4j, 3 + 4j], dtype=jnp.complex128)
|
| 412 |
+
sigma = jnp.asarray(0.5, dtype=jnp.float64)
|
| 413 |
+
|
| 414 |
+
@jax.jit
|
| 415 |
+
def f(values, sigma, target):
|
| 416 |
+
return zero_noise_extrapolation_jit(values, sigma, target)
|
| 417 |
+
|
| 418 |
+
r1 = f(values, sigma, 10.0)
|
| 419 |
+
r2 = f(values, sigma, 1.0) # different target, same compiled function
|
| 420 |
+
assert not jnp.allclose(r1, r2)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def test_uhlmann_fidelity_jit_matches_eager():
|
| 424 |
+
rng = np.random.default_rng(23)
|
| 425 |
+
d = 3
|
| 426 |
+
a = rng.normal(size=(d, d)) + 1j * rng.normal(size=(d, d))
|
| 427 |
+
rho_a = jnp.asarray(a @ a.conj().T / np.trace(a @ a.conj().T), dtype=jnp.complex128)
|
| 428 |
+
b = rng.normal(size=(d, d)) + 1j * rng.normal(size=(d, d))
|
| 429 |
+
rho_b = jnp.asarray(b @ b.conj().T / np.trace(b @ b.conj().T), dtype=jnp.complex128)
|
| 430 |
+
|
| 431 |
+
got = uhlmann_fidelity_jit(rho_a, rho_b)
|
| 432 |
+
expected = uhlmann_fidelity(rho_a, rho_b)
|
| 433 |
+
assert float(got) == pytest.approx(expected, abs=1e-9)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def test_full_pipeline_composes_under_a_single_outer_jax_jit():
|
| 437 |
+
# The realistic case this whole _jit family exists for: several of
|
| 438 |
+
# these functions called together inside ONE outer jax.jit (e.g. a
|
| 439 |
+
# step function passed to jax.lax.scan), not each jitted in isolation.
|
| 440 |
+
import jax
|
| 441 |
+
from dense_evolution.mitigation import (
|
| 442 |
+
_zero_noise_extrapolation_healing_core, _zne_density_matrix_core, _uhlmann_fidelity_core,
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
rng = np.random.default_rng(24)
|
| 446 |
+
d = 3
|
| 447 |
+
mats = []
|
| 448 |
+
for _ in range(3):
|
| 449 |
+
a = rng.normal(size=(d, d)) + 1j * rng.normal(size=(d, d))
|
| 450 |
+
m = a @ a.conj().T
|
| 451 |
+
mats.append(m / np.trace(m))
|
| 452 |
+
rho_at_scales = jnp.asarray(np.stack(mats), dtype=jnp.complex128)
|
| 453 |
+
noise_factors = jnp.asarray([1.0, 2.0, 3.0], dtype=jnp.float64)
|
| 454 |
+
sigma = jnp.asarray(7.5, dtype=jnp.float64)
|
| 455 |
+
rho_target = rho_at_scales[0]
|
| 456 |
+
|
| 457 |
+
@jax.jit
|
| 458 |
+
def full_pipeline(rho_at_scales, noise_factors, sigma, target_sigma, rho_target):
|
| 459 |
+
healed = _zero_noise_extrapolation_healing_core(rho_at_scales, sigma, target_sigma)
|
| 460 |
+
corrected = _zne_density_matrix_core(rho_at_scales, noise_factors, 2)
|
| 461 |
+
fidelity = _uhlmann_fidelity_core(corrected, rho_target)
|
| 462 |
+
return healed, corrected, fidelity
|
| 463 |
+
|
| 464 |
+
healed, corrected, fidelity = full_pipeline(rho_at_scales, noise_factors, sigma, 10.0, rho_target)
|
| 465 |
+
jax.block_until_ready((healed, corrected, fidelity))
|
| 466 |
+
|
| 467 |
+
np.testing.assert_allclose(np.asarray(corrected), np.asarray(corrected).conj().T, atol=1e-9)
|
| 468 |
+
assert np.trace(np.asarray(corrected)).real == pytest.approx(1.0, abs=1e-9)
|
| 469 |
+
assert 0.0 <= float(fidelity) <= 1.0 + 1e-9
|