dense-Evolution / docs /examples.md
Tatopenn's picture
Sync v8.1.42: docs site launch, examples/benchmarks/contributing pages, doc-example CI tests
b9d7591 verified
|
Raw
History Blame Contribute Delete
6.04 kB
# 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).