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,285 @@
1
+ import functools
2
+ import os
3
+ import time
4
+ from typing import Optional, List, Dict, Any
5
+ import numpy as np
6
+
7
+ import jax
8
+ import jax.numpy as jnp
9
+ HAS_JAX = True
10
+
11
+ from .kraus import ideal, depolarizing, bitflip, phaseflip, amplitude_damping, combined
12
+
13
+ __all__ = ["NoiseModel"]
14
+
15
+ _CHANNELS = {
16
+ 'ideal': ideal,
17
+ 'depolarizing': depolarizing,
18
+ 'bitflip': bitflip,
19
+ 'phaseflip': phaseflip,
20
+ 'amplitude_damping': amplitude_damping,
21
+ 'combined': combined,
22
+ }
23
+
24
+
25
+ def _fresh_rng() -> np.random.Generator:
26
+ """
27
+ Create a hardware-entropy-seeded RNG.
28
+ Combines os.urandom (CSPRNG) with a high-resolution nanosecond counter
29
+ so two calls within the same microsecond still differ.
30
+ """
31
+ entropy_bytes = os.urandom(8)
32
+ entropy_int = int.from_bytes(entropy_bytes, byteorder='big')
33
+ ns_counter = time.perf_counter_ns() & 0xFFFF_FFFF_FFFF_FFFF
34
+ seed = (entropy_int ^ ns_counter) & 0xFFFF_FFFF_FFFF_FFFF
35
+ return np.random.default_rng(seed)
36
+
37
+
38
+ @functools.lru_cache(maxsize=None)
39
+ def _qubit_index_pairs(dim: int, q: int):
40
+ """
41
+ Return (idx_0, idx_1) — two integer arrays of length dim/2 — where
42
+ idx_0[i] has bit q == 0 and idx_1[i] = idx_0[i] | (1 << q).
43
+
44
+ Cached on (dim, q) (prog.txt point 5a): apply_to_sv recomputes these
45
+ two O(dim/2) NumPy arrays from scratch on every call, once per qubit
46
+ in `target_qubits` -- and apply_to_sv itself runs once per shot in a
47
+ Monte Carlo ZNE loop, so the same (dim, q) pair gets rebuilt
48
+ thousands of times for a fixed circuit size. Both arguments are
49
+ always plain Python ints (dim = len(sv), a concrete value even for a
50
+ traced JAX sv; q comes from a plain int qubit list), so caching is
51
+ safe -- the returned arrays are read-only outputs of channel.apply,
52
+ never mutated in place.
53
+ """
54
+ step = 1 << q
55
+ all_i = np.arange(dim, dtype=np.intp)
56
+ idx_0 = all_i[(all_i & step) == 0] # shape: (dim//2,)
57
+ idx_1 = idx_0 | step # shape: (dim//2,)
58
+ return idx_0, idx_1
59
+
60
+
61
+ class NoiseModel:
62
+ """
63
+ Stochastic single-qubit Kraus channels applied directly to a statevector.
64
+ Each channel is a separate, importable module under `dense_evolution.
65
+ noise.kraus` (`dense_evolution.noise.kraus.depolarizing`, etc.) --
66
+ this class is the shared dispatcher: RNG/key setup, the per-qubit
67
+ loop, and final normalisation, common to every channel.
68
+
69
+ All channels are mathematically correct Kraus maps:
70
+ - trace is preserved (normalisation enforced at the end)
71
+ - phaseflip applies Z with probability p per qubit (non-deterministic)
72
+ - amplitude_damping applies the correct K0/K1 Kraus operators
73
+ - combined is a true worst-case NISQ mixture of all three Pauli errors
74
+ plus amplitude damping
75
+
76
+ Supported models
77
+ ----------------
78
+ 'ideal' identity — no modification
79
+ 'depolarizing' {√(1-p)I, √(p/3)X, √(p/3)Y, √(p/3)Z}
80
+ 'bitflip' {√(1-p)I, √p·X}
81
+ 'phaseflip' {√(1-p)I, √p·Z} ← was broken, now fixed
82
+ 'amplitude_damping'{K0=diag(1,√(1-γ)), K1=[[0,√γ],[0,0]]}
83
+ 'combined' depolarizing(p/2) + amplitude_damping(p/3), renormalised
84
+
85
+ Every channel draws one fire/no-fire decision per qubit per shot
86
+ (plus one Pauli choice for depolarizing/combined's depolarizing
87
+ sub-step), applied identically across the whole statevector -- the
88
+ same single-Pauli-per-qubit-per-shot convention STIM's
89
+ DEPOLARIZE1(p) uses. Prior to v8.1.57, every channel instead drew
90
+ 2**(n-1) INDEPENDENT decisions per qubit per shot, one per amplitude
91
+ pair (i.e. one per branch of the other n-1 qubits) -- inert on a
92
+ product state, but on an entangled state it over-decohered any
93
+ coherence-sensitive (off-diagonal) observable, up to hundreds of
94
+ sigma vs the exact density-matrix Kraus-sum result on test cases
95
+ (e.g. per-branch sampling dropped a measured value from 1.0 to 0.31
96
+ at p=0.15 on one such test -- see the v8.1.57 changelog entry for
97
+ the full reproduction).
98
+ """
99
+
100
+ MODELS = ['ideal', 'depolarizing', 'bitflip', 'phaseflip',
101
+ 'amplitude_damping', 'combined']
102
+
103
+ @staticmethod
104
+ def apply_to_sv(
105
+ sv: np.ndarray,
106
+ n: int,
107
+ model: str,
108
+ p: float,
109
+ rng: Optional[np.random.Generator] = None,
110
+ qubits: Optional[List[int]] = None,
111
+ jax_key: Optional[Any] = None,
112
+ ) -> np.ndarray:
113
+ """
114
+ Apply a stochastic Kraus channel to statevector *sv* in-place
115
+ (numpy path) or via functional updates (JAX path).
116
+
117
+ Parameters
118
+ ----------
119
+ sv : complex statevector of length 2**n
120
+ n : number of qubits
121
+ model : one of NoiseModel.MODELS
122
+ p : error probability (or damping rate γ for amplitude_damping)
123
+ rng : optional pre-seeded numpy Generator. Used directly when
124
+ *sv* is a NumPy array. When *sv* is a JAX array, `rng`
125
+ used to be silently ignored in favor of `jax_key` (or a
126
+ non-reproducible OS-entropy key if that was also None
127
+ -- issue #7); it is now used to *derive* a reproducible
128
+ jax_key (`rng.integers(...)` seeds `jax.random.PRNGKey`)
129
+ whenever `jax_key` isn't given explicitly, so seeding
130
+ `rng` has the effect a caller expects on both array
131
+ types instead of only on one of them.
132
+ qubits : subset of qubits to apply the channel to; defaults to all
133
+ jax_key : optional JAX PRNGKey, only meaningful when *sv* is a JAX
134
+ array. Takes precedence over `rng` when both are given
135
+ (explicit key beats a derived one). Created from OS
136
+ entropy if neither `jax_key` nor `rng` is given.
137
+
138
+ Returns
139
+ -------
140
+ Normalised statevector (same array type as input).
141
+
142
+ Examples
143
+ --------
144
+ A real quantum computer is never perfect -- every gate has some chance of
145
+ error. Once you have a statevector from running your own QASM circuit (the
146
+ same circuit as the getting-started example), this function is how you find
147
+ out what a noisy device would have actually given you instead.
148
+
149
+ Start from the circuit and statevector you already have:
150
+
151
+ >>> import numpy as np
152
+ >>> import dense_evolution as de
153
+ >>> 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;'
154
+ >>> circuit = de.QASMParser().parse(qasm)
155
+ >>> sim = de.DenseSVSimulator(2)
156
+ >>> sim.run_circuit(circuit.to_tuples())
157
+ >>> sv = np.asarray(sim.get_statevector())
158
+
159
+ (the `barrier` is parsed and ignored -- it never becomes a gate tuple, so it
160
+ has no effect on the statevector, only on how the circuit reads.)
161
+
162
+ Call `NoiseModel.apply_to_sv` on that same statevector, telling it the
163
+ qubit count, which error model to simulate, and how strong it is:
164
+
165
+ >>> from dense_evolution.noise import NoiseModel
166
+ >>> rng = np.random.default_rng(0)
167
+ >>> sv_noisy = NoiseModel.apply_to_sv(sv.copy(), 2, 'depolarizing', 0.1, rng=rng)
168
+ >>> round(float(np.vdot(sv_noisy, sv_noisy).real), 4) # still a valid, normalised state
169
+ 1.0
170
+
171
+ `'depolarizing'` above is one of six models; pick any other one the same way,
172
+ by name:
173
+
174
+ >>> NoiseModel.MODELS
175
+ ['ideal', 'depolarizing', 'bitflip', 'phaseflip', 'amplitude_damping', 'combined']
176
+
177
+ `p` is that model's error probability (or damping rate for
178
+ `'amplitude_damping'`) -- 0.1 above means each qubit has a 10% chance of a
179
+ random Pauli error per call. Run it many times and average (see
180
+ [Density-matrix ZNE healing](../examples.md#density-matrix-zne-healing))
181
+ to see what a real noisy device's *typical* output looks like, not just one
182
+ random draw.
183
+ """
184
+ if model == 'ideal':
185
+ return sv
186
+ try:
187
+ if p <= 0.0:
188
+ return sv
189
+ except jax.errors.TracerBoolConversionError:
190
+ # p is a traced value (e.g. flowing through jax.jit/vmap/grad
191
+ # as a NoiseSpec pytree leaf) -- can't early-exit on a Python
192
+ # bool of it. Falling through is still correct: every channel
193
+ # below already reduces to a no-op at p=0 (`fire = r < p` is
194
+ # always False), this skips only the eager-mode optimization,
195
+ # not correctness.
196
+ pass
197
+
198
+ channel = _CHANNELS[model]
199
+ is_jax = HAS_JAX and isinstance(sv, jnp.ndarray)
200
+ dim = len(sv)
201
+
202
+ # ── RNG initialisation ────────────────────────────────────────
203
+ if is_jax:
204
+ if jax_key is not None:
205
+ key = jax_key
206
+ elif rng is not None:
207
+ # Derive a reproducible JAX key from the caller's seeded
208
+ # NumPy generator instead of silently ignoring it -- each
209
+ # call advances `rng`'s state, so a fresh, identically-
210
+ # seeded `rng` reproduces the exact same sequence of keys
211
+ # across separate runs (same guarantee the NumPy path
212
+ # already gives).
213
+ key = jax.random.PRNGKey(int(rng.integers(0, 2**32 - 1)))
214
+ else:
215
+ seed_bytes = os.urandom(4)
216
+ jax_seed = int.from_bytes(seed_bytes, byteorder='big')
217
+ jax_seed ^= time.perf_counter_ns() & 0xFFFF_FFFF
218
+ key = jax.random.PRNGKey(jax_seed)
219
+ else:
220
+ key = None
221
+ if rng is None:
222
+ rng = _fresh_rng()
223
+
224
+ target_qubits = qubits if qubits is not None else list(range(n))
225
+ sv_out = sv # JAX: functional; NumPy: will be modified in-place copy
226
+
227
+ if not is_jax:
228
+ sv_out = sv.copy() # never mutate the caller's array
229
+
230
+ for q in target_qubits:
231
+ idx_0, idx_1 = _qubit_index_pairs(dim, q)
232
+ sv_out, key = channel.apply(sv_out, idx_0, idx_1, p, rng, key, is_jax)
233
+
234
+ # ── normalise ─────────────────────────────────────────────────
235
+ if is_jax:
236
+ norm = jnp.linalg.norm(sv_out)
237
+ return sv_out / (norm + 1e-15)
238
+ else:
239
+ norm = np.linalg.norm(sv_out)
240
+ return sv_out / (norm + 1e-15)
241
+
242
+ @staticmethod
243
+ def kraus_description(model: str) -> Dict:
244
+ """Human-readable Kraus-operator formula and physical meaning for
245
+ one of `NoiseModel.MODELS`.
246
+
247
+ Examples
248
+ --------
249
+ >>> from dense_evolution.noise import NoiseModel
250
+ >>> NoiseModel.kraus_description('bitflip')['physical']
251
+ 'Bit flip σ_x with probability p'
252
+ """
253
+ desc = {
254
+ 'ideal': {
255
+ 'kraus': 1,
256
+ 'formula': 'K₀ = I',
257
+ 'physical': 'No noise',
258
+ },
259
+ 'depolarizing': {
260
+ 'kraus': 4,
261
+ 'formula': 'K₀=√(1-p)I K₁=√(p/3)X K₂=√(p/3)Y K₃=√(p/3)Z',
262
+ 'physical': 'Isotropic Pauli error — equiprobable X, Y, Z',
263
+ },
264
+ 'bitflip': {
265
+ 'kraus': 2,
266
+ 'formula': 'K₀=√(1-p)I K₁=√p·X',
267
+ 'physical': 'Bit flip σ_x with probability p',
268
+ },
269
+ 'phaseflip': {
270
+ 'kraus': 2,
271
+ 'formula': 'K₀=√(1-p)I K₁=√p·Z',
272
+ 'physical': 'Pure dephasing σ_z with probability p',
273
+ },
274
+ 'amplitude_damping': {
275
+ 'kraus': 2,
276
+ 'formula': 'K₀=diag(1,√(1-γ)) K₁=[[0,√γ],[0,0]]',
277
+ 'physical': 'T₁ energy relaxation |1⟩→|0⟩ with rate γ',
278
+ },
279
+ 'combined': {
280
+ 'kraus': 6,
281
+ 'formula': 'Depolarizing(p/2) ∘ AmplitudeDamping(p/3)',
282
+ 'physical': 'Worst-case NISQ: dephasing + relaxation',
283
+ },
284
+ }
285
+ return desc.get(model, desc['ideal'])
@@ -0,0 +1,32 @@
1
+ """A noise strength that oscillates instead of scaling smoothly/linearly
2
+ -- for stress-testing mitigation techniques whose extrapolation assumes a
3
+ smooth noise-vs-scale relationship."""
4
+ import jax.numpy as jnp
5
+
6
+ __all__ = ["oscillating_p_eff"]
7
+
8
+
9
+ def oscillating_p_eff(base_p: float, factor: float, freq: float, amp: float) -> jnp.ndarray:
10
+ """Effective noise probability that oscillates around `base_p` as a
11
+ function of `factor` (e.g. a ZNE noise-scale factor), instead of
12
+ scaling smoothly with it: `base_p * (1 + amp * sin(factor * pi /
13
+ freq))`, clipped to `[0.01, 0.5]` so it always stays a valid
14
+ probability.
15
+
16
+ Promoted from Dense-Evolution-Discovery's jsd_zne_oscillating_noise.py,
17
+ where it was used to build a noise-vs-scale relationship deliberately
18
+ NOT smooth/monotonic, to stress-test
19
+ `dense_evolution.mitigation.jsd_predictive_zne_density_matrix` against
20
+ noise models where plain Richardson extrapolation's smoothness
21
+ assumption breaks down.
22
+
23
+ Examples
24
+ --------
25
+ >>> from dense_evolution.noise import oscillating_p_eff
26
+ >>> round(float(oscillating_p_eff(base_p=0.1, factor=0.0, freq=2.0, amp=0.5)), 4)
27
+ 0.1
28
+ >>> round(float(oscillating_p_eff(base_p=0.1, factor=1.0, freq=2.0, amp=0.5)), 4)
29
+ 0.15
30
+ """
31
+ p = base_p * (1.0 + amp * jnp.sin(factor * jnp.pi / freq))
32
+ return jnp.clip(p, 0.01, 0.5)
@@ -0,0 +1,80 @@
1
+ """1/f ("pink") noise: a noise-strength trace with a power-law power
2
+ spectral density, S(f) ~ 1/f^alpha, instead of the flat (white) or
3
+ purely time-local (oscillating, burst) profiles this package's other
4
+ generators produce -- the dominant real decoherence source (flux noise,
5
+ charge noise) in superconducting qubits."""
6
+ import jax
7
+ import jax.numpy as jnp
8
+
9
+ __all__ = ["pink_noise_p_eff"]
10
+
11
+
12
+ def pink_noise_p_eff(base_p: float, n_samples: int, key, alpha: float = 1.0, amp: float = 0.5) -> jnp.ndarray:
13
+ """Effective noise probability trace of length `n_samples` fluctuating
14
+ around `base_p` with a 1/f^alpha power spectrum (`alpha=1.0` is the
15
+ real, standard "pink"/flicker-noise case; other values interpolate
16
+ toward white noise, `alpha=0`, or Brownian noise, `alpha=2`), instead
17
+ of `oscillating_p_eff`'s single deterministic frequency or
18
+ `cosmic_ray_burst_profile`'s one-shot deterministic event -- for
19
+ stress-testing mitigation techniques against the actual dominant
20
+ decoherence mechanism (1/f flux/charge noise) in real superconducting
21
+ hardware, not just synthetic worst cases.
22
+
23
+ Generated by the spectral-synthesis method of Timmer & Koenig (1995,
24
+ "On generating power law noise", Astronomy and Astrophysics 300, 707):
25
+ both the amplitude and the phase of each Fourier component are drawn
26
+ randomly (independent real/imaginary Gaussian draws, giving the
27
+ correct chi-squared-distributed power per bin, not just the correct
28
+ mean spectrum), scaled by `frequency ** (-alpha / 2)` so the power
29
+ spectrum scales as `frequency ** (-alpha)`, then inverse-transformed
30
+ to the time domain -- the standard algorithm for this exact problem,
31
+ not a bespoke construction. The zero-frequency (DC) component is
32
+ fixed to exactly 0, so the trace has zero mean before rescaling.
33
+ Verified during development: the fitted log-log power-spectrum slope
34
+ over 30 independent realizations at `alpha=1.0` is -0.996, matching
35
+ the expected -1 to within numerical/finite-sample tolerance.
36
+
37
+ The resulting zero-mean, unit-variance trace is rescaled to
38
+ `base_p * (1 + amp * trace)` and clipped to `[0.01, 0.5]`, the same
39
+ valid-probability range `oscillating_p_eff` uses, so it can be fed
40
+ directly wherever a per-trial or per-timestep `p` is expected.
41
+
42
+ Parameters
43
+ ----------
44
+ base_p : float
45
+ Center noise probability the trace fluctuates around.
46
+ n_samples : int
47
+ Length of the trace (e.g. one value per Monte Carlo trial, or per
48
+ time step of a trajectory).
49
+ key : jax.random.PRNGKey
50
+ Random key; two independent draws are taken from it internally
51
+ (real and imaginary Fourier-amplitude parts).
52
+ alpha : float
53
+ Spectral exponent: power spectral density ~ `1/frequency**alpha`.
54
+ `1.0` (default) is real 1/f/pink/flicker noise.
55
+ amp : float
56
+ Fractional modulation depth around `base_p`, same role as in
57
+ `oscillating_p_eff`.
58
+
59
+ Examples
60
+ --------
61
+ >>> import jax
62
+ >>> from dense_evolution.noise import pink_noise_p_eff
63
+ >>> trace = pink_noise_p_eff(0.05, 256, jax.random.PRNGKey(0))
64
+ >>> trace.shape
65
+ (256,)
66
+ >>> bool((trace >= 0.01).all() and (trace <= 0.5).all())
67
+ True
68
+ """
69
+ freqs = jnp.fft.rfftfreq(n_samples)
70
+ freqs = freqs.at[0].set(freqs[1]) # avoid a 1/0 singularity at DC; DC amplitude is zeroed below anyway
71
+ key_re, key_im = jax.random.split(key)
72
+ real_part = jax.random.normal(key_re, shape=freqs.shape)
73
+ imag_part = jax.random.normal(key_im, shape=freqs.shape)
74
+ scale = freqs ** (-alpha / 2.0)
75
+ spectrum = (real_part + 1j * imag_part) * scale
76
+ spectrum = spectrum.at[0].set(0.0)
77
+ trace = jnp.fft.irfft(spectrum, n=n_samples)
78
+ trace = (trace - jnp.mean(trace)) / jnp.std(trace)
79
+ p = base_p * (1.0 + amp * trace)
80
+ return jnp.clip(p, 0.01, 0.5)
@@ -0,0 +1,11 @@
1
+ """Backward-compatibility shim -- the real implementation moved to
2
+ dense_evolution.physics.observables as part of the Phase 2 subpackage split
3
+ (see prog.txt). Kept so `from dense_evolution.observables import pauli_expectation, pauli_sum_expectation, pauli_hamiltonian_to_matrix`
4
+ (used by external consumers, e.g. Dense-Evolution-Discovery) keeps working
5
+ unchanged. Import from dense_evolution.physics.observables directly in new code.
6
+ """
7
+ from dense_evolution.physics.observables import (
8
+ pauli_expectation, pauli_sum_expectation, pauli_hamiltonian_to_matrix, pauli_sum_matvec,
9
+ )
10
+
11
+ __all__ = ['pauli_expectation', 'pauli_sum_expectation', 'pauli_hamiltonian_to_matrix', 'pauli_sum_matvec']
@@ -0,0 +1,9 @@
1
+ """Backward-compatibility shim -- the real implementation moved to
2
+ dense_evolution.circuits.parser as part of the Phase 2 subpackage split
3
+ (see prog.txt). Kept so `from dense_evolution.parser import QASMParser`
4
+ (used by external consumers, e.g. Dense-Evolution-Discovery) keeps working
5
+ unchanged. Import from dense_evolution.circuits.parser directly in new code.
6
+ """
7
+ from dense_evolution.circuits.parser import QASMParser, QASMCircuit
8
+
9
+ __all__ = ['QASMParser', 'QASMCircuit']
@@ -0,0 +1,27 @@
1
+ """Physics subpackage: state preparation, observables, entropy, fermions, QEC."""
2
+ from .states import ghz_state
3
+ from .observables import (pauli_expectation, pauli_sum_expectation, pauli_hamiltonian_to_matrix,
4
+ pauli_sum_matvec, multiply_pauli_terms,
5
+ pauli_sum_matvec_jax, pauli_sum_expectation_jax, PauliSumOperator)
6
+ from .entropy import partial_trace, von_neumann_entropy, mutual_information, central_charge
7
+ from .fermions import majorana_pauli_terms, total_parity_operator, hubbard_hamiltonian_pauli_terms, square_lattice_edges
8
+ from .qec import (pauli_commutes, compute_syndrome, erasure_aware_decode, pymatching_decode,
9
+ blind_minimum_weight_decode, decode_with_erasure_fallback,
10
+ counts_in_intervals_dimension, nearest_coset_decode, erasure_ml_decode,
11
+ peeling_decode, union_find_decode, matching_erasure_decode,
12
+ estimate_edge_probabilities_from_detection_events)
13
+ from .spectral import has_exact_degeneracy, matrix_function_eigh, spectral_evolve
14
+
15
+ __all__ = [
16
+ "ghz_state",
17
+ "pauli_expectation", "pauli_sum_expectation", "pauli_hamiltonian_to_matrix", "pauli_sum_matvec",
18
+ "multiply_pauli_terms",
19
+ "pauli_sum_matvec_jax", "pauli_sum_expectation_jax", "PauliSumOperator",
20
+ "partial_trace", "von_neumann_entropy", "mutual_information", "central_charge",
21
+ "majorana_pauli_terms", "total_parity_operator", "hubbard_hamiltonian_pauli_terms", "square_lattice_edges",
22
+ "pauli_commutes", "compute_syndrome", "erasure_aware_decode", "pymatching_decode", "blind_minimum_weight_decode",
23
+ "decode_with_erasure_fallback", "counts_in_intervals_dimension", "nearest_coset_decode",
24
+ "erasure_ml_decode", "peeling_decode", "union_find_decode", "matching_erasure_decode",
25
+ "estimate_edge_probabilities_from_detection_events",
26
+ "has_exact_degeneracy", "matrix_function_eigh", "spectral_evolve",
27
+ ]
@@ -0,0 +1,161 @@
1
+ """
2
+ Multi-qubit partial trace, von Neumann entropy, and mutual information.
3
+
4
+ Nothing like this existed anywhere in the package before: the only prior
5
+ partial trace (dashboard_core/state_visuals.py's private
6
+ `_reduced_density_matrix`) is single-qubit-only and uses the *opposite*,
7
+ little-endian convention (qubit 0 = least significant bit). Everything
8
+ here uses this package's own convention instead, matching
9
+ dense_evolution.observables/pauli_hamiltonian_to_matrix: qubit 0 is the
10
+ *most* significant bit of the basis-state index. Do not mix the two --
11
+ reusing dashboard_core's helper here would silently transpose which
12
+ qubits get traced out.
13
+
14
+ Originated in research/wormhole_syk.py (a traversable-wormhole-inspired
15
+ quantum teleportation reproduction) -- promoted here because these are
16
+ generic quantum-information utilities, not specific to that experiment.
17
+ Any state can have a subsystem's reduced density matrix, entropy, or the
18
+ mutual information between two subsystems computed with these three
19
+ functions; the wormhole work needed all three because the physically
20
+ meaningful readout there (a message injected into one system showing up
21
+ correlated with a reference qubit) is *not* visible in any single-qubit
22
+ expectation value -- see mutual_information's docstring.
23
+ """
24
+
25
+ import numpy as np
26
+
27
+ __all__ = ['partial_trace', 'von_neumann_entropy', 'mutual_information', 'central_charge']
28
+
29
+
30
+ def partial_trace(state, n_qubits, keep_qubits):
31
+ """Reduced density matrix on `keep_qubits`, tracing out the rest.
32
+
33
+ MSB-first (qubit 0 = most significant bit), this package's own
34
+ convention everywhere else -- NOT the same as dashboard_core.
35
+ state_visuals._reduced_density_matrix, which is deliberately
36
+ little-endian (Qiskit's convention) for its own Bloch-sphere/Q-sphere
37
+ display consumers. Two genuinely different conventions for two
38
+ different consumers, not an accidental divergence.
39
+
40
+ Parameters
41
+ ----------
42
+ state : np.ndarray
43
+ A pure statevector of length 2**n_qubits.
44
+ n_qubits : int
45
+ keep_qubits : list[int]
46
+ Qubit indices (this package's MSB-first convention) to keep.
47
+
48
+ Returns
49
+ -------
50
+ np.ndarray
51
+ Density matrix of shape (2**len(keep_qubits), 2**len(keep_qubits)).
52
+ """
53
+ keep_qubits = sorted(keep_qubits)
54
+ trace_qubits = [q for q in range(n_qubits) if q not in keep_qubits]
55
+ psi = np.transpose(np.asarray(state).reshape([2] * n_qubits), keep_qubits + trace_qubits)
56
+ keep_dim, trace_dim = 2 ** len(keep_qubits), 2 ** len(trace_qubits)
57
+ psi = psi.reshape(keep_dim, trace_dim)
58
+ return psi @ psi.conj().T
59
+
60
+
61
+ def von_neumann_entropy(rho):
62
+ """S(rho) = -Tr(rho log rho), computed from rho's eigenvalues. Nearly-
63
+ zero eigenvalues (which a numerically pure/near-pure state produces,
64
+ and which are mathematically forbidden from being exactly negative
65
+ for a real density matrix but can land at a tiny negative float) are
66
+ clipped before the log rather than raising or propagating a NaN.
67
+
68
+ Natural log (nats), NOT log2 (bits) -- unlike this package's other
69
+ entropy-family quantities (magic_entropy, kl_divergence,
70
+ sandwiched_renyi_divergence, stabilizer_renyi_entropy), which all
71
+ use log2 and say so explicitly. mutual_information/central_charge
72
+ below inherit this same nats convention."""
73
+ eigs = np.clip(np.linalg.eigvalsh(rho).real, 1e-14, None)
74
+ return float(-np.sum(eigs * np.log(eigs)))
75
+
76
+
77
+ def mutual_information(state, n_qubits, qubits_a, qubits_b):
78
+ """I(A:B) = S(A) + S(B) - S(A union B), the standard quantum mutual
79
+ information between two disjoint subsystems of a pure global state.
80
+ In nats (natural log), same as von_neumann_entropy above -- see its
81
+ docstring for how this differs from this package's other,
82
+ log2-based entropy quantities.
83
+
84
+ Why this and not a single-qubit expectation value: a qubit entangled
85
+ in a Bell pair (or more generally, maximally mixed on its own) has a
86
+ marginal <Z> of exactly 0 regardless of what operation was applied to
87
+ its partner -- this is the no-signaling theorem, not a measurement
88
+ limitation, and no amount of clever circuit design around a
89
+ single-qubit readout can get around it. Mutual information *can*
90
+ reveal correlations a marginal expectation value structurally cannot,
91
+ because it depends on the *joint* state of A and B, not either one
92
+ alone. Verified in tests/unit/test_entropy.py against the exact textbook
93
+ value for a Bell pair (I = 2*ln(2), maximal) and a GHZ state.
94
+ """
95
+ if not set(qubits_a).isdisjoint(qubits_b):
96
+ raise ValueError(
97
+ f"qubits_a and qubits_b must be disjoint, got qubits_a={qubits_a!r}, "
98
+ f"qubits_b={qubits_b!r} -- an overlapping qubit would be traced "
99
+ "into S(A), S(B) AND S(A union B) inconsistently, silently "
100
+ "corrupting the result rather than raising."
101
+ )
102
+ s_a = von_neumann_entropy(partial_trace(state, n_qubits, qubits_a))
103
+ s_b = von_neumann_entropy(partial_trace(state, n_qubits, qubits_b))
104
+ s_ab = von_neumann_entropy(partial_trace(state, n_qubits, list(qubits_a) + list(qubits_b)))
105
+ return s_a + s_b - s_ab
106
+
107
+
108
+ def central_charge(Ls, S, n_qubits):
109
+ """Fit an open-chain entanglement entropy curve S(L) to the Calabrese-
110
+ Cardy CFT prediction S(L) = (c/6)*ln[(2N/pi)*sin(pi*L/N)] + const
111
+ (Calabrese & Cardy, J. Stat. Mech. 2004, P06002, eq. 4/19 combined via
112
+ the standard open-chain doubling trick) and return (c, r_squared).
113
+ The fit itself is in the natural-log (nats) convention shown above --
114
+ `S` must be too (von_neumann_entropy's own convention) for the fitted
115
+ `c` to come out right; a log2-based S would scale it off by ln(2).
116
+
117
+ Backend-agnostic: `S` can come from any source (exact diagonalization
118
+ via `partial_trace`/`von_neumann_entropy` on this package's own
119
+ `DenseSVSimulator`, `MPSSimulator`, `Chunk`, or elsewhere) -- this
120
+ doesn't compute the entropy itself, only fits an already-measured
121
+ curve. Meant as a benchmark diagnostic: does a given backend/
122
+ truncation scheme preserve genuine critical CFT scaling, and with
123
+ what effective central charge?
124
+
125
+ A high r_squared alone does NOT mean the extracted c is trustworthy --
126
+ Dense-Evolution-Discovery Experiment 36 found fitting at a finite-size
127
+ pseudo-critical point (a susceptibility peak, not the true CFT point)
128
+ gives a deceptively clean fit (r_squared=0.999997) to a wrong answer
129
+ (c off by 2x). Only trust this near a genuine, independently-verified
130
+ critical point.
131
+
132
+ Parameters
133
+ ----------
134
+ Ls : array-like of int
135
+ Subsystem sizes, each counted from one physical boundary of an
136
+ open chain of `n_qubits` sites (not a bulk interval -- see
137
+ Discovery Experiment 36 for the periodic/bulk c/3 case instead).
138
+ S : array-like of float
139
+ Entanglement entropy at each L in `Ls`, same length.
140
+ n_qubits : int
141
+ Total open-chain length N.
142
+
143
+ Returns
144
+ -------
145
+ c : float
146
+ Extracted central charge (theory: 0.5 for Ising, 1.0 for a free
147
+ boson/XX chain, ...).
148
+ r_squared : float
149
+ Fit quality, in [0, 1] for a sane fit (can go negative for a
150
+ pathological fit worse than the mean).
151
+ """
152
+ Ls = np.asarray(Ls, dtype=float)
153
+ S = np.asarray(S, dtype=float)
154
+ x = np.log((2.0 * n_qubits / np.pi) * np.sin(np.pi * Ls / n_qubits))
155
+ slope, intercept = np.polyfit(x, S, 1)
156
+ c = 6.0 * slope
157
+ pred = slope * x + intercept
158
+ ss_res = float(np.sum((S - pred) ** 2))
159
+ ss_tot = float(np.sum((S - S.mean()) ** 2))
160
+ r_squared = 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan")
161
+ return float(c), r_squared