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.
- dashboard_core/__init__.py +115 -0
- dashboard_core/_gate_tables.py +30 -0
- dashboard_core/band_structure.py +71 -0
- dashboard_core/circuit_builder_component.py +232 -0
- dashboard_core/circuit_diagram.py +216 -0
- dashboard_core/crypto_protocols.py +77 -0
- dashboard_core/engine.py +326 -0
- dashboard_core/graphical_builder.py +114 -0
- dashboard_core/hamiltonians.py +593 -0
- dashboard_core/mass_decomposition_tool.py +47 -0
- dashboard_core/mitigation.py +343 -0
- dashboard_core/native_hf_diagnostics.py +62 -0
- dashboard_core/noise_tools.py +125 -0
- dashboard_core/qasm_library.py +233 -0
- dashboard_core/qmmm.py +16 -0
- dashboard_core/rag_tool.py +45 -0
- dashboard_core/state_visuals.py +288 -0
- dashboard_core/system_limits.py +60 -0
- dashboard_core/vector_healing.py +102 -0
- dashboard_core/visuals.py +158 -0
- dashboard_core/vqe.py +533 -0
- dashboard_core/wormhole.py +580 -0
- dense_evolution/__init__.py +114 -0
- dense_evolution/autodiff.py +10 -0
- dense_evolution/backends/__init__.py +5 -0
- dense_evolution/backends/chunk/__init__.py +37 -0
- dense_evolution/backends/chunk/_engine_imports.py +57 -0
- dense_evolution/backends/chunk/circuit_chunker.py +55 -0
- dense_evolution/backends/chunk/core.py +432 -0
- dense_evolution/backends/chunk/disk_overflow.py +232 -0
- dense_evolution/backends/chunk/geometry.py +95 -0
- dense_evolution/backends/chunk/guard.py +190 -0
- dense_evolution/backends/chunk/kernels.py +531 -0
- dense_evolution/backends/mps.py +1569 -0
- dense_evolution/backends/statevector.py +616 -0
- dense_evolution/chunk.py +25 -0
- dense_evolution/circuits/__init__.py +20 -0
- dense_evolution/circuits/compiler.py +488 -0
- dense_evolution/circuits/diagram.py +94 -0
- dense_evolution/circuits/gates.py +91 -0
- dense_evolution/circuits/parser.py +632 -0
- dense_evolution/circuits/qft.py +66 -0
- dense_evolution/circuits/random_circuit.py +85 -0
- dense_evolution/circuits/registry.py +74 -0
- dense_evolution/circuits/topology.py +79 -0
- dense_evolution/circuits/trotter.py +265 -0
- dense_evolution/circuits/uccsd.py +275 -0
- dense_evolution/cli.py +199 -0
- dense_evolution/compiler.py +9 -0
- dense_evolution/config.py +49 -0
- dense_evolution/drawing.py +10 -0
- dense_evolution/entropy.py +9 -0
- dense_evolution/fermions.py +9 -0
- dense_evolution/gates.py +9 -0
- dense_evolution/harrison_tb.py +16 -0
- dense_evolution/healing.py +18 -0
- dense_evolution/interop/__init__.py +18 -0
- dense_evolution/interop/qiskit_pennylane.py +406 -0
- dense_evolution/measurement.py +10 -0
- dense_evolution/mitigation/__init__.py +54 -0
- dense_evolution/mitigation/healing.py +215 -0
- dense_evolution/mitigation/kl_divergence.py +93 -0
- dense_evolution/mitigation/magic_entropy.py +163 -0
- dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
- dense_evolution/mitigation/renyi.py +168 -0
- dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
- dense_evolution/mitigation/zne.py +990 -0
- dense_evolution/mps.py +9 -0
- dense_evolution/native_hf/__init__.py +26 -0
- dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
- dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
- dense_evolution/native_hf/assembly.py +304 -0
- dense_evolution/native_hf/basis.py +117 -0
- dense_evolution/native_hf/boys.py +35 -0
- dense_evolution/native_hf/bridge.py +112 -0
- dense_evolution/native_hf/cartesian.py +64 -0
- dense_evolution/native_hf/coulomb.py +196 -0
- dense_evolution/native_hf/differentiable.py +53 -0
- dense_evolution/native_hf/gaussians.py +79 -0
- dense_evolution/native_hf/kinetic.py +52 -0
- dense_evolution/native_hf/libcint_bridge.py +167 -0
- dense_evolution/native_hf/overlap.py +91 -0
- dense_evolution/native_hf/scf.py +404 -0
- dense_evolution/noise/__init__.py +79 -0
- dense_evolution/noise/coherent_attack.py +264 -0
- dense_evolution/noise/cosmic_ray.py +61 -0
- dense_evolution/noise/density_matrix_channels.py +78 -0
- dense_evolution/noise/differentiable.py +66 -0
- dense_evolution/noise/kraus/__init__.py +6 -0
- dense_evolution/noise/kraus/amplitude_damping.py +47 -0
- dense_evolution/noise/kraus/bitflip.py +22 -0
- dense_evolution/noise/kraus/combined.py +16 -0
- dense_evolution/noise/kraus/depolarizing.py +47 -0
- dense_evolution/noise/kraus/ideal.py +10 -0
- dense_evolution/noise/kraus/phaseflip.py +21 -0
- dense_evolution/noise/kraus_channels.py +285 -0
- dense_evolution/noise/oscillating.py +32 -0
- dense_evolution/noise/pink.py +80 -0
- dense_evolution/observables.py +11 -0
- dense_evolution/parser.py +9 -0
- dense_evolution/physics/__init__.py +27 -0
- dense_evolution/physics/entropy.py +161 -0
- dense_evolution/physics/fermions.py +322 -0
- dense_evolution/physics/observables.py +523 -0
- dense_evolution/physics/qec.py +1113 -0
- dense_evolution/physics/spectral.py +143 -0
- dense_evolution/physics/states.py +43 -0
- dense_evolution/protocols/__init__.py +27 -0
- dense_evolution/protocols/bb84.py +133 -0
- dense_evolution/protocols/di_qkd_ghz.py +199 -0
- dense_evolution/protocols/dicka_protocol2.py +124 -0
- dense_evolution/qec.py +20 -0
- dense_evolution/qft.py +9 -0
- dense_evolution/qmmm/__init__.py +13 -0
- dense_evolution/qmmm/ase_bridge.py +97 -0
- dense_evolution/qmmm/forces.py +388 -0
- dense_evolution/qmmm/propagation.py +80 -0
- dense_evolution/qmmm/region.py +137 -0
- dense_evolution/random_circuit.py +15 -0
- dense_evolution/registry.py +9 -0
- dense_evolution/simulator.py +10 -0
- dense_evolution/solvers/__init__.py +19 -0
- dense_evolution/solvers/autodiff.py +169 -0
- dense_evolution/solvers/harrison_tb.py +189 -0
- dense_evolution/solvers/vhd_tb.py +187 -0
- dense_evolution/states.py +9 -0
- dense_evolution/topology.py +9 -0
- dense_evolution/trotter.py +9 -0
- dense_evolution/utils/__init__.py +13 -0
- dense_evolution/utils/drawing.py +101 -0
- dense_evolution/utils/mass_decomposition.py +246 -0
- dense_evolution/utils/measurement.py +94 -0
- dense_evolution/vhd_tb.py +16 -0
- dense_evolution-8.3.0.dist-info/METADATA +366 -0
- dense_evolution-8.3.0.dist-info/RECORD +165 -0
- dense_evolution-8.3.0.dist-info/WHEEL +5 -0
- dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
- dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
- dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
- ia_utils/__init__.py +0 -0
- ia_utils/adversarial_vector_attack.py +196 -0
- ia_utils/rag.py +288 -0
- ia_utils/vector_healing.py +399 -0
- local_site/__init__.py +0 -0
- local_site/app/__init__.py +0 -0
- local_site/app/server.py +1009 -0
- mcp_server/__init__.py +0 -0
- mcp_server/client.py +324 -0
- mcp_server/config.py +32 -0
- mcp_server/models.py +347 -0
- mcp_server/molecules.py +71 -0
- mcp_server/server.py +119 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/chemistry_tools.py +225 -0
- mcp_server/tools/circuit_tools.py +83 -0
- mcp_server/tools/crypto_tools.py +66 -0
- mcp_server/tools/mitigation_tools.py +81 -0
- mcp_server/tools/noise_tools.py +60 -0
- mcp_server/tools/retrieval_tools.py +44 -0
- mcp_server/tools/system_tools.py +149 -0
- mcp_server/tools/wormhole_tools.py +142 -0
- mcp_server/utils/__init__.py +0 -0
- mcp_server/utils/cache.py +55 -0
- mcp_server/utils/images.py +67 -0
- 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
|