dense-evolution 8.3.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. dashboard_core/__init__.py +115 -0
  2. dashboard_core/_gate_tables.py +30 -0
  3. dashboard_core/band_structure.py +71 -0
  4. dashboard_core/circuit_builder_component.py +232 -0
  5. dashboard_core/circuit_diagram.py +216 -0
  6. dashboard_core/crypto_protocols.py +77 -0
  7. dashboard_core/engine.py +326 -0
  8. dashboard_core/graphical_builder.py +114 -0
  9. dashboard_core/hamiltonians.py +593 -0
  10. dashboard_core/mass_decomposition_tool.py +47 -0
  11. dashboard_core/mitigation.py +343 -0
  12. dashboard_core/native_hf_diagnostics.py +62 -0
  13. dashboard_core/noise_tools.py +125 -0
  14. dashboard_core/qasm_library.py +233 -0
  15. dashboard_core/qmmm.py +16 -0
  16. dashboard_core/rag_tool.py +45 -0
  17. dashboard_core/state_visuals.py +288 -0
  18. dashboard_core/system_limits.py +60 -0
  19. dashboard_core/vector_healing.py +102 -0
  20. dashboard_core/visuals.py +158 -0
  21. dashboard_core/vqe.py +533 -0
  22. dashboard_core/wormhole.py +580 -0
  23. dense_evolution/__init__.py +114 -0
  24. dense_evolution/autodiff.py +10 -0
  25. dense_evolution/backends/__init__.py +5 -0
  26. dense_evolution/backends/chunk/__init__.py +37 -0
  27. dense_evolution/backends/chunk/_engine_imports.py +57 -0
  28. dense_evolution/backends/chunk/circuit_chunker.py +55 -0
  29. dense_evolution/backends/chunk/core.py +432 -0
  30. dense_evolution/backends/chunk/disk_overflow.py +232 -0
  31. dense_evolution/backends/chunk/geometry.py +95 -0
  32. dense_evolution/backends/chunk/guard.py +190 -0
  33. dense_evolution/backends/chunk/kernels.py +531 -0
  34. dense_evolution/backends/mps.py +1569 -0
  35. dense_evolution/backends/statevector.py +616 -0
  36. dense_evolution/chunk.py +25 -0
  37. dense_evolution/circuits/__init__.py +20 -0
  38. dense_evolution/circuits/compiler.py +488 -0
  39. dense_evolution/circuits/diagram.py +94 -0
  40. dense_evolution/circuits/gates.py +91 -0
  41. dense_evolution/circuits/parser.py +632 -0
  42. dense_evolution/circuits/qft.py +66 -0
  43. dense_evolution/circuits/random_circuit.py +85 -0
  44. dense_evolution/circuits/registry.py +74 -0
  45. dense_evolution/circuits/topology.py +79 -0
  46. dense_evolution/circuits/trotter.py +265 -0
  47. dense_evolution/circuits/uccsd.py +275 -0
  48. dense_evolution/cli.py +199 -0
  49. dense_evolution/compiler.py +9 -0
  50. dense_evolution/config.py +49 -0
  51. dense_evolution/drawing.py +10 -0
  52. dense_evolution/entropy.py +9 -0
  53. dense_evolution/fermions.py +9 -0
  54. dense_evolution/gates.py +9 -0
  55. dense_evolution/harrison_tb.py +16 -0
  56. dense_evolution/healing.py +18 -0
  57. dense_evolution/interop/__init__.py +18 -0
  58. dense_evolution/interop/qiskit_pennylane.py +406 -0
  59. dense_evolution/measurement.py +10 -0
  60. dense_evolution/mitigation/__init__.py +54 -0
  61. dense_evolution/mitigation/healing.py +215 -0
  62. dense_evolution/mitigation/kl_divergence.py +93 -0
  63. dense_evolution/mitigation/magic_entropy.py +163 -0
  64. dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
  65. dense_evolution/mitigation/renyi.py +168 -0
  66. dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
  67. dense_evolution/mitigation/zne.py +990 -0
  68. dense_evolution/mps.py +9 -0
  69. dense_evolution/native_hf/__init__.py +26 -0
  70. dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
  71. dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
  72. dense_evolution/native_hf/assembly.py +304 -0
  73. dense_evolution/native_hf/basis.py +117 -0
  74. dense_evolution/native_hf/boys.py +35 -0
  75. dense_evolution/native_hf/bridge.py +112 -0
  76. dense_evolution/native_hf/cartesian.py +64 -0
  77. dense_evolution/native_hf/coulomb.py +196 -0
  78. dense_evolution/native_hf/differentiable.py +53 -0
  79. dense_evolution/native_hf/gaussians.py +79 -0
  80. dense_evolution/native_hf/kinetic.py +52 -0
  81. dense_evolution/native_hf/libcint_bridge.py +167 -0
  82. dense_evolution/native_hf/overlap.py +91 -0
  83. dense_evolution/native_hf/scf.py +404 -0
  84. dense_evolution/noise/__init__.py +79 -0
  85. dense_evolution/noise/coherent_attack.py +264 -0
  86. dense_evolution/noise/cosmic_ray.py +61 -0
  87. dense_evolution/noise/density_matrix_channels.py +78 -0
  88. dense_evolution/noise/differentiable.py +66 -0
  89. dense_evolution/noise/kraus/__init__.py +6 -0
  90. dense_evolution/noise/kraus/amplitude_damping.py +47 -0
  91. dense_evolution/noise/kraus/bitflip.py +22 -0
  92. dense_evolution/noise/kraus/combined.py +16 -0
  93. dense_evolution/noise/kraus/depolarizing.py +47 -0
  94. dense_evolution/noise/kraus/ideal.py +10 -0
  95. dense_evolution/noise/kraus/phaseflip.py +21 -0
  96. dense_evolution/noise/kraus_channels.py +285 -0
  97. dense_evolution/noise/oscillating.py +32 -0
  98. dense_evolution/noise/pink.py +80 -0
  99. dense_evolution/observables.py +11 -0
  100. dense_evolution/parser.py +9 -0
  101. dense_evolution/physics/__init__.py +27 -0
  102. dense_evolution/physics/entropy.py +161 -0
  103. dense_evolution/physics/fermions.py +322 -0
  104. dense_evolution/physics/observables.py +523 -0
  105. dense_evolution/physics/qec.py +1113 -0
  106. dense_evolution/physics/spectral.py +143 -0
  107. dense_evolution/physics/states.py +43 -0
  108. dense_evolution/protocols/__init__.py +27 -0
  109. dense_evolution/protocols/bb84.py +133 -0
  110. dense_evolution/protocols/di_qkd_ghz.py +199 -0
  111. dense_evolution/protocols/dicka_protocol2.py +124 -0
  112. dense_evolution/qec.py +20 -0
  113. dense_evolution/qft.py +9 -0
  114. dense_evolution/qmmm/__init__.py +13 -0
  115. dense_evolution/qmmm/ase_bridge.py +97 -0
  116. dense_evolution/qmmm/forces.py +388 -0
  117. dense_evolution/qmmm/propagation.py +80 -0
  118. dense_evolution/qmmm/region.py +137 -0
  119. dense_evolution/random_circuit.py +15 -0
  120. dense_evolution/registry.py +9 -0
  121. dense_evolution/simulator.py +10 -0
  122. dense_evolution/solvers/__init__.py +19 -0
  123. dense_evolution/solvers/autodiff.py +169 -0
  124. dense_evolution/solvers/harrison_tb.py +189 -0
  125. dense_evolution/solvers/vhd_tb.py +187 -0
  126. dense_evolution/states.py +9 -0
  127. dense_evolution/topology.py +9 -0
  128. dense_evolution/trotter.py +9 -0
  129. dense_evolution/utils/__init__.py +13 -0
  130. dense_evolution/utils/drawing.py +101 -0
  131. dense_evolution/utils/mass_decomposition.py +246 -0
  132. dense_evolution/utils/measurement.py +94 -0
  133. dense_evolution/vhd_tb.py +16 -0
  134. dense_evolution-8.3.0.dist-info/METADATA +366 -0
  135. dense_evolution-8.3.0.dist-info/RECORD +165 -0
  136. dense_evolution-8.3.0.dist-info/WHEEL +5 -0
  137. dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
  138. dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
  139. dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
  140. ia_utils/__init__.py +0 -0
  141. ia_utils/adversarial_vector_attack.py +196 -0
  142. ia_utils/rag.py +288 -0
  143. ia_utils/vector_healing.py +399 -0
  144. local_site/__init__.py +0 -0
  145. local_site/app/__init__.py +0 -0
  146. local_site/app/server.py +1009 -0
  147. mcp_server/__init__.py +0 -0
  148. mcp_server/client.py +324 -0
  149. mcp_server/config.py +32 -0
  150. mcp_server/models.py +347 -0
  151. mcp_server/molecules.py +71 -0
  152. mcp_server/server.py +119 -0
  153. mcp_server/tools/__init__.py +0 -0
  154. mcp_server/tools/chemistry_tools.py +225 -0
  155. mcp_server/tools/circuit_tools.py +83 -0
  156. mcp_server/tools/crypto_tools.py +66 -0
  157. mcp_server/tools/mitigation_tools.py +81 -0
  158. mcp_server/tools/noise_tools.py +60 -0
  159. mcp_server/tools/retrieval_tools.py +44 -0
  160. mcp_server/tools/system_tools.py +149 -0
  161. mcp_server/tools/wormhole_tools.py +142 -0
  162. mcp_server/utils/__init__.py +0 -0
  163. mcp_server/utils/cache.py +55 -0
  164. mcp_server/utils/images.py +67 -0
  165. mcp_server/utils/truncation.py +38 -0
@@ -0,0 +1,264 @@
1
+ """Coherent, differentiable adversarial noise for stabilizer codes.
2
+
3
+ Promoted from Dense-Evolution-Discovery's Steane [[7,1,3]] investigation
4
+ (scripts/steane_code_block2_adversarial_noise.py and
5
+ steane_code_block3_linf_constrained_attack.py), generalized here from a
6
+ hard-coded 7-qubit Steane table to any stabilizer list, matching this
7
+ module's own `dense_evolution.qec.compute_syndrome` conventions (equal-
8
+ length Pauli strings over IXYZ, qubit q = character q).
9
+
10
+ Unlike `dense_evolution.registry.NoiseModel` (stochastic Kraus channels,
11
+ sampled via a hard threshold that is not usefully differentiable), this
12
+ module attacks a genuinely continuous channel: independent per-qubit
13
+ coherent over-rotations rz(delta_q) applied to all qubits at once --
14
+ the natural differentiable analogue of physical miscalibration. Because
15
+ it is JAX-differentiable end-to-end, `craft_adversarial_delta` can
16
+ gradient-search for a worst-case coherent-error direction instead of
17
+ only sampling random ones.
18
+
19
+ Honest negative result, kept because it is the actual finding, not
20
+ smoothed over: unconstrained (`craft_adversarial_delta`, L2-ball only)
21
+ PGD on `x_stabilizer_leakage` degenerately concentrates the whole error
22
+ budget on whichever single qubit is shared by every X-stabilizer
23
+ generator. A coherent rz error on one qubit alone always collapses to
24
+ weight-0 or weight-1, which a distance-3 code's decoder corrects
25
+ exactly every time -- so the "adversarial" direction found this way has
26
+ ZERO real decoder-failure rate, while random multi-qubit directions of
27
+ the same L2 budget fail readily. `craft_adversarial_delta_constrained`
28
+ fixes this by also capping the per-qubit angle (L-infinity), forcing
29
+ the search to spread the budget across multiple qubits -- the regime
30
+ that actually causes failures. Use the constrained version if the goal
31
+ is a genuine worst-case test; the unconstrained version is kept because
32
+ it is a real, useful example of a differentiable proxy objective being
33
+ anti-correlated with the thing it was meant to approximate.
34
+ """
35
+ import numpy as np
36
+ import jax
37
+ import jax.numpy as jnp
38
+
39
+ from dense_evolution.physics.qec import compute_syndrome
40
+
41
+ __all__ = [
42
+ "apply_rz_all",
43
+ "x_stabilizer_leakage",
44
+ "craft_adversarial_delta",
45
+ "project_l2_linf",
46
+ "craft_adversarial_delta_constrained",
47
+ "decoder_failure_rate",
48
+ "random_delta_failure_stats",
49
+ ]
50
+
51
+
52
+ def _flip_mask(pauli_str: str) -> int:
53
+ """Bitmask of qubits carrying an X or Y term, qubit q = bit (n-1-q) --
54
+ dense_evolution's own MSB-first statevector-index convention."""
55
+ n = len(pauli_str)
56
+ mask = 0
57
+ for q, p in enumerate(pauli_str):
58
+ if p in ('X', 'Y'):
59
+ mask |= 1 << (n - 1 - q)
60
+ return mask
61
+
62
+
63
+ def apply_rz_all(sv0: jnp.ndarray, delta: jnp.ndarray) -> jnp.ndarray:
64
+ """Coherent per-qubit rz(delta_q) applied to every qubit of `sv0` at
65
+ once. rz gates are diagonal and all commute, so this is exact
66
+ elementwise phase multiplication, not a per-gate circuit simulation
67
+ -- and fully JAX-differentiable in `delta`.
68
+
69
+ Parameters
70
+ ----------
71
+ sv0 : statevector, length 2**n_qubits
72
+ delta : real array, length n_qubits -- rz angle for each qubit
73
+
74
+ Examples
75
+ --------
76
+ >>> import numpy as np
77
+ >>> import jax.numpy as jnp
78
+ >>> sv0 = jnp.array([1.0, 0.0], dtype=jnp.complex128)
79
+ >>> sv1 = apply_rz_all(sv0, jnp.array([np.pi]))
80
+ >>> round(float(jnp.abs(sv1[0]) ** 2), 6)
81
+ 1.0
82
+ """
83
+ n = delta.shape[0]
84
+ dim = sv0.shape[0]
85
+ idx = jnp.arange(dim, dtype=jnp.int32)
86
+ bit_pos = jnp.arange(n - 1, -1, -1, dtype=jnp.int32)
87
+ bits = (idx[:, None] >> bit_pos[None, :]) & 1
88
+ s = (1 - 2 * bits).astype(jnp.float64)
89
+ phase_arg = -0.5 * jnp.sum(delta[None, :] * s, axis=1)
90
+ return sv0 * jnp.exp(1j * phase_arg)
91
+
92
+
93
+ def x_stabilizer_leakage(delta: jnp.ndarray, sv0: jnp.ndarray, stabilizers) -> jnp.ndarray:
94
+ """Total leakage of `sv0`, after a coherent `apply_rz_all(sv0, delta)`
95
+ perturbation, out of the +1 joint eigenspace of `stabilizers` --
96
+ sum_i (1 - <stabilizer_i>) / 2, one term per generator. A smooth,
97
+ bounded ([0, len(stabilizers)]) proxy for how much the coherent
98
+ error disturbs the syndrome; NOT the actual decoder failure
99
+ probability (see module docstring for the documented gap between
100
+ the two). `stabilizers` should be X-type generators to pair with an
101
+ rz (Z-type-diagonal) coherent error -- the same reasoning
102
+ `dense_evolution.qec.compute_syndrome` uses generically for any
103
+ Pauli-string stabilizer list.
104
+ """
105
+ sv = apply_rz_all(sv0, delta)
106
+ dim = sv0.shape[0]
107
+ idx = jnp.arange(dim, dtype=jnp.int32)
108
+ total = 0.0
109
+ for g in stabilizers:
110
+ mask = _flip_mask(g)
111
+ src = idx ^ mask
112
+ expectation = jnp.real(jnp.sum(jnp.conj(sv) * sv[src]))
113
+ total = total + (1.0 - expectation) / 2.0
114
+ return total
115
+
116
+
117
+ def craft_adversarial_delta(sv0: jnp.ndarray, stabilizers, epsilon: float,
118
+ n_steps: int = 150, step_size: float = 0.05, seed: int = 0):
119
+ """Gradient-ascent PGD on `x_stabilizer_leakage`, projected into the L2
120
+ epsilon-ball around delta=0 after every step. Returns (best_delta as
121
+ numpy, best_leakage, leakage_history).
122
+
123
+ See the module docstring: on a distance-3 code this unconstrained
124
+ search finds a direction with real decoder-failure rate 0 -- use
125
+ `craft_adversarial_delta_constrained` for a genuine worst-case test.
126
+ """
127
+ n_qubits = len(stabilizers[0])
128
+ leakage_grad = jax.grad(lambda d, sv: x_stabilizer_leakage(d, sv, stabilizers), argnums=0)
129
+
130
+ rng = np.random.default_rng(seed)
131
+ init_dir = rng.normal(size=n_qubits)
132
+ init_dir /= np.linalg.norm(init_dir)
133
+ init_norm = min(epsilon, 1e-2)
134
+ delta = jnp.array(init_dir * init_norm)
135
+
136
+ best_delta = delta
137
+ best_leakage = float(x_stabilizer_leakage(delta, sv0, stabilizers))
138
+ history = [best_leakage]
139
+
140
+ for _ in range(n_steps):
141
+ grad = leakage_grad(delta, sv0)
142
+ grad_norm = jnp.linalg.norm(grad)
143
+ step = jnp.where(grad_norm > 1e-12, grad / grad_norm, jnp.zeros_like(grad))
144
+ delta = delta + step_size * step
145
+ delta_norm = jnp.linalg.norm(delta)
146
+ delta = jnp.where(delta_norm > epsilon, delta / delta_norm * epsilon, delta)
147
+
148
+ current = float(x_stabilizer_leakage(delta, sv0, stabilizers))
149
+ history.append(current)
150
+ if current > best_leakage:
151
+ best_leakage = current
152
+ best_delta = delta
153
+
154
+ return np.asarray(best_delta), best_leakage, history
155
+
156
+
157
+ def project_l2_linf(y: np.ndarray, epsilon: float, linf_cap: float, n_bisect: int = 60) -> np.ndarray:
158
+ """Exact projection of `y` onto the intersection of an L2 ball of
159
+ radius `epsilon` and an L-infinity ball (box) of radius `linf_cap` --
160
+ not the same as clip-then-rescale, which can push coordinates back
161
+ outside the box. Box-clips first; if that's already inside the L2
162
+ ball it's the exact answer, otherwise bisects the Lagrange
163
+ multiplier on the L2 constraint until the clipped, rescaled point
164
+ lands exactly on the L2 boundary.
165
+
166
+ Examples
167
+ --------
168
+ >>> import numpy as np
169
+ >>> project_l2_linf(np.array([10.0, 0.0]), epsilon=1.0, linf_cap=5.0).round(4)
170
+ array([1., 0.])
171
+ >>> project_l2_linf(np.array([0.1, 0.1]), epsilon=1.0, linf_cap=0.05).round(4)
172
+ array([0.05, 0.05])
173
+ """
174
+ y = np.asarray(y, dtype=np.float64)
175
+ box = np.clip(y, -linf_cap, linf_cap)
176
+ if np.linalg.norm(box) <= epsilon + 1e-12:
177
+ return box
178
+ lo, hi = 0.0, 1e8
179
+ for _ in range(n_bisect):
180
+ mid = 0.5 * (lo + hi)
181
+ z = np.clip(y / (1.0 + mid), -linf_cap, linf_cap)
182
+ if np.linalg.norm(z) > epsilon:
183
+ lo = mid
184
+ else:
185
+ hi = mid
186
+ return np.clip(y / (1.0 + hi), -linf_cap, linf_cap)
187
+
188
+
189
+ def craft_adversarial_delta_constrained(sv0: jnp.ndarray, stabilizers, epsilon: float, linf_cap: float,
190
+ n_steps: int = 150, step_size: float = 0.05, seed: int = 0):
191
+ """Same PGD search as `craft_adversarial_delta`, but each step is
192
+ projected into the L2-epsilon-ball INTERSECTED with an L-infinity
193
+ box of radius `linf_cap` (via `project_l2_linf`) instead of the L2
194
+ ball alone. Capping the per-qubit angle forbids the degenerate
195
+ one-qubit-takes-everything solution and forces the search to spread
196
+ the budget across multiple qubits -- the regime that actually causes
197
+ real decoder failures (see module docstring)."""
198
+ n_qubits = len(stabilizers[0])
199
+ leakage = lambda d, sv: x_stabilizer_leakage(d, sv, stabilizers)
200
+ leakage_grad = jax.grad(leakage, argnums=0)
201
+
202
+ rng = np.random.default_rng(seed)
203
+ init_dir = rng.normal(size=n_qubits)
204
+ init_dir /= np.linalg.norm(init_dir)
205
+ init_norm = min(epsilon, 1e-2)
206
+ delta_np = project_l2_linf(init_dir * init_norm, epsilon, linf_cap)
207
+ delta = jnp.array(delta_np)
208
+
209
+ best_delta = delta
210
+ best_leakage = float(leakage(delta, sv0))
211
+ history = [best_leakage]
212
+
213
+ for _ in range(n_steps):
214
+ grad = leakage_grad(delta, sv0)
215
+ grad_norm = jnp.linalg.norm(grad)
216
+ step = jnp.where(grad_norm > 1e-12, grad / grad_norm, jnp.zeros_like(grad))
217
+ delta_raw = np.asarray(delta) + step_size * np.asarray(step)
218
+ delta_np = project_l2_linf(delta_raw, epsilon, linf_cap)
219
+ delta = jnp.array(delta_np)
220
+
221
+ current = float(leakage(delta, sv0))
222
+ history.append(current)
223
+ if current > best_leakage:
224
+ best_leakage = current
225
+ best_delta = delta
226
+
227
+ return np.asarray(best_delta), best_leakage, history
228
+
229
+
230
+ def decoder_failure_rate(delta_np: np.ndarray, sv0_np: np.ndarray, decode_fn,
231
+ n_trials: int, rng: np.random.Generator) -> float:
232
+ """Real, discrete evaluation of how often a coherent error `delta_np`
233
+ actually fools a decoder -- as opposed to `x_stabilizer_leakage`'s
234
+ smooth proxy. `decode_fn(sv_noisy, rng) -> sv_corrected` is any
235
+ projective-measurement-based decoder (e.g. built around
236
+ `dense_evolution.qec.compute_syndrome`); this function applies the
237
+ coherent error once, then calls `decode_fn` `n_trials` times (the
238
+ syndrome measurement that collapses the coherently-perturbed state
239
+ is itself stochastic, so repeated trials are meaningful even though
240
+ `delta_np` is fixed), and reports the fraction that fail to recover
241
+ `sv0_np` exactly."""
242
+ sv_delta = np.asarray(apply_rz_all(jnp.array(sv0_np), jnp.array(delta_np)))
243
+ n_fail = 0
244
+ for _ in range(n_trials):
245
+ sv_corrected = decode_fn(sv_delta.copy(), rng)
246
+ fidelity = np.abs(np.vdot(sv_corrected, sv0_np)) ** 2
247
+ if fidelity < 1.0 - 1e-6:
248
+ n_fail += 1
249
+ return n_fail / n_trials
250
+
251
+
252
+ def random_delta_failure_stats(sv0_np: np.ndarray, decode_fn, epsilon: float, n_qubits: int,
253
+ n_random: int, n_trials_each: int, rng: np.random.Generator) -> np.ndarray:
254
+ """Same evaluation as `decoder_failure_rate`, but over `n_random`
255
+ random directions of L2 norm `epsilon` instead of one crafted
256
+ `delta` -- the baseline `craft_adversarial_delta`'s result should be
257
+ compared against (see module docstring: the unconstrained crafted
258
+ direction can score WORSE than this random baseline)."""
259
+ rates = np.zeros(n_random)
260
+ for i in range(n_random):
261
+ d = rng.normal(size=n_qubits)
262
+ d = d / np.linalg.norm(d) * epsilon
263
+ rates[i] = decoder_failure_rate(d, sv0_np, decode_fn, n_trials_each, rng)
264
+ return rates
@@ -0,0 +1,61 @@
1
+ """A cosmic-ray/gamma-ray-induced quasiparticle burst, as a real
2
+ time-dependent noise-strength profile instead of a constant rate."""
3
+ import jax.numpy as jnp
4
+
5
+ __all__ = ["cosmic_ray_burst_profile"]
6
+
7
+
8
+ def cosmic_ray_burst_profile(time_us, baseline_gamma: float, ratio_intermediate: float = 2.5,
9
+ ratio_peak: float = 3.75, tau1_us: float = 3.0,
10
+ tau2_us: float = 300.0, tau_decay_ms: float = 25.0) -> jnp.ndarray:
11
+ """Time-dependent decay-probability profile for a cosmic-ray/gamma-ray-
12
+ induced quasiparticle burst: a two-stage rise (fast to
13
+ `ratio_intermediate`x baseline, slower to `ratio_peak`x baseline) times
14
+ a single-exponential recovery, generalized out of a fixed, paper-number
15
+ validation (Dense-Evolution-Discovery Experiment 34, reproducing
16
+ arXiv:2104.05219's real measured event on a 26-qubit chip).
17
+
18
+ Feed the result to `continuous_dissipative_evolve` alongside
19
+ `amplitude_damping_channel` (or any other single-time-varying-parameter
20
+ channel) to inject a realistic burst into any circuit or QEC study,
21
+ without re-deriving this shape by hand each time.
22
+
23
+ The default ratios/timescales are the paper's own real numbers -- see
24
+ Experiment 34's docstring for exactly which are paper-fitted (the 25ms
25
+ decay) versus chosen to match the paper's two described rise points
26
+ (tau1/tau2). All are overridable for a different event severity or
27
+ device generation; `baseline_gamma` is never derived here -- pass
28
+ whatever per-slice decay probability corresponds to your own dt/T1
29
+ convention (see Experiment 34 for one worked example of that
30
+ conversion).
31
+
32
+ Parameters
33
+ ----------
34
+ time_us : array_like
35
+ Time since impact, in microseconds (t=0 is the impact instant).
36
+ baseline_gamma : float
37
+ Undisturbed per-slice decay probability; this profile scales it
38
+ up, it does not derive it.
39
+ ratio_intermediate, ratio_peak : float
40
+ Multiplier on `baseline_gamma` at the two described checkpoints
41
+ (paper defaults 2.5=10/4, 3.75=15/4, from Fig. 3's ~10us/~1ms
42
+ readings).
43
+ tau1_us, tau2_us : float
44
+ Rise timescales for the two saturating-exponential stages (paper
45
+ defaults 3, 300 -- chosen to match its ~10us/~1ms descriptions,
46
+ not fitted by the paper itself).
47
+ tau_decay_ms : float
48
+ Recovery time constant (paper default 25 -- its own fitted central
49
+ value, real range 25-30ms across 415 events).
50
+
51
+ Returns
52
+ -------
53
+ jnp.ndarray
54
+ Per-slice decay probability at each entry of `time_us`.
55
+ """
56
+ time_us = jnp.asarray(time_us)
57
+ stage1 = (ratio_intermediate - 1.0) * (1.0 - jnp.exp(-time_us / tau1_us))
58
+ stage2 = (ratio_peak - ratio_intermediate) * (1.0 - jnp.exp(-time_us / tau2_us))
59
+ decay = jnp.exp(-time_us / (tau_decay_ms * 1000.0))
60
+ scaling = 1.0 + (stage1 + stage2) * decay
61
+ return baseline_gamma * scaling
@@ -0,0 +1,78 @@
1
+ """Noise applied directly to a density matrix, instead of a statevector --
2
+ what density-matrix ZNE (`dense_evolution.mitigation.zne_density_matrix`)
3
+ needs its noise ensemble built from."""
4
+ import jax.numpy as jnp
5
+
6
+ __all__ = ["global_depolarizing_channel", "amplitude_damping_channel", "phaseflip_channel_exact"]
7
+
8
+
9
+ def global_depolarizing_channel(rho: jnp.ndarray, p: float) -> jnp.ndarray:
10
+ """Global n-qubit depolarizing channel, D_p(rho) = (1-p)*rho + (p/dim)*I.
11
+
12
+ Distinct from `NoiseModel`'s `'depolarizing'` model, which applies an
13
+ independent PER-QUBIT local Kraus channel -- a different physical map
14
+ from this GLOBAL channel, which mixes the whole `dim`-dimensional state
15
+ toward the fully mixed state as one unit. Use this one when modeling
16
+ e.g. state-prep/measurement (SPAM) error reported as a single joint
17
+ depolarizing parameter over the whole register, not per-qubit gate
18
+ noise (promoted from a real reproduction of arXiv:2608.16716's own
19
+ SPAM model, Dense-Evolution-Discovery Experiment 33).
20
+ """
21
+ rho = jnp.asarray(rho, dtype=jnp.complex128)
22
+ dim = rho.shape[0]
23
+ identity = jnp.eye(dim, dtype=jnp.complex128)
24
+ return (1.0 - p) * rho + (p / dim) * identity
25
+
26
+
27
+ def amplitude_damping_channel(rho: jnp.ndarray, gamma: float) -> jnp.ndarray:
28
+ """Single-qubit amplitude-damping channel: E0 @ rho @ E0.conj().T +
29
+ E1 @ rho @ E1.conj().T, with E0=diag(1, sqrt(1-gamma)) and
30
+ E1=[[0,sqrt(gamma)],[0,0]] -- population only ever moves |1>->|0>,
31
+ never the reverse.
32
+
33
+ Distinct from `global_depolarizing_channel` (symmetric, mixes toward
34
+ the fully-mixed state regardless of which state is |1> or |0>) -- this
35
+ one is asymmetric by construction, the real signature of energy-relaxation
36
+ (T1) processes and of quasiparticle poisoning (promoted from a real
37
+ reproduction of arXiv:2104.05219's measured cosmic-ray-induced error
38
+ bursts, Dense-Evolution-Discovery Experiment 34, where this asymmetry is
39
+ exactly the mechanism's own reported signature: decay errors only, no
40
+ excess excitation errors).
41
+
42
+ Single-qubit only (rho must be 2x2) -- unlike `global_depolarizing_channel`,
43
+ this is not dimension-generic, since amplitude damping is inherently a
44
+ per-qubit process, not a joint-register one.
45
+ """
46
+ rho = jnp.asarray(rho, dtype=jnp.complex128)
47
+ e0 = jnp.array([[1.0, 0.0], [0.0, jnp.sqrt(1.0 - gamma)]], dtype=jnp.complex128)
48
+ e1 = jnp.array([[0.0, jnp.sqrt(gamma)], [0.0, 0.0]], dtype=jnp.complex128)
49
+ return e0 @ rho @ e0.conj().T + e1 @ rho @ e1.conj().T
50
+
51
+
52
+ def phaseflip_channel_exact(rho: jnp.ndarray, p: float) -> jnp.ndarray:
53
+ """Exact (zero-variance) multi-qubit phaseflip channel: K0=sqrt(1-p)*I,
54
+ K1=sqrt(p)*Z, applied independently per qubit across the whole
55
+ register, matching `NoiseModel`'s `'phaseflip'` statevector model
56
+ (`noise/kraus/phaseflip.py`) in the limit of infinitely many Monte
57
+ Carlo trials -- this function computes that limit directly instead of
58
+ averaging finite trials, for use as a zero-sampling-variance
59
+ "classical node" in Classically Augmented ZNE (Scheiber et al., "CA-ZNE:
60
+ high-noise Richardson nodes replaced by classically simulated
61
+ estimates", arXiv:2607.25746).
62
+
63
+ Since Z is diagonal in the computational basis, conjugating rho by Z_q
64
+ (Pauli-Z on qubit q, identity elsewhere) only flips the sign of
65
+ off-diagonal entries whose row and column disagree on bit q --
66
+ (Z_q rho Z_q)_ij = rho_ij * s_i * s_j, s_k = -1 if bit q of k else +1
67
+ -- so each qubit's channel (1-p)*rho + p*(Z_q rho Z_q) is one
68
+ elementwise multiply, no explicit operator ever built.
69
+ """
70
+ rho = jnp.asarray(rho, dtype=jnp.complex128)
71
+ dim = rho.shape[0]
72
+ n_qubits = dim.bit_length() - 1
73
+ idx = jnp.arange(dim)
74
+ for q in range(n_qubits):
75
+ sign = 1.0 - 2.0 * ((idx >> q) & 1)
76
+ flip_sign = jnp.outer(sign, sign)
77
+ rho = (1.0 - p) * rho + p * (flip_sign * rho)
78
+ return rho
@@ -0,0 +1,66 @@
1
+ from typing import Optional, List
2
+
3
+ import jax
4
+
5
+ __all__ = ["NoiseSpec"]
6
+
7
+
8
+ class NoiseSpec:
9
+ """Native JAX-differentiable representation of a noise configuration --
10
+ a real JAX PyTree, so noise parameters thread through jax.jit/jax.grad/
11
+ jax.vmap natively -- e.g. as the `noise=` argument to
12
+ circuit_to_energy_fn's energy_fn -- instead of being applied as an
13
+ external, Python-side step around the already-traced circuit (the old
14
+ way: build sv, exit the trace, call apply_to_sv separately).
15
+
16
+ `model`/`qubits` are static (aux_data): they select which code path
17
+ runs, not values to differentiate or batch over -- the same role
18
+ `static_argnames` plays for a plain jax.jit function, but automatic
19
+ here because it's part of the pytree structure. `p`/`jax_key` are
20
+ pytree leaves (children): `p` can be a traced/differentiable value
21
+ (e.g. optimizing noise strength itself), and `jax_key` flows through
22
+ jit/vmap/scan the way any other JAX array does -- no external
23
+ Python-level key management, no OS-entropy fallback (unlike
24
+ apply_to_sv called standalone with jax_key=None), so a NoiseSpec's
25
+ result is always reproducible from the key it was built with.
26
+
27
+ `jax_key` is required (not Optional) -- the whole point of wiring
28
+ noise into the traced computation this way is to remove the need for
29
+ an external, ad-hoc key-management workaround; a caller who wants a
30
+ fresh key per call should split one themselves (`jax.random.split`)
31
+ and build a fresh NoiseSpec, the same as any other JAX-idiomatic
32
+ stateless-key pattern.
33
+
34
+ Examples
35
+ --------
36
+ >>> import jax
37
+ >>> from dense_evolution.noise import NoiseSpec
38
+ >>> key = jax.random.PRNGKey(0)
39
+ >>> spec = NoiseSpec(model="depolarizing", p=0.05, jax_key=key, qubits=[0, 1])
40
+ >>> spec
41
+ NoiseSpec(model='depolarizing', p=0.05, qubits=(0, 1))
42
+ """
43
+ def __init__(self, model: str, p, jax_key, qubits: Optional[List[int]] = None):
44
+ self.model = model
45
+ self.p = p
46
+ self.jax_key = jax_key
47
+ self.qubits = tuple(qubits) if qubits is not None else None
48
+
49
+ def __repr__(self):
50
+ return f"NoiseSpec(model={self.model!r}, p={self.p!r}, qubits={self.qubits!r})"
51
+
52
+ def tree_flatten(self):
53
+ children = (self.p, self.jax_key)
54
+ aux_data = (self.model, self.qubits)
55
+ return children, aux_data
56
+
57
+ @classmethod
58
+ def tree_unflatten(cls, aux_data, children):
59
+ model, qubits = aux_data
60
+ p, jax_key = children
61
+ return cls(model, p, jax_key, qubits)
62
+
63
+
64
+ jax.tree_util.register_pytree_node(
65
+ NoiseSpec, NoiseSpec.tree_flatten, NoiseSpec.tree_unflatten
66
+ )
@@ -0,0 +1,6 @@
1
+ """One file per Kraus channel: `ideal`, `depolarizing`, `bitflip`,
2
+ `phaseflip`, `amplitude_damping`, `combined`. Each module exposes a single
3
+ `apply(sv_out, idx_0, idx_1, p, rng, key, is_jax) -> (sv_out, key)`
4
+ function; `dense_evolution.noise.kraus_channels.NoiseModel` is the shared
5
+ dispatcher (RNG/key setup, per-qubit loop, normalisation) that calls them.
6
+ """
@@ -0,0 +1,47 @@
1
+ """Amplitude-damping channel: K0=diag(1, sqrt(1-gamma)), K1=[[0,
2
+ sqrt(gamma)], [0, 0]]. T1 energy relaxation |1> -> |0> with rate gamma.
3
+
4
+ Single-trajectory ("quantum jump") unraveling: ONE decay/no-decay
5
+ decision per qubit per shot, using the Born-rule probability aggregated
6
+ over the WHOLE statevector, P(K1) = <psi|K1^dagger K1|psi> =
7
+ gamma * sum_i |v1[i]|^2 (summed over every branch of the other n-1
8
+ qubits, not per branch). If it fires, K1 collapses the entire qubit-q=1
9
+ branch onto q=0, preserving the other qubits' relative amplitudes
10
+ (v1[i]/norm, not flattened to unit phase per branch). Renormalizing by
11
+ the GLOBAL P(K1) (or 1-P(K1) for the no-decay branch), not a per-branch
12
+ norm, is what a correct sequential multi-qubit trajectory needs.
13
+ """
14
+ import numpy as np
15
+ import jax
16
+ import jax.numpy as jnp
17
+
18
+ __all__ = ["apply"]
19
+
20
+
21
+ def apply(sv_out, idx_0, idx_1, p, rng, key, is_jax):
22
+ gamma = float(np.clip(p, 0.0, 1.0))
23
+ if is_jax:
24
+ key, subkey = jax.random.split(key)
25
+ r = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0)
26
+ v0, v1 = sv_out[idx_0], sv_out[idx_1]
27
+ p1 = jnp.clip(gamma * jnp.sum(jnp.abs(v1) ** 2), 0.0, 1.0)
28
+ decay = r < p1
29
+ norm_decay = jnp.sqrt(jnp.maximum(p1, 1e-15))
30
+ norm_no_decay = jnp.sqrt(jnp.maximum(1.0 - p1, 1e-15))
31
+ new_v0 = jnp.where(decay, v1 * jnp.sqrt(gamma) / norm_decay, v0 / norm_no_decay)
32
+ new_v1 = jnp.where(decay, 0.0 + 0j, v1 * jnp.sqrt(1.0 - gamma) / norm_no_decay)
33
+ sv_out = sv_out.at[idx_0].set(new_v0)
34
+ sv_out = sv_out.at[idx_1].set(new_v1)
35
+ else:
36
+ r = rng.random()
37
+ v0, v1 = sv_out[idx_0].copy(), sv_out[idx_1].copy()
38
+ p1 = float(np.clip(gamma * np.sum(np.abs(v1) ** 2), 0.0, 1.0))
39
+ if r < p1:
40
+ norm_decay = np.sqrt(max(p1, 1e-15))
41
+ sv_out[idx_0] = v1 * np.sqrt(gamma) / norm_decay
42
+ sv_out[idx_1] = 0.0
43
+ else:
44
+ norm_no_decay = np.sqrt(max(1.0 - p1, 1e-15))
45
+ sv_out[idx_0] = v0 / norm_no_decay
46
+ sv_out[idx_1] = v1 * np.sqrt(1.0 - gamma) / norm_no_decay
47
+ return sv_out, key
@@ -0,0 +1,22 @@
1
+ """Bit-flip channel: K0=sqrt(1-p)I, K1=sqrt(p)X. X applied with
2
+ probability p, once per qubit per shot, uniformly across the whole
3
+ statevector."""
4
+ import jax
5
+ import jax.numpy as jnp
6
+
7
+ __all__ = ["apply"]
8
+
9
+
10
+ def apply(sv_out, idx_0, idx_1, p, rng, key, is_jax):
11
+ if is_jax:
12
+ key, subkey = jax.random.split(key)
13
+ r = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0)
14
+ fire = r < p
15
+ v0, v1 = sv_out[idx_0], sv_out[idx_1]
16
+ sv_out = sv_out.at[idx_0].set(jnp.where(fire, v1, v0))
17
+ sv_out = sv_out.at[idx_1].set(jnp.where(fire, v0, v1))
18
+ else:
19
+ r = rng.random()
20
+ if r < p:
21
+ sv_out[idx_0], sv_out[idx_1] = sv_out[idx_1].copy(), sv_out[idx_0].copy()
22
+ return sv_out, key
@@ -0,0 +1,16 @@
1
+ """Combined channel: depolarizing(p/2) then amplitude_damping(p/3),
2
+ applied sequentially on the same qubit -- a worst-case NISQ mixture
3
+ (dephasing + relaxation). Reuses `depolarizing.apply` and
4
+ `amplitude_damping.apply` directly rather than duplicating their logic,
5
+ each called with its own scaled probability."""
6
+ from . import depolarizing, amplitude_damping
7
+
8
+ __all__ = ["apply"]
9
+
10
+
11
+ def apply(sv_out, idx_0, idx_1, p, rng, key, is_jax):
12
+ p_dep = p * 0.5
13
+ p_damp = p * 0.333333
14
+ sv_out, key = depolarizing.apply(sv_out, idx_0, idx_1, p_dep, rng, key, is_jax)
15
+ sv_out, key = amplitude_damping.apply(sv_out, idx_0, idx_1, p_damp, rng, key, is_jax)
16
+ return sv_out, key
@@ -0,0 +1,47 @@
1
+ """Depolarizing channel: K0=sqrt(1-p)I, K1=sqrt(p/3)X, K2=sqrt(p/3)Y,
2
+ K3=sqrt(p/3)Z. Isotropic Pauli error -- equiprobable X, Y, Z given that
3
+ the channel fires at all.
4
+
5
+ One fire/no-fire decision per qubit per shot (probability p), then --
6
+ only if it fired -- one equiprobable Pauli choice among X/Y/Z. The
7
+ 1-in-3 choice thresholds are fixed at 1/3 and 2/3 regardless of p: p
8
+ only gates whether an error happens at all, not which Pauli it is.
9
+ """
10
+ import jax
11
+ import jax.numpy as jnp
12
+
13
+ __all__ = ["apply"]
14
+
15
+ _THIRD = 1.0 / 3.0
16
+
17
+
18
+ def apply(sv_out, idx_0, idx_1, p, rng, key, is_jax):
19
+ if is_jax:
20
+ key, sk1, sk2 = jax.random.split(key, 3)
21
+ r = jax.random.uniform(sk1, shape=(), minval=0.0, maxval=1.0)
22
+ ch = jax.random.uniform(sk2, shape=(), minval=0.0, maxval=1.0)
23
+ fire = r < p
24
+ x_gate = fire & (ch < _THIRD)
25
+ y_gate = fire & (ch >= _THIRD) & (ch < 2.0 * _THIRD)
26
+ z_gate = fire & (ch >= 2.0 * _THIRD)
27
+ v0, v1 = sv_out[idx_0], sv_out[idx_1]
28
+ new_v0 = jnp.where(x_gate, v1,
29
+ jnp.where(y_gate, -1j * v1, v0))
30
+ new_v1 = jnp.where(x_gate, v0,
31
+ jnp.where(y_gate, 1j * v0,
32
+ jnp.where(z_gate, -v1, v1)))
33
+ sv_out = sv_out.at[idx_0].set(new_v0)
34
+ sv_out = sv_out.at[idx_1].set(new_v1)
35
+ else:
36
+ r = rng.random()
37
+ ch = rng.random()
38
+ if r < p:
39
+ if ch < _THIRD:
40
+ sv_out[idx_0], sv_out[idx_1] = sv_out[idx_1].copy(), sv_out[idx_0].copy()
41
+ elif ch < 2.0 * _THIRD:
42
+ v0, v1 = sv_out[idx_0].copy(), sv_out[idx_1].copy()
43
+ sv_out[idx_0] = -1j * v1
44
+ sv_out[idx_1] = 1j * v0
45
+ else:
46
+ sv_out[idx_1] = -sv_out[idx_1]
47
+ return sv_out, key
@@ -0,0 +1,10 @@
1
+ """Identity channel -- no noise. K0 = I."""
2
+
3
+ __all__ = ["apply"]
4
+
5
+
6
+ def apply(sv_out, idx_0, idx_1, p, rng, key, is_jax):
7
+ """No-op: returns `(sv_out, key)` unchanged. Kept as its own channel
8
+ (rather than a special case elsewhere) so `NoiseModel.MODELS` has one
9
+ real implementation per entry, `'ideal'` included."""
10
+ return sv_out, key
@@ -0,0 +1,21 @@
1
+ """Phase-flip channel: K0=sqrt(1-p)I, K1=sqrt(p)Z. Z applied with
2
+ probability p, once per qubit per shot: Z|0> = |0> (idx_0 amplitudes
3
+ untouched), Z|1> = -|1> (idx_1 amplitudes negated when fired)."""
4
+ import jax
5
+ import jax.numpy as jnp
6
+
7
+ __all__ = ["apply"]
8
+
9
+
10
+ def apply(sv_out, idx_0, idx_1, p, rng, key, is_jax):
11
+ if is_jax:
12
+ key, subkey = jax.random.split(key)
13
+ r = jax.random.uniform(subkey, shape=(), minval=0.0, maxval=1.0)
14
+ fire = r < p
15
+ v1 = sv_out[idx_1]
16
+ sv_out = sv_out.at[idx_1].set(jnp.where(fire, -v1, v1))
17
+ else:
18
+ r = rng.random()
19
+ if r < p:
20
+ sv_out[idx_1] = -sv_out[idx_1]
21
+ return sv_out, key