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).