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,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
|