File size: 6,040 Bytes
b9d7591 | 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 | # Examples
Three runnable, end-to-end examples, each lifted from real experiments/tests already in
the repository rather than written fresh for this page:
- [Density-matrix ZNE healing](#density-matrix-zne-healing) — from
[`experiments/matrix_healing_zne.py`](https://github.com/tatopenn-cell/Dense-Evolution/blob/main/experiments/matrix_healing_zne.py),
the exact script `zne_density_matrix`'s docstring reports measured numbers from.
- [MPS for low-entanglement circuits](#mps-for-low-entanglement-circuits) — from
`test_mps.py::test_run_circuit_jit_ghz_chain`.
- [Differentiable VQE](#differentiable-vqe) — from
`test_autodiff.py::TestCircuitToEnergyFn`.
## Density-matrix ZNE healing
Zero-noise extrapolation on full density matrices: run a circuit at several *scaled* noise
levels, extrapolate back to the zero-noise limit, then project the (generally unphysical)
extrapolated result back onto the physical cone of valid density matrices
([Smolin-Gambetta-Smith 2012](https://arxiv.org/abs/1106.5458)).
The ideal state (`rho_ideal`) is built once here purely to *grade* the result at the end.
It is never fed into the noise ensemble, the extrapolation, or the physical-projection
step — feeding it back in anywhere but the final fidelity check would be oracle access,
not error mitigation.
```python
import numpy as np
import jax.numpy as jnp
import dense_evolution as de
from dense_evolution.registry import NoiseModel
from dense_evolution.mitigation import uhlmann_fidelity, zne_density_matrix
N_QUBITS = 2
BASE_P = 0.05
SCALES = (1.0, 2.0, 3.0)
K_TRAJECTORIES = 200
def bell_state_sv():
sim = de.DenseSVSimulator(N_QUBITS)
sim.run_circuit([("h", 0), ("cx", 0, 1)])
return np.asarray(sim.get_statevector())
def noisy_density_matrix(ideal_sv, p, k, rng):
dim = len(ideal_sv)
rho = np.zeros((dim, dim), dtype=np.complex128)
for _ in range(k):
sv_noisy = NoiseModel.apply_to_sv(ideal_sv.copy(), N_QUBITS, 'depolarizing', p, rng=rng)
rho += np.outer(sv_noisy, sv_noisy.conj())
rho /= k
return jnp.asarray(rho, dtype=jnp.complex128)
ideal_sv = bell_state_sv()
rho_ideal = jnp.asarray(np.outer(ideal_sv, ideal_sv.conj()), dtype=jnp.complex128)
rng = np.random.default_rng(0)
rho_at_scales = jnp.stack([
noisy_density_matrix(ideal_sv, BASE_P * scale, K_TRAJECTORIES, rng)
for scale in SCALES
])
raw_fidelity = uhlmann_fidelity(rho_at_scales[0], rho_ideal) # base-scale, uncorrected
corrected = zne_density_matrix(rho_at_scales, SCALES) # Richardson + physical projection
corrected_fidelity = uhlmann_fidelity(corrected, rho_ideal) # grading only
print(f"raw fidelity: {raw_fidelity:.4f}")
print(f"corrected fidelity: {corrected_fidelity:.4f}")
```
Measured on this exact script (5 seeds, 400 trajectories each, 2–5 qubits, 5 noise
channels): 96/100 runs improve fidelity, mean delta +0.12. See the
[Changelog](changelog.md) and [`dense_evolution.mitigation`](api/mitigation.md) docstrings
for the full sweep and the honest negative results (predictive-healing coefficient
perturbation was tested and rejected for the matrix case — negligible effect even
amplified 100x).
Run it yourself:
```bash
python experiments/matrix_healing_zne.py
```
## MPS for low-entanglement circuits
`MPSSimulator` keeps a bounded bond dimension (`max_bond`), trading exactness for circuits
that stay low-entanglement (GHZ chains, shallow QAOA layers, most NISQ ansätze) at qubit
counts a dense statevector could never hold. `run_circuit_jit` fuses the whole circuit into
a single `jax.lax.scan`.
```python
import numpy as np
from dense_evolution import MPSSimulator
n = 6
ops = [["h", 0]] + [["cx", q, q + 1] for q in range(n - 1)] # GHZ chain
mps = MPSSimulator(n_qubits=n, max_bond=8)
mps.run_circuit_jit(ops)
prob = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
print(prob[0], prob[2 ** n - 1]) # both ~0.5, everything else ~0 -- the GHZ signature
```
See [`dense_evolution.mps`](api/mps.md) for the truncated-SVD mechanics and the
`jax.lax.scan` fusion this relies on.
## Differentiable VQE
`circuit_to_energy_fn` turns a parsed circuit into a `(theta, hamiltonian) -> (energy,
statevector)` function that is differentiable end-to-end with `jax.grad`/`jax.value_and_grad`
— no manual parameter-shift rule needed. This runs a short Adam loop against a random
diagonal Hamiltonian.
```python
import numpy as np
import jax
import jax.numpy as jnp
import dense_evolution as de
VQE_QASM = (
'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; creg c[2]; '
'ry(0.5) q[0]; rx(0.5) q[1]; cx q[0],q[1]; rz(0.2) q[1]; cx q[0],q[1]; '
'ry(0.5) q[0]; rx(0.5) q[1]; measure q -> c;'
)
def random_hamiltonian(n_qubits, seed=7):
rng = np.random.default_rng(seed)
values = np.sort(rng.uniform(-2.5, 2.5, 2 ** n_qubits))
return jnp.diag(jnp.array(values, dtype=jnp.float64))
circ = de.QASMParser().parse(VQE_QASM)
energy_fn, n_params = de.circuit_to_energy_fn(circ, circ.n_qubits)
h_matrix = random_hamiltonian(circ.n_qubits)
rng = np.random.default_rng(3)
theta = rng.uniform(-np.pi, np.pi, n_params)
energy_and_grad = jax.jit(jax.value_and_grad(energy_fn, argnums=0, has_aux=True))
m, v = np.zeros(n_params), np.zeros(n_params)
lr, beta1, beta2 = 0.1, 0.9, 0.999
for epoch in range(1, 31):
(energy, _), grad = energy_and_grad(jnp.asarray(theta), h_matrix)
grad = np.asarray(grad)
m = beta1 * m + (1 - beta1) * grad
v = beta2 * v + (1 - beta2) * (grad ** 2)
m_hat, v_hat = m / (1 - beta1 ** epoch), v / (1 - beta2 ** epoch)
theta = theta - lr * m_hat / (np.sqrt(v_hat) + 1e-8)
print(f"final energy: {float(energy):.4f}")
print(f"ground state: {float(jnp.min(jnp.diag(h_matrix))):.4f}")
```
`circuit_to_energy_fn` also accepts an optional `noise=` (`dense_evolution.NoiseSpec`, a
JAX pytree) to trace noisy VQE runs natively — composable with `jax.jit`, `jax.grad`, and
`jax.vmap` over noise-key batches. See [`dense_evolution.autodiff`](api/autodiff.md).
|