Registry (hardware detection)¶
Before running a big circuit, it's worth knowing how big "big" safely is on the actual
machine running it -- QuantumHardwareRegistry reads the current machine's RAM and GPU
availability and suggests a qubit ceiling from that, once, at construction time.
Despite living in dense_evolution.circuits.registry historically, this has nothing to
do with noise -- see Noise for NoiseModel/NoiseSpec instead.
Step 1. What does this machine look like?¶
import dense_evolution as de
reg = de.QuantumHardwareRegistry()
reg.ram_total, reg.has_jax, reg.has_gpu, reg.max_dense_qubits
ram_total is total system RAM in GB (this machine's own, whatever it happens to be),
has_jax/has_gpu are booleans, and max_dense_qubits is a suggested ceiling for a
dense statevector simulation: 28 at ram_total >= 50, 24 at >= 12, 20
otherwise -- three fixed tiers, not a formula fit to this machine's exact number. 20
above reflects an 8GB machine landing in the lowest tier.
Step 2. The same numbers, printed¶
print_diagnostics() is the same four fields from Step 1, condensed to one line --
useful as a quick sanity check at the top of a script before committing to a large
qubit count.
Details¶
max_dense_qubits is a suggestion, not an enforced limit: nothing in this class
stops a caller from constructing a DenseSVSimulator above it -- pair it with
Chunk's SafeMemoryGuard, which does actively refuse an allocation once
available memory drops below its own threshold, for a real enforced ceiling instead of
an advisory one.
Lazy x64: constructing QuantumHardwareRegistry is one of the entry points that
enables jax_enable_x64 the first time it runs, same as DenseSVSimulator/
circuit_to_energy_fn -- see Autodiff's own precision note.
registry ¶
NoiseModel ¶
Stochastic single-qubit Kraus channels applied directly to a statevector.
Each channel is a separate, importable module under dense_evolution.
noise.kraus (dense_evolution.noise.kraus.depolarizing, etc.) --
this class is the shared dispatcher: RNG/key setup, the per-qubit
loop, and final normalisation, common to every channel.
All channels are mathematically correct Kraus maps: - trace is preserved (normalisation enforced at the end) - phaseflip applies Z with probability p per qubit (non-deterministic) - amplitude_damping applies the correct K0/K1 Kraus operators - combined is a true worst-case NISQ mixture of all three Pauli errors plus amplitude damping
Supported models
'ideal' identity — no modification 'depolarizing' {√(1-p)I, √(p/3)X, √(p/3)Y, √(p/3)Z} 'bitflip' {√(1-p)I, √p·X} 'phaseflip' {√(1-p)I, √p·Z} ← was broken, now fixed 'amplitude_damping'{K0=diag(1,√(1-γ)), K1=[[0,√γ],[0,0]]} 'combined' depolarizing(p/2) + amplitude_damping(p/3), renormalised
Every channel draws one fire/no-fire decision per qubit per shot (plus one Pauli choice for depolarizing/combined's depolarizing sub-step), applied identically across the whole statevector -- the same single-Pauli-per-qubit-per-shot convention STIM's DEPOLARIZE1(p) uses. Prior to v8.1.57, every channel instead drew 2**(n-1) INDEPENDENT decisions per qubit per shot, one per amplitude pair (i.e. one per branch of the other n-1 qubits) -- inert on a product state, but on an entangled state it over-decohered any coherence-sensitive (off-diagonal) observable, up to hundreds of sigma vs the exact density-matrix Kraus-sum result on test cases (e.g. per-branch sampling dropped a measured value from 1.0 to 0.31 at p=0.15 on one such test -- see the v8.1.57 changelog entry for the full reproduction).
apply_to_sv
staticmethod
¶
apply_to_sv(
sv: ndarray,
n: int,
model: str,
p: float,
rng: Optional[Generator] = None,
qubits: Optional[List[int]] = None,
jax_key: Optional[Any] = None,
) -> np.ndarray
Apply a stochastic Kraus channel to statevector sv in-place (numpy path) or via functional updates (JAX path).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sv
|
ndarray
|
|
required |
n
|
int
|
|
required |
model
|
str
|
|
required |
p
|
float
|
|
required |
rng
|
Optional[Generator]
|
|
None
|
qubits
|
Optional[List[int]]
|
|
None
|
jax_key
|
optional JAX PRNGKey, only meaningful when *sv* is a JAX
|
|
None
|
Returns:
| Type | Description |
|---|---|
Normalised statevector (same array type as input).
|
|
Examples:
A real quantum computer is never perfect -- every gate has some chance of error. Once you have a statevector from running your own QASM circuit (the same circuit as the getting-started example), this function is how you find out what a noisy device would have actually given you instead.
Start from the circuit and statevector you already have:
>>> import numpy as np
>>> import dense_evolution as de
>>> qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; creg c[2]; h q[0]; barrier q; cx q[0],q[1]; measure q -> c;'
>>> circuit = de.QASMParser().parse(qasm)
>>> sim = de.DenseSVSimulator(2)
>>> sim.run_circuit(circuit.to_tuples())
>>> sv = np.asarray(sim.get_statevector())
(the barrier is parsed and ignored -- it never becomes a gate tuple, so it
has no effect on the statevector, only on how the circuit reads.)
Call NoiseModel.apply_to_sv on that same statevector, telling it the
qubit count, which error model to simulate, and how strong it is:
>>> from dense_evolution.noise import NoiseModel
>>> rng = np.random.default_rng(0)
>>> sv_noisy = NoiseModel.apply_to_sv(sv.copy(), 2, 'depolarizing', 0.1, rng=rng)
>>> round(float(np.vdot(sv_noisy, sv_noisy).real), 4) # still a valid, normalised state
1.0
'depolarizing' above is one of six models; pick any other one the same way,
by name:
>>> NoiseModel.MODELS
['ideal', 'depolarizing', 'bitflip', 'phaseflip', 'amplitude_damping', 'combined']
p is that model's error probability (or damping rate for
'amplitude_damping') -- 0.1 above means each qubit has a 10% chance of a
random Pauli error per call. Run it many times and average (see
Density-matrix ZNE healing)
to see what a real noisy device's typical output looks like, not just one
random draw.
Source code in dense_evolution/noise/kraus_channels.py
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 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | |
kraus_description
staticmethod
¶
Human-readable Kraus-operator formula and physical meaning for
one of NoiseModel.MODELS.
Examples:
>>> from dense_evolution.noise import NoiseModel
>>> NoiseModel.kraus_description('bitflip')['physical']
'Bit flip σ_x with probability p'
Source code in dense_evolution/noise/kraus_channels.py
NoiseSpec ¶
Native JAX-differentiable representation of a noise configuration --
a real JAX PyTree, so noise parameters thread through jax.jit/jax.grad/
jax.vmap natively -- e.g. as the noise= argument to
circuit_to_energy_fn's energy_fn -- instead of being applied as an
external, Python-side step around the already-traced circuit (the old
way: build sv, exit the trace, call apply_to_sv separately).
model/qubits are static (aux_data): they select which code path
runs, not values to differentiate or batch over -- the same role
static_argnames plays for a plain jax.jit function, but automatic
here because it's part of the pytree structure. p/jax_key are
pytree leaves (children): p can be a traced/differentiable value
(e.g. optimizing noise strength itself), and jax_key flows through
jit/vmap/scan the way any other JAX array does -- no external
Python-level key management, no OS-entropy fallback (unlike
apply_to_sv called standalone with jax_key=None), so a NoiseSpec's
result is always reproducible from the key it was built with.
jax_key is required (not Optional) -- the whole point of wiring
noise into the traced computation this way is to remove the need for
an external, ad-hoc key-management workaround; a caller who wants a
fresh key per call should split one themselves (jax.random.split)
and build a fresh NoiseSpec, the same as any other JAX-idiomatic
stateless-key pattern.
Examples:
>>> import jax
>>> from dense_evolution.noise import NoiseSpec
>>> key = jax.random.PRNGKey(0)
>>> spec = NoiseSpec(model="depolarizing", p=0.05, jax_key=key, qubits=[0, 1])
>>> spec
NoiseSpec(model='depolarizing', p=0.05, qubits=(0, 1))
Source code in dense_evolution/noise/differentiable.py
apply_dark_theme ¶
Dashboard-only diagnostic-plot styling (dark background + GitHub-
dark-ish palette). Used to run as a plt.style.use('dark_background')
module-level side effect here, so it fired on ANY import
dense_evolution and silently recolored every matplotlib figure a
caller made afterward, dashboard or not (prog.txt point 2). Now
opt-in: call this explicitly from the dashboard's own startup.