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,523 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pauli-string expectation values, computed directly from a statevector via
|
|
3
|
+
O(dim) bit manipulation -- the 2**n_qubits Hamiltonian matrix is never
|
|
4
|
+
built. The same technique (XOR a flip-mask into the basis-state indices,
|
|
5
|
+
track a per-qubit phase from the bit values) shows up hand-duplicated
|
|
6
|
+
across dozens of VQE/observable scripts built on this package, each with
|
|
7
|
+
its own slightly different bit-twiddling for whichever two or three Pauli
|
|
8
|
+
operators that script happened to need. This module factors it into one
|
|
9
|
+
tested, general implementation for an arbitrary Pauli string on any subset
|
|
10
|
+
of qubits.
|
|
11
|
+
|
|
12
|
+
Indexing convention: this package's DenseSVSimulator stores qubit 0 as the
|
|
13
|
+
*most* significant bit of the basis-state index (empirically: `('x', 0)`
|
|
14
|
+
on a 2-qubit register lands on index 2 = '10', not index 1) -- so qubit q
|
|
15
|
+
is bit (n_qubits - 1 - q) of the index, and every bit-position computed
|
|
16
|
+
here is translated through that offset rather than assuming qubit q is
|
|
17
|
+
bit q directly. The string form of a Pauli term reads left-to-right as
|
|
18
|
+
qubit 0 upward (`pauli_terms[q]` is the operator on qubit q), independent
|
|
19
|
+
of this internal bit-position detail.
|
|
20
|
+
"""
|
|
21
|
+
import numpy as np
|
|
22
|
+
import jax.numpy as jnp
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'pauli_expectation', 'pauli_sum_expectation', 'pauli_hamiltonian_to_matrix',
|
|
26
|
+
'pauli_sum_matvec', 'multiply_pauli_terms',
|
|
27
|
+
'pauli_sum_matvec_jax', 'pauli_sum_expectation_jax', 'PauliSumOperator',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
_PAULI_MATRICES = {
|
|
31
|
+
'I': np.eye(2, dtype=np.complex128),
|
|
32
|
+
'X': np.array([[0, 1], [1, 0]], dtype=np.complex128),
|
|
33
|
+
'Y': np.array([[0, -1j], [1j, 0]], dtype=np.complex128),
|
|
34
|
+
'Z': np.array([[1, 0], [0, -1]], dtype=np.complex128),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_terms(pauli_terms, n_qubits=None):
|
|
39
|
+
"""Accepts a string ('IXYZ...', pauli_terms[q] = qubit q), a dict
|
|
40
|
+
{qubit: 'X'|'Y'|'Z'|'I'}, or an iterable of (qubit, pauli) pairs.
|
|
41
|
+
Returns a plain dict {qubit: 'X'|'Y'|'Z'} with identity terms dropped
|
|
42
|
+
and every Pauli letter validated."""
|
|
43
|
+
if isinstance(pauli_terms, str):
|
|
44
|
+
if n_qubits is not None and len(pauli_terms) != n_qubits:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"pauli_terms string has length {len(pauli_terms)}, "
|
|
47
|
+
f"but n_qubits={n_qubits}")
|
|
48
|
+
terms = {q: p.upper() for q, p in enumerate(pauli_terms) if p.upper() != 'I'}
|
|
49
|
+
elif isinstance(pauli_terms, dict):
|
|
50
|
+
terms = {int(q): str(p).upper() for q, p in pauli_terms.items() if str(p).upper() != 'I'}
|
|
51
|
+
else:
|
|
52
|
+
terms = {int(q): str(p).upper() for q, p in pauli_terms if str(p).upper() != 'I'}
|
|
53
|
+
|
|
54
|
+
for q, p in terms.items():
|
|
55
|
+
if p not in ('X', 'Y', 'Z'):
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"unknown Pauli operator {p!r} for qubit {q}, expected one of X, Y, Z, I")
|
|
58
|
+
if q < 0:
|
|
59
|
+
raise ValueError(f"qubit index {q} must be >= 0")
|
|
60
|
+
return terms
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _apply_pauli_term(statevector, terms, inferred_n_qubits):
|
|
64
|
+
"""P|psi> for a single already-normalized Pauli term (terms: plain
|
|
65
|
+
dict {qubit: 'X'|'Y'|'Z'}, identity qubits already dropped), computed
|
|
66
|
+
in O(dim) via the same flip-mask/phase technique pauli_expectation
|
|
67
|
+
uses -- factored out here so both pauli_expectation (which reduces
|
|
68
|
+
this to a scalar via vdot) and pauli_sum_matvec (which needs the
|
|
69
|
+
vector itself, not a scalar) share one tested implementation instead
|
|
70
|
+
of two copies of the same bit-twiddling."""
|
|
71
|
+
if not terms:
|
|
72
|
+
return statevector
|
|
73
|
+
|
|
74
|
+
def bit_pos(q):
|
|
75
|
+
return inferred_n_qubits - 1 - q
|
|
76
|
+
|
|
77
|
+
flip_mask = 0
|
|
78
|
+
for q, p in terms.items():
|
|
79
|
+
if p in ('X', 'Y'):
|
|
80
|
+
flip_mask |= (1 << bit_pos(q))
|
|
81
|
+
|
|
82
|
+
dim = statevector.shape[0]
|
|
83
|
+
indices = np.arange(dim)
|
|
84
|
+
source_idx = indices ^ flip_mask
|
|
85
|
+
|
|
86
|
+
coeff = np.ones(dim, dtype=np.complex128)
|
|
87
|
+
for q, p in terms.items():
|
|
88
|
+
bit = (source_idx >> bit_pos(q)) & 1
|
|
89
|
+
if p == 'Y':
|
|
90
|
+
coeff = coeff * np.where(bit == 0, 1j, -1j)
|
|
91
|
+
elif p == 'Z':
|
|
92
|
+
coeff = coeff * np.where(bit == 0, 1.0, -1.0)
|
|
93
|
+
|
|
94
|
+
return statevector[source_idx] * coeff
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def pauli_expectation(statevector, pauli_terms, n_qubits=None):
|
|
98
|
+
"""
|
|
99
|
+
Exact expectation value <psi|P|psi> of a single Pauli string P on a
|
|
100
|
+
pure statevector, computed in O(dim) without ever building the
|
|
101
|
+
2**n_qubits matrix for P.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
statevector : array-like, shape (2**n_qubits,)
|
|
106
|
+
A normalized statevector (as returned by
|
|
107
|
+
DenseSVSimulator.get_statevector()).
|
|
108
|
+
pauli_terms : str | dict | iterable of (int, str)
|
|
109
|
+
The Pauli string, in any of three equivalent forms:
|
|
110
|
+
- a string, e.g. ``'XIZ'`` -- pauli_terms[q] is the operator on
|
|
111
|
+
qubit q (qubit 0 first, left-to-right; see module docstring)
|
|
112
|
+
- a dict ``{qubit: 'X'|'Y'|'Z'}`` -- omitted qubits are
|
|
113
|
+
identity, convenient when only a few qubits are non-identity
|
|
114
|
+
- an iterable of ``(qubit, pauli)`` pairs
|
|
115
|
+
Any qubit not mentioned (or given 'I') is identity.
|
|
116
|
+
n_qubits : int, optional
|
|
117
|
+
Only used to validate a string-form pauli_terms' length up front;
|
|
118
|
+
ignored for the dict/iterable forms.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
float
|
|
123
|
+
Real by construction: every Pauli string is Hermitian, so its
|
|
124
|
+
expectation value on any state is real.
|
|
125
|
+
|
|
126
|
+
Examples
|
|
127
|
+
--------
|
|
128
|
+
>>> import dense_evolution as de
|
|
129
|
+
>>> sim = de.DenseSVSimulator(2)
|
|
130
|
+
>>> sim.run_circuit([('h', 0), ('cx', 0, 1)])
|
|
131
|
+
>>> de.pauli_expectation(sim.get_statevector(), 'ZZ')
|
|
132
|
+
1.0
|
|
133
|
+
>>> de.pauli_expectation(sim.get_statevector(), {0: 'X', 1: 'X'})
|
|
134
|
+
1.0
|
|
135
|
+
"""
|
|
136
|
+
statevector = np.asarray(statevector)
|
|
137
|
+
dim = statevector.shape[0]
|
|
138
|
+
inferred_n_qubits = dim.bit_length() - 1
|
|
139
|
+
if 1 << inferred_n_qubits != dim:
|
|
140
|
+
raise ValueError(f"statevector length {dim} is not a power of 2")
|
|
141
|
+
|
|
142
|
+
terms = _normalize_terms(pauli_terms, n_qubits)
|
|
143
|
+
if terms and max(terms) >= inferred_n_qubits:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"pauli_terms references qubit {max(terms)}, but the statevector "
|
|
146
|
+
f"only spans {inferred_n_qubits} qubits")
|
|
147
|
+
|
|
148
|
+
p_psi = _apply_pauli_term(statevector, terms, inferred_n_qubits)
|
|
149
|
+
return float(np.real(np.vdot(statevector, p_psi)))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def pauli_sum_expectation(statevector, terms, n_qubits=None):
|
|
153
|
+
"""
|
|
154
|
+
Expectation value of a weighted sum of Pauli strings, i.e. a
|
|
155
|
+
Hamiltonian given directly in Pauli form:
|
|
156
|
+
``sum_i coeff_i * <psi|P_i|psi>``.
|
|
157
|
+
|
|
158
|
+
Unlike ``circuit_to_energy_fn``'s ``h_matrix @ statevector`` approach,
|
|
159
|
+
this never builds the 2**n_qubits Hamiltonian matrix -- useful once
|
|
160
|
+
the system is too large for a dense Hamiltonian to be practical, or
|
|
161
|
+
simply when the Hamiltonian is more naturally expressed as a Pauli
|
|
162
|
+
sum than as an explicit matrix.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
statevector : array-like, shape (2**n_qubits,)
|
|
167
|
+
terms : iterable of (coeff, pauli_terms)
|
|
168
|
+
coeff : float or complex weight for that term.
|
|
169
|
+
pauli_terms : in any form ``pauli_expectation`` accepts (string,
|
|
170
|
+
dict, or pair-iterable).
|
|
171
|
+
n_qubits : int, optional
|
|
172
|
+
Forwarded to ``pauli_expectation`` for string-form term validation.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
float
|
|
177
|
+
|
|
178
|
+
Examples
|
|
179
|
+
--------
|
|
180
|
+
>>> # H = 1.0 * Z0 Z1 + 0.5 * X0 (a 2-site transverse-field-Ising term)
|
|
181
|
+
>>> pauli_sum_expectation(sv, [(1.0, 'ZZ'), (0.5, {0: 'X'})])
|
|
182
|
+
"""
|
|
183
|
+
total = 0.0
|
|
184
|
+
for coeff, pauli_terms in terms:
|
|
185
|
+
total += coeff * pauli_expectation(statevector, pauli_terms, n_qubits=n_qubits)
|
|
186
|
+
return total
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def pauli_hamiltonian_to_matrix(terms, n_qubits):
|
|
190
|
+
"""
|
|
191
|
+
Builds the real, explicit dense Hermitian Hamiltonian matrix for a
|
|
192
|
+
weighted sum of Pauli strings, H = sum_i coeff_i * P_i -- the
|
|
193
|
+
(2**n_qubits, 2**n_qubits) matrix pauli_sum_expectation deliberately
|
|
194
|
+
avoids building. Use this when something downstream genuinely needs
|
|
195
|
+
the matrix itself (exact diagonalization for a ground-state energy,
|
|
196
|
+
a VQE cost function computed as ``<psi| H @ psi>`` instead of a
|
|
197
|
+
Pauli-by-Pauli sum, ...), not just an expectation value.
|
|
198
|
+
|
|
199
|
+
Same qubit-0-is-MSB convention as the rest of this module (see the
|
|
200
|
+
module docstring): each term's matrix is the Kronecker product of
|
|
201
|
+
per-qubit 2x2 Pauli matrices in qubit order 0..n_qubits-1, so this
|
|
202
|
+
matrix's basis-state index lines up exactly with the one
|
|
203
|
+
pauli_expectation/pauli_sum_expectation use -- H @ statevector and
|
|
204
|
+
pauli_sum_expectation(statevector, terms) agree to floating-point
|
|
205
|
+
precision for the same terms.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
terms : iterable of (coeff, pauli_terms)
|
|
210
|
+
Same format pauli_sum_expectation accepts: coeff is a real or
|
|
211
|
+
complex weight, pauli_terms is a string/dict/pair-iterable in any
|
|
212
|
+
form _normalize_terms accepts.
|
|
213
|
+
n_qubits : int
|
|
214
|
+
Total number of qubits the matrix spans (every term's qubits must
|
|
215
|
+
be < n_qubits).
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
numpy.ndarray, shape (2**n_qubits, 2**n_qubits), dtype complex128
|
|
220
|
+
Hermitian by construction (a real-weighted sum of Hermitian
|
|
221
|
+
Pauli-string matrices, each a Kronecker product of Hermitian 2x2
|
|
222
|
+
Pauli matrices -- Hermiticity is closed under both operations).
|
|
223
|
+
|
|
224
|
+
Examples
|
|
225
|
+
--------
|
|
226
|
+
>>> H = pauli_hamiltonian_to_matrix([(1.0, 'ZZ'), (0.5, {0: 'X'})], n_qubits=2)
|
|
227
|
+
>>> H.shape
|
|
228
|
+
(4, 4)
|
|
229
|
+
"""
|
|
230
|
+
if n_qubits < 1:
|
|
231
|
+
raise ValueError(f"n_qubits must be >= 1, got {n_qubits}")
|
|
232
|
+
dim = 1 << n_qubits
|
|
233
|
+
H = np.zeros((dim, dim), dtype=np.complex128)
|
|
234
|
+
|
|
235
|
+
for coeff, pauli_terms in terms:
|
|
236
|
+
normalized = _normalize_terms(pauli_terms, n_qubits)
|
|
237
|
+
if normalized and max(normalized) >= n_qubits:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"term references qubit {max(normalized)}, but n_qubits={n_qubits}"
|
|
240
|
+
)
|
|
241
|
+
term_matrix = np.array([[1.0]], dtype=np.complex128)
|
|
242
|
+
for q in range(n_qubits):
|
|
243
|
+
letter = normalized.get(q, 'I')
|
|
244
|
+
term_matrix = np.kron(term_matrix, _PAULI_MATRICES[letter])
|
|
245
|
+
H += coeff * term_matrix
|
|
246
|
+
|
|
247
|
+
return H
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def pauli_sum_matvec(vector, terms, n_qubits=None):
|
|
251
|
+
"""
|
|
252
|
+
H @ vector for a Hamiltonian given as a weighted sum of Pauli strings,
|
|
253
|
+
computed in O(dim * n_terms) WITHOUT ever building the (2**n_qubits,
|
|
254
|
+
2**n_qubits) matrix pauli_hamiltonian_to_matrix materializes -- the
|
|
255
|
+
matrix-free counterpart needed for an iterative/sparse eigensolver
|
|
256
|
+
(e.g. scipy.sparse.linalg.eigsh via a LinearOperator wrapping this
|
|
257
|
+
function), where pauli_hamiltonian_to_matrix's O(dim**2) memory is
|
|
258
|
+
exactly the thing being avoided.
|
|
259
|
+
|
|
260
|
+
Same qubit-0-is-MSB convention as the rest of this module: this
|
|
261
|
+
agrees with pauli_hamiltonian_to_matrix(terms, n_qubits) @ vector to
|
|
262
|
+
floating-point precision for the same terms -- same underlying
|
|
263
|
+
per-term application as pauli_expectation, just returning the vector
|
|
264
|
+
P|psi> instead of reducing it to <psi|P|psi>.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
vector : array-like, shape (2**n_qubits,)
|
|
269
|
+
Not required to be normalized (this is a linear map, not an
|
|
270
|
+
expectation value) -- e.g. an intermediate Lanczos vector, not
|
|
271
|
+
necessarily a physical statevector.
|
|
272
|
+
terms : iterable of (coeff, pauli_terms)
|
|
273
|
+
Same format pauli_sum_expectation/pauli_hamiltonian_to_matrix
|
|
274
|
+
accept.
|
|
275
|
+
n_qubits : int, optional
|
|
276
|
+
Only used to validate string-form terms' length; inferred from
|
|
277
|
+
vector's length otherwise (same convention as pauli_expectation).
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
numpy.ndarray, shape (2**n_qubits,), dtype complex128
|
|
282
|
+
|
|
283
|
+
Examples
|
|
284
|
+
--------
|
|
285
|
+
>>> import numpy as np
|
|
286
|
+
>>> terms = [(1.0, 'ZZ'), (0.5, {0: 'X'})]
|
|
287
|
+
>>> v = np.array([1, 0, 0, 0], dtype=complex)
|
|
288
|
+
>>> pauli_sum_matvec(v, terms, n_qubits=2)
|
|
289
|
+
array([1. +0.j, 0. +0.j, 0. +0.j, 0.5+0.j])
|
|
290
|
+
>>> pauli_hamiltonian_to_matrix(terms, n_qubits=2) @ v
|
|
291
|
+
array([1. +0.j, 0. +0.j, 0. +0.j, 0.5+0.j])
|
|
292
|
+
"""
|
|
293
|
+
vector = np.asarray(vector, dtype=np.complex128)
|
|
294
|
+
dim = vector.shape[0]
|
|
295
|
+
inferred_n_qubits = dim.bit_length() - 1
|
|
296
|
+
if 1 << inferred_n_qubits != dim:
|
|
297
|
+
raise ValueError(f"vector length {dim} is not a power of 2")
|
|
298
|
+
|
|
299
|
+
result = np.zeros(dim, dtype=np.complex128)
|
|
300
|
+
for coeff, pauli_terms in terms:
|
|
301
|
+
normalized = _normalize_terms(pauli_terms, n_qubits)
|
|
302
|
+
if normalized and max(normalized) >= inferred_n_qubits:
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f"term references qubit {max(normalized)}, but vector only "
|
|
305
|
+
f"spans {inferred_n_qubits} qubits")
|
|
306
|
+
result += coeff * _apply_pauli_term(vector, normalized, inferred_n_qubits)
|
|
307
|
+
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _apply_pauli_term_jax(statevector, terms, inferred_n_qubits):
|
|
312
|
+
"""JAX-native counterpart of _apply_pauli_term -- pure jnp ops, no
|
|
313
|
+
np.asarray/float() cast on `statevector`, so this stays valid under
|
|
314
|
+
jax.grad/jax.jit tracing (same split as _jsd_vectors/_jsd_vectors_jax
|
|
315
|
+
in dense_evolution.backends.mps). `terms` (already-normalized dict)
|
|
316
|
+
and `inferred_n_qubits` are always plain Python objects, never traced
|
|
317
|
+
-- only `statevector` is ever a tracer here."""
|
|
318
|
+
if not terms:
|
|
319
|
+
return statevector
|
|
320
|
+
|
|
321
|
+
def bit_pos(q):
|
|
322
|
+
return inferred_n_qubits - 1 - q
|
|
323
|
+
|
|
324
|
+
flip_mask = 0
|
|
325
|
+
for q, p in terms.items():
|
|
326
|
+
if p in ('X', 'Y'):
|
|
327
|
+
flip_mask |= (1 << bit_pos(q))
|
|
328
|
+
|
|
329
|
+
dim = statevector.shape[0]
|
|
330
|
+
indices = jnp.arange(dim)
|
|
331
|
+
source_idx = indices ^ flip_mask
|
|
332
|
+
|
|
333
|
+
coeff = jnp.ones(dim, dtype=statevector.dtype)
|
|
334
|
+
for q, p in terms.items():
|
|
335
|
+
bit = (source_idx >> bit_pos(q)) & 1
|
|
336
|
+
if p == 'Y':
|
|
337
|
+
coeff = coeff * jnp.where(bit == 0, 1j, -1j)
|
|
338
|
+
elif p == 'Z':
|
|
339
|
+
coeff = coeff * jnp.where(bit == 0, 1.0, -1.0)
|
|
340
|
+
|
|
341
|
+
return statevector[source_idx] * coeff
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def pauli_sum_matvec_jax(vector, terms, n_qubits=None):
|
|
345
|
+
"""JAX-native counterpart of pauli_sum_matvec: H @ vector for a
|
|
346
|
+
Hamiltonian given as a weighted sum of Pauli strings, never building
|
|
347
|
+
the 2**n_qubits matrix -- but pure jnp internally (no np.asarray/
|
|
348
|
+
float() on `vector`), so unlike pauli_sum_matvec itself this stays
|
|
349
|
+
valid under jax.grad/jax.jit tracing. Verified to agree with
|
|
350
|
+
pauli_sum_matvec to floating-point precision, and to give correct
|
|
351
|
+
gradients (matching a dense pauli_hamiltonian_to_matrix reference)
|
|
352
|
+
-- see test_pauli_sum_jax_matches_numpy_and_is_differentiable.
|
|
353
|
+
|
|
354
|
+
`terms`/`n_qubits` must stay plain Python objects (never traced) --
|
|
355
|
+
only `vector` may be a JAX tracer.
|
|
356
|
+
|
|
357
|
+
PRECISION GOTCHA: this function never calls dense_evolution.config's
|
|
358
|
+
ensure_x64() itself (it's a pure math function, no opinion on global
|
|
359
|
+
JAX state) -- if nothing else in the process has constructed a
|
|
360
|
+
DenseSVSimulator/QuantumHardwareRegistry/circuit_to_energy_fn yet
|
|
361
|
+
(the only things that call ensure_x64() lazily), JAX is still at its
|
|
362
|
+
float32 default, and this silently runs at complex64 precision with
|
|
363
|
+
no error, just ~1e-7 relative accuracy instead of ~1e-16 -- verified
|
|
364
|
+
directly (a standalone correctness selftest run before constructing
|
|
365
|
+
anything else failed at max_diff=7.10e-07, exactly float32 relative
|
|
366
|
+
precision, until dense_evolution.set_precision(True) was called
|
|
367
|
+
first). Call dense_evolution.set_precision(True) yourself up front
|
|
368
|
+
if you're using this standalone, before anything else has a chance
|
|
369
|
+
to enable x64 for you.
|
|
370
|
+
|
|
371
|
+
This is what lets a differentiable VQE loop reach 20+ qubits at all.
|
|
372
|
+
circuit_to_energy_fn's own h_matrix @ statevector path needs a dense
|
|
373
|
+
(2**n_qubits, 2**n_qubits) matrix -- physically impossible to hold
|
|
374
|
+
much past ~14 qubits (2**28 complex128 entries = 4GB, x4 per extra
|
|
375
|
+
qubit) -- while the statevector itself stays linear in dim (2**20
|
|
376
|
+
complex128 = 16MB at 20 qubits, no problem at all). Drop this in as
|
|
377
|
+
circuit_to_energy_fn's `h_matrix` argument via PauliSumOperator
|
|
378
|
+
(below), whose only job is wrapping this behind `__matmul__` since
|
|
379
|
+
that's the only operation energy_fn performs on h_matrix:
|
|
380
|
+
|
|
381
|
+
from dense_evolution import circuit_to_energy_fn, PauliSumOperator
|
|
382
|
+
energy_fn, n_params = circuit_to_energy_fn(circuit, n_qubits=20)
|
|
383
|
+
h_op = PauliSumOperator(terms, n_qubits=20)
|
|
384
|
+
energy, sv = energy_fn(theta, h_op)
|
|
385
|
+
grad = jax.grad(lambda th: energy_fn(th, h_op)[0])(theta)
|
|
386
|
+
"""
|
|
387
|
+
dim = vector.shape[0]
|
|
388
|
+
inferred_n_qubits = dim.bit_length() - 1
|
|
389
|
+
if 1 << inferred_n_qubits != dim:
|
|
390
|
+
raise ValueError(f"vector length {dim} is not a power of 2")
|
|
391
|
+
|
|
392
|
+
vector = jnp.asarray(vector, dtype=jnp.complex128)
|
|
393
|
+
result = jnp.zeros(dim, dtype=jnp.complex128)
|
|
394
|
+
for coeff, pauli_terms in terms:
|
|
395
|
+
normalized = _normalize_terms(pauli_terms, n_qubits)
|
|
396
|
+
if normalized and max(normalized) >= inferred_n_qubits:
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"term references qubit {max(normalized)}, but vector only "
|
|
399
|
+
f"spans {inferred_n_qubits} qubits")
|
|
400
|
+
result = result + coeff * _apply_pauli_term_jax(vector, normalized, inferred_n_qubits)
|
|
401
|
+
|
|
402
|
+
return result
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def pauli_sum_expectation_jax(statevector, terms, n_qubits=None):
|
|
406
|
+
"""JAX-native counterpart of pauli_sum_expectation -- same
|
|
407
|
+
sum_i coeff_i * <psi|P_i|psi>, pure jnp internally so it stays valid
|
|
408
|
+
under jax.grad/jax.jit (unlike pauli_sum_expectation, whose
|
|
409
|
+
np.asarray(statevector) forces concretization). Returns a jnp float
|
|
410
|
+
scalar, not a Python float -- call float(...) yourself outside a
|
|
411
|
+
traced context if you need one. `terms`/`n_qubits` must stay plain
|
|
412
|
+
Python objects; only `statevector` may be a tracer."""
|
|
413
|
+
dim = statevector.shape[0]
|
|
414
|
+
inferred_n_qubits = dim.bit_length() - 1
|
|
415
|
+
if 1 << inferred_n_qubits != dim:
|
|
416
|
+
raise ValueError(f"statevector length {dim} is not a power of 2")
|
|
417
|
+
|
|
418
|
+
statevector = jnp.asarray(statevector, dtype=jnp.complex128)
|
|
419
|
+
total = jnp.zeros((), dtype=jnp.float64)
|
|
420
|
+
for coeff, pauli_terms in terms:
|
|
421
|
+
normalized = _normalize_terms(pauli_terms, n_qubits)
|
|
422
|
+
if normalized and max(normalized) >= inferred_n_qubits:
|
|
423
|
+
raise ValueError(
|
|
424
|
+
f"term references qubit {max(normalized)}, but statevector only "
|
|
425
|
+
f"spans {inferred_n_qubits} qubits")
|
|
426
|
+
p_psi = _apply_pauli_term_jax(statevector, normalized, inferred_n_qubits)
|
|
427
|
+
total = total + coeff * jnp.real(jnp.vdot(statevector, p_psi))
|
|
428
|
+
|
|
429
|
+
return total
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class PauliSumOperator:
|
|
433
|
+
"""Matrix-free Hamiltonian wrapper: presents a Pauli-sum Hamiltonian
|
|
434
|
+
(the same `terms` format pauli_sum_expectation/pauli_sum_matvec_jax
|
|
435
|
+
accept) as an object supporting `@`, so it can be dropped in wherever
|
|
436
|
+
a dense h_matrix is expected without ever materializing one.
|
|
437
|
+
|
|
438
|
+
Written specifically for circuit_to_energy_fn(circuit, n_qubits)'s
|
|
439
|
+
energy_fn(theta, h_matrix, ...), whose only use of h_matrix is
|
|
440
|
+
`h_matrix @ statevector` -- see pauli_sum_matvec_jax's docstring for
|
|
441
|
+
the full worked example. `terms`/`n_qubits` are fixed at construction
|
|
442
|
+
(plain Python objects, never traced); only the vector passed to
|
|
443
|
+
`@` may be a JAX tracer, keeping the whole thing jax.grad-safe."""
|
|
444
|
+
|
|
445
|
+
def __init__(self, terms, n_qubits):
|
|
446
|
+
self.terms = terms
|
|
447
|
+
self.n_qubits = n_qubits
|
|
448
|
+
|
|
449
|
+
def __matmul__(self, vector):
|
|
450
|
+
return pauli_sum_matvec_jax(vector, self.terms, n_qubits=self.n_qubits)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
_SAME_QUBIT_PAULI_PRODUCT = {
|
|
454
|
+
('X', 'X'): (1, None), ('Y', 'Y'): (1, None), ('Z', 'Z'): (1, None),
|
|
455
|
+
('X', 'Y'): (1j, 'Z'), ('Y', 'X'): (-1j, 'Z'),
|
|
456
|
+
('Y', 'Z'): (1j, 'X'), ('Z', 'Y'): (-1j, 'X'),
|
|
457
|
+
('Z', 'X'): (1j, 'Y'), ('X', 'Z'): (-1j, 'Y'),
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def multiply_pauli_terms(factors):
|
|
462
|
+
"""
|
|
463
|
+
Multiplies several Pauli-string OPERATORS together into one combined
|
|
464
|
+
term, tracking the i^k phase picked up whenever two factors act on the
|
|
465
|
+
same qubit (X*Y=iZ, Y*X=-iZ, etc.) -- the exact symbolic algebra
|
|
466
|
+
needed to compose Pauli strings by hand, e.g. building the "Klein
|
|
467
|
+
factor" total-parity operator for a set of Jordan-Wigner-mapped
|
|
468
|
+
Majorana modes (see dense_evolution.physics.fermions.total_parity_operator),
|
|
469
|
+
or any other manual Pauli-operator product.
|
|
470
|
+
|
|
471
|
+
This multiplies OPERATORS (order matters -- Pauli matrices don't
|
|
472
|
+
commute), unlike pauli_hamiltonian_to_matrix/pauli_sum_expectation,
|
|
473
|
+
which take a SUM of independent terms (order doesn't matter there).
|
|
474
|
+
|
|
475
|
+
Promoted from Dense-Evolution-Discovery's dashboard_core.wormhole
|
|
476
|
+
module (`_multiply_pauli_dicts`), where it was originally written to
|
|
477
|
+
combine independently-Jordan-Wigner-mapped Majorana operators across
|
|
478
|
+
the two sides of a wormhole-teleportation simulation -- a generic
|
|
479
|
+
Pauli-algebra operation with no dependency on that use case, so it
|
|
480
|
+
belongs here instead of duplicated wherever it's next needed.
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
factors : iterable of (coeff, pauli_terms)
|
|
485
|
+
Same (coeff, pauli_terms) pair format pauli_hamiltonian_to_matrix's
|
|
486
|
+
`terms` accepts -- no bare-term shorthand (a Python int coefficient
|
|
487
|
+
would be indistinguishable from a bare (qubit, pauli) pair, e.g.
|
|
488
|
+
(1, 'X'), so this deliberately doesn't try to support one; pass
|
|
489
|
+
(1.0, pauli_terms) explicitly instead). Applied left to right --
|
|
490
|
+
the first factor is the leftmost operator in the product.
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
(complex, dict)
|
|
495
|
+
combined_coeff : the product of every factor's own coefficient,
|
|
496
|
+
times the i^k phase accumulated from same-qubit collisions.
|
|
497
|
+
combined_pauli_dict : {qubit: 'X'|'Y'|'Z'}, identity qubits
|
|
498
|
+
dropped -- ready for pauli_hamiltonian_to_matrix / pauli_expectation
|
|
499
|
+
/ another multiply_pauli_terms call.
|
|
500
|
+
|
|
501
|
+
Examples
|
|
502
|
+
--------
|
|
503
|
+
>>> multiply_pauli_terms([(1.0, 'X'), (1.0, 'Y')]) # X0 * Y0 = i*Z0
|
|
504
|
+
(1j, {0: 'Z'})
|
|
505
|
+
>>> multiply_pauli_terms([(2.0, 'X'), (3.0, {1: 'Z'})]) # disjoint qubits, no collision
|
|
506
|
+
(6.0, {0: 'X', 1: 'Z'})
|
|
507
|
+
"""
|
|
508
|
+
merged = {}
|
|
509
|
+
total_coeff = 1.0 + 0j
|
|
510
|
+
for coeff, pauli_terms in factors:
|
|
511
|
+
total_coeff *= coeff
|
|
512
|
+
normalized = _normalize_terms(pauli_terms)
|
|
513
|
+
for q, p in normalized.items():
|
|
514
|
+
if q not in merged:
|
|
515
|
+
merged[q] = p
|
|
516
|
+
else:
|
|
517
|
+
phase, new_p = _SAME_QUBIT_PAULI_PRODUCT[(merged[q], p)]
|
|
518
|
+
total_coeff *= phase
|
|
519
|
+
if new_p is None:
|
|
520
|
+
del merged[q]
|
|
521
|
+
else:
|
|
522
|
+
merged[q] = new_p
|
|
523
|
+
return total_coeff, merged
|