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,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Fourier Transform circuit builder -- the textbook cascade of H and
|
|
3
|
+
controlled-phase gates, plus the trailing qubit-order swap, returned as a
|
|
4
|
+
gate-tuple list ready for run_circuit. A standard building block (phase
|
|
5
|
+
estimation, Shor's algorithm, arithmetic circuits) that every general-
|
|
6
|
+
purpose SDK ships (Qiskit's `QFT`, Cirq's `cirq.QuantumFourierTransformGate`).
|
|
7
|
+
"""
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
__all__ = ['qft']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _invert_op(op):
|
|
14
|
+
"""H and SWAP are self-inverse; CP(theta)^-1 = CP(-theta)."""
|
|
15
|
+
if op[0] == 'cp':
|
|
16
|
+
gate, ctrl, tgt, angle = op
|
|
17
|
+
return (gate, ctrl, tgt, -angle)
|
|
18
|
+
return op
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def qft(n_qubits, inverse=False, do_swaps=True):
|
|
22
|
+
"""
|
|
23
|
+
Build the Quantum Fourier Transform circuit on n_qubits.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
n_qubits : int
|
|
28
|
+
Number of qubits, must be >= 1.
|
|
29
|
+
inverse : bool
|
|
30
|
+
Build the inverse QFT instead (same gate set, reversed order,
|
|
31
|
+
negated phase angles).
|
|
32
|
+
do_swaps : bool
|
|
33
|
+
Include the trailing qubit-reversal swaps that put the output in
|
|
34
|
+
the same qubit order as the input. The H/CP cascade alone
|
|
35
|
+
produces the Fourier-transformed amplitudes in bit-reversed
|
|
36
|
+
qubit order; set this to False only if the caller will account
|
|
37
|
+
for that reversal itself (e.g. chaining straight into another
|
|
38
|
+
subroutine that expects it).
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
list[tuple]
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
>>> import dense_evolution as de
|
|
47
|
+
>>> sim = de.DenseSVSimulator(3)
|
|
48
|
+
>>> sim.run_circuit(de.qft(3))
|
|
49
|
+
>>> sim.get_probabilities() # uniform: QFT|000> = equal superposition
|
|
50
|
+
"""
|
|
51
|
+
if n_qubits < 1:
|
|
52
|
+
raise ValueError(f"qft needs at least 1 qubit, got {n_qubits}")
|
|
53
|
+
|
|
54
|
+
ops = []
|
|
55
|
+
for j in range(n_qubits):
|
|
56
|
+
ops.append(('h', j))
|
|
57
|
+
for k in range(j + 1, n_qubits):
|
|
58
|
+
angle = np.pi / (2 ** (k - j))
|
|
59
|
+
ops.append(('cp', k, j, angle))
|
|
60
|
+
if do_swaps:
|
|
61
|
+
for i in range(n_qubits // 2):
|
|
62
|
+
ops.append(('swap', i, n_qubits - 1 - i))
|
|
63
|
+
|
|
64
|
+
if inverse:
|
|
65
|
+
ops = [_invert_op(op) for op in reversed(ops)]
|
|
66
|
+
return ops
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Random circuit generation for benchmarking and fuzz-testing -- the same
|
|
3
|
+
purpose Qiskit's `random_circuit` serves.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
__all__ = ['random_circuit']
|
|
8
|
+
|
|
9
|
+
_1Q_STATIC = ('h', 'x', 'y', 'z', 's', 'sdg', 't', 'tdg', 'sx')
|
|
10
|
+
_1Q_PARAMETRIC = ('rx', 'ry', 'rz')
|
|
11
|
+
_2Q_STATIC = ('cx', 'cz', 'cy', 'swap')
|
|
12
|
+
_ALL_KNOWN = set(_1Q_STATIC) | set(_1Q_PARAMETRIC) | set(_2Q_STATIC)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def random_circuit(n_qubits, n_gates, seed=None, gate_set=None, two_qubit_prob=0.4):
|
|
16
|
+
"""
|
|
17
|
+
Build a random circuit for benchmarking or fuzz-testing.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
n_qubits : int
|
|
22
|
+
Number of qubits, must be >= 1.
|
|
23
|
+
n_gates : int
|
|
24
|
+
Number of gate operations to generate, must be >= 0.
|
|
25
|
+
seed : int | numpy.random.Generator, optional
|
|
26
|
+
Seed (or an existing Generator) for reproducible circuits.
|
|
27
|
+
gate_set : iterable of str, optional
|
|
28
|
+
Restrict generation to this set of gate names, mixing 1- and
|
|
29
|
+
2-qubit gates freely (default spans both: h, x, y, z, s, sdg, t,
|
|
30
|
+
tdg, sx, rx, ry, rz, cx, cz, cy, swap). Unrecognized names raise
|
|
31
|
+
immediately rather than failing later inside run_circuit.
|
|
32
|
+
two_qubit_prob : float
|
|
33
|
+
Probability in [0, 1] of picking a 2-qubit gate at each step.
|
|
34
|
+
Ignored (forced to single-qubit gates) once n_qubits < 2, or once
|
|
35
|
+
gate_set excludes every 2-qubit gate name.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
list[tuple]
|
|
40
|
+
"""
|
|
41
|
+
if n_qubits < 1:
|
|
42
|
+
raise ValueError(f"random_circuit needs at least 1 qubit, got {n_qubits}")
|
|
43
|
+
if n_gates < 0:
|
|
44
|
+
raise ValueError(f"n_gates must be >= 0, got {n_gates}")
|
|
45
|
+
if not (0.0 <= two_qubit_prob <= 1.0):
|
|
46
|
+
raise ValueError(f"two_qubit_prob must be in [0, 1], got {two_qubit_prob}")
|
|
47
|
+
|
|
48
|
+
rng = seed if isinstance(seed, np.random.Generator) else np.random.default_rng(seed)
|
|
49
|
+
|
|
50
|
+
if gate_set is not None:
|
|
51
|
+
gate_set = list(gate_set)
|
|
52
|
+
unknown = set(gate_set) - _ALL_KNOWN
|
|
53
|
+
if unknown:
|
|
54
|
+
raise ValueError(f"unknown gate name(s) in gate_set: {sorted(unknown)}")
|
|
55
|
+
one_q_static = [g for g in gate_set if g in _1Q_STATIC]
|
|
56
|
+
one_q_param = [g for g in gate_set if g in _1Q_PARAMETRIC]
|
|
57
|
+
two_q = [g for g in gate_set if g in _2Q_STATIC]
|
|
58
|
+
else:
|
|
59
|
+
one_q_static = list(_1Q_STATIC)
|
|
60
|
+
one_q_param = list(_1Q_PARAMETRIC)
|
|
61
|
+
two_q = list(_2Q_STATIC) if n_qubits >= 2 else []
|
|
62
|
+
|
|
63
|
+
one_q_pool = one_q_static + one_q_param
|
|
64
|
+
if not one_q_pool and not two_q:
|
|
65
|
+
raise ValueError("gate_set leaves no usable gates for this n_qubits")
|
|
66
|
+
|
|
67
|
+
ops = []
|
|
68
|
+
for _ in range(n_gates):
|
|
69
|
+
use_two_qubit = (
|
|
70
|
+
bool(two_q) and n_qubits >= 2
|
|
71
|
+
and (not one_q_pool or rng.random() < two_qubit_prob)
|
|
72
|
+
)
|
|
73
|
+
if use_two_qubit:
|
|
74
|
+
name = two_q[rng.integers(len(two_q))]
|
|
75
|
+
a, b = rng.choice(n_qubits, size=2, replace=False)
|
|
76
|
+
ops.append((name, int(a), int(b)))
|
|
77
|
+
else:
|
|
78
|
+
name = one_q_pool[rng.integers(len(one_q_pool))]
|
|
79
|
+
q = int(rng.integers(n_qubits))
|
|
80
|
+
if name in _1Q_PARAMETRIC:
|
|
81
|
+
theta = float(rng.uniform(0, 2 * np.pi))
|
|
82
|
+
ops.append((name, q, theta))
|
|
83
|
+
else:
|
|
84
|
+
ops.append((name, q))
|
|
85
|
+
return ops
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import platform
|
|
3
|
+
import psutil
|
|
4
|
+
import matplotlib
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
|
|
7
|
+
from ..config import ensure_x64
|
|
8
|
+
|
|
9
|
+
# NoiseModel/NoiseSpec moved to dense_evolution.noise -- this was the
|
|
10
|
+
# wrong home for them (this module is hardware-capability detection,
|
|
11
|
+
# an unrelated concern). Re-exported here for backward compatibility
|
|
12
|
+
# with existing `from dense_evolution.registry import NoiseModel`-style
|
|
13
|
+
# imports; new code should import from dense_evolution.noise directly.
|
|
14
|
+
from ..noise import NoiseModel, NoiseSpec
|
|
15
|
+
from ..noise.kraus_channels import HAS_JAX
|
|
16
|
+
|
|
17
|
+
__all__ = ["QuantumHardwareRegistry", "NoiseModel", "NoiseSpec", "HAS_JAX", "apply_dark_theme"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QuantumHardwareRegistry:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
# Lazy, not at import time -- see dense_evolution/config.py.
|
|
23
|
+
ensure_x64()
|
|
24
|
+
self.processor = platform.processor()
|
|
25
|
+
self.ram_total = psutil.virtual_memory().total / (1024**3)
|
|
26
|
+
self.ram_avail = psutil.virtual_memory().available / (1024**3)
|
|
27
|
+
self.has_jax = HAS_JAX
|
|
28
|
+
self.has_gpu = self._detect_gpu()
|
|
29
|
+
self.max_dense_qubits = self._get_qubit_limit()
|
|
30
|
+
|
|
31
|
+
def _detect_gpu(self) -> bool:
|
|
32
|
+
try:
|
|
33
|
+
subprocess.check_output(['nvidia-smi'], stderr=subprocess.DEVNULL)
|
|
34
|
+
return True
|
|
35
|
+
except Exception:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def _get_qubit_limit(self) -> int:
|
|
39
|
+
if self.ram_total >= 50: return 28
|
|
40
|
+
elif self.ram_total >= 12: return 24
|
|
41
|
+
return 20
|
|
42
|
+
|
|
43
|
+
def print_diagnostics(self):
|
|
44
|
+
print(f"MAX_DENSE={self.max_dense_qubits}q | JAX={self.has_jax} | GPU={self.has_gpu}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# BUG FIX: this module used to instantiate a module-level
|
|
48
|
+
# `HARDWARE_REGISTRY = QuantumHardwareRegistry()` singleton here -- never
|
|
49
|
+
# referenced anywhere else in the codebase (dead code), but its __init__
|
|
50
|
+
# calls ensure_x64() unconditionally, so merely `import dense_evolution`
|
|
51
|
+
# still forced jax_enable_x64=True at import time even after PR #130
|
|
52
|
+
# supposedly made that lazy (see dense_evolution/config.py) -- the
|
|
53
|
+
# laziness moved into ensure_x64() itself, but this eager singleton
|
|
54
|
+
# construction defeated it one level up. Removed entirely: nothing
|
|
55
|
+
# needs it, and QuantumHardwareRegistry() remains available for any
|
|
56
|
+
# caller who actually wants one, constructed on their own schedule.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def apply_dark_theme():
|
|
60
|
+
"""Dashboard-only diagnostic-plot styling (dark background + GitHub-
|
|
61
|
+
dark-ish palette). Used to run as a `plt.style.use('dark_background')`
|
|
62
|
+
module-level side effect here, so it fired on ANY `import
|
|
63
|
+
dense_evolution` and silently recolored every matplotlib figure a
|
|
64
|
+
caller made afterward, dashboard or not (prog.txt point 2). Now
|
|
65
|
+
opt-in: call this explicitly from the dashboard's own startup."""
|
|
66
|
+
plt.style.use('dark_background')
|
|
67
|
+
matplotlib.rcParams.update({
|
|
68
|
+
'figure.facecolor': '#010409',
|
|
69
|
+
'axes.facecolor': '#0d1117',
|
|
70
|
+
'axes.edgecolor': '#21262d',
|
|
71
|
+
'grid.color': '#21262d',
|
|
72
|
+
'font.family': 'monospace',
|
|
73
|
+
'font.size': 9,
|
|
74
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entangling-layer topology helpers.
|
|
3
|
+
|
|
4
|
+
Every variational circuit (VQE, QAOA, hardware-efficient ansätze) needs an
|
|
5
|
+
entangling layer, and hand-writing it as a `for` loop of two-qubit gates is
|
|
6
|
+
one of the most repeated patterns across quantum-circuit code. Other
|
|
7
|
+
libraries give it a name and a single call instead (Qiskit's
|
|
8
|
+
`TwoLocal(entanglement=...)`, PennyLane's `qml.broadcast(pattern=...)`).
|
|
9
|
+
`entangling_layer` is the Dense-Evolution equivalent: it returns a plain
|
|
10
|
+
list of gate tuples in the circuit format `run_circuit` already accepts, so
|
|
11
|
+
it drops straight into any existing circuit list via concatenation.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
VALID_PATTERNS = ('linear', 'circular', 'full', 'star', 'brick')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def entangling_layer(n_qubits, pattern='linear', gate='cx', reverse=False, hub=0):
|
|
18
|
+
"""
|
|
19
|
+
Build a list of two-qubit gate tuples connecting n_qubits according to
|
|
20
|
+
a named topology.
|
|
21
|
+
|
|
22
|
+
Patterns
|
|
23
|
+
--------
|
|
24
|
+
'linear' -- chain: (0,1), (1,2), ..., (n-2,n-1)
|
|
25
|
+
'circular' -- linear + one wraparound edge (n-1,0) ("ring"); identical
|
|
26
|
+
to 'linear' when n_qubits == 2, since there is only one
|
|
27
|
+
possible edge between two qubits
|
|
28
|
+
'full' -- every pair (i,j) with i<j ("complete"/all-to-all)
|
|
29
|
+
'star' -- a single hub qubit connected to every other qubit
|
|
30
|
+
'brick' -- alternating even/odd layers: (0,1)(2,3).. then (1,2)(3,4)..
|
|
31
|
+
("brickwork"/staircase, the pattern behind most Trotterized
|
|
32
|
+
and hardware-efficient ansätze)
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
n_qubits : int
|
|
37
|
+
Number of qubits involved, must be >= 2.
|
|
38
|
+
pattern : str
|
|
39
|
+
One of VALID_PATTERNS.
|
|
40
|
+
gate : str
|
|
41
|
+
Two-qubit gate name applied to every edge (e.g. 'cx', 'cz', 'cy').
|
|
42
|
+
Not validated against dense_evolution.gates.GATES here, so a custom
|
|
43
|
+
gate name registered elsewhere still works.
|
|
44
|
+
reverse : bool
|
|
45
|
+
Swap (control, target) -> (target, control) for every edge. Some
|
|
46
|
+
ansätze alternate direction layer to layer for symmetry.
|
|
47
|
+
hub : int
|
|
48
|
+
Hub qubit index, only used by pattern='star'.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
list[tuple[str, int, int]]
|
|
53
|
+
"""
|
|
54
|
+
if n_qubits < 2:
|
|
55
|
+
raise ValueError(f"entangling_layer needs at least 2 qubits, got {n_qubits}")
|
|
56
|
+
if pattern not in VALID_PATTERNS:
|
|
57
|
+
raise ValueError(f"unknown pattern {pattern!r}, expected one of {VALID_PATTERNS}")
|
|
58
|
+
|
|
59
|
+
if pattern == 'linear':
|
|
60
|
+
edges = [(q, q + 1) for q in range(n_qubits - 1)]
|
|
61
|
+
elif pattern == 'circular':
|
|
62
|
+
edges = [(q, q + 1) for q in range(n_qubits - 1)]
|
|
63
|
+
if n_qubits > 2:
|
|
64
|
+
edges.append((n_qubits - 1, 0))
|
|
65
|
+
elif pattern == 'full':
|
|
66
|
+
edges = [(i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)]
|
|
67
|
+
elif pattern == 'star':
|
|
68
|
+
if not (0 <= hub < n_qubits):
|
|
69
|
+
raise ValueError(f"hub={hub} out of range for n_qubits={n_qubits}")
|
|
70
|
+
edges = [(hub, q) for q in range(n_qubits) if q != hub]
|
|
71
|
+
elif pattern == 'brick':
|
|
72
|
+
even = [(q, q + 1) for q in range(0, n_qubits - 1, 2)]
|
|
73
|
+
odd = [(q, q + 1) for q in range(1, n_qubits - 1, 2)]
|
|
74
|
+
edges = even + odd
|
|
75
|
+
|
|
76
|
+
if reverse:
|
|
77
|
+
edges = [(b, a) for a, b in edges]
|
|
78
|
+
|
|
79
|
+
return [(gate, a, b) for a, b in edges]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-time Hamiltonian evolution as an actual gate circuit (Trotterization)
|
|
3
|
+
-- did not exist anywhere in this package before. Every existing piece of
|
|
4
|
+
"evolution" machinery here is either gate-based-and-fixed (a hand-written
|
|
5
|
+
or VQE-optimized circuit template) or exact-and-not-a-circuit
|
|
6
|
+
(dashboard_core.hamiltonians.ground_state_energy's dense diagonalization).
|
|
7
|
+
Nothing composed exp(-i*H*t) for an arbitrary Hamiltonian into gates a
|
|
8
|
+
real quantum computer could run.
|
|
9
|
+
|
|
10
|
+
Originated in research/wormhole_syk.py, where it closed an explicit,
|
|
11
|
+
previously-open follow-on: reproducing a traversable-wormhole-teleportation
|
|
12
|
+
signal (arXiv:2604.10090) first via exact matrix exponentiation (cheap,
|
|
13
|
+
but not what real hardware executes), then via this module's Trotterized
|
|
14
|
+
gate circuit -- verified the signal wasn't an artifact of the exact-
|
|
15
|
+
evolution shortcut, it survives with real gates too. Neither function
|
|
16
|
+
here is specific to that experiment or to SYK physics; both drop
|
|
17
|
+
straight into any future feature needing exp(-i*H*t) as gates (a
|
|
18
|
+
Trotterized VQE-adjacent ansatz, quench dynamics, etc.).
|
|
19
|
+
|
|
20
|
+
pauli_rotation_ops is exact for a single Pauli-string term (fidelity
|
|
21
|
+
1.0 against scipy.linalg.expm, verified in tests/unit/test_trotter.py for
|
|
22
|
+
1-4 qubit mixed X/Y/Z strings, not just Z-strings); trotter_evolve_ops
|
|
23
|
+
composes many such terms via the first-order product formula by default,
|
|
24
|
+
which is an *approximation* whose error shrinks as n_steps grows (also
|
|
25
|
+
verified: infidelity drops roughly 4x per doubling of steps against a
|
|
26
|
+
real, non-trivial multi-qubit Hamiltonian, consistent with the expected
|
|
27
|
+
quadratic convergence of first-order Trotter error in state overlap).
|
|
28
|
+
|
|
29
|
+
order=2 selects the second-order (Strang/symmetric) product formula
|
|
30
|
+
instead -- each step applies the terms forward at half the angle, then
|
|
31
|
+
backward (reversed order) at half the angle again:
|
|
32
|
+
[prod_k exp(-i*c_k*P_k*dt/2)] * [prod_k(reversed) exp(-i*c_k*P_k*dt/2)],
|
|
33
|
+
which cancels the first-order formula's leading error term (verified in
|
|
34
|
+
tests/unit/test_trotter.py: infidelity drops roughly 16x per doubling of
|
|
35
|
+
steps, consistent with the expected quartic convergence of second-order
|
|
36
|
+
Trotter error in state overlap, vs. order=1's ~4x). Costs 2x the gates
|
|
37
|
+
of order=1 for the same n_steps -- the standard second-order tradeoff,
|
|
38
|
+
worth it when n_steps would otherwise need to be large for accuracy
|
|
39
|
+
(e.g. the noise-robustness experiments in wormhole_syk_teleportation.py,
|
|
40
|
+
where gate count directly limits how much depolarizing noise the
|
|
41
|
+
circuit accumulates).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import jax
|
|
45
|
+
import jax.numpy as jnp
|
|
46
|
+
from jax.scipy.linalg import expm
|
|
47
|
+
|
|
48
|
+
__all__ = ['pauli_rotation_ops', 'trotter_evolve_ops', 'continuous_pulse_evolve',
|
|
49
|
+
'continuous_dissipative_evolve']
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pauli_rotation_ops(pauli_dict, angle):
|
|
53
|
+
"""Gate-tuple circuit for exp(-i*angle*P), P a Pauli string given as
|
|
54
|
+
{qubit: 'X'/'Y'/'Z'} -- basis-change + CNOT-staircase + RZ + inverse,
|
|
55
|
+
the same identity already used elsewhere in this codebase
|
|
56
|
+
(dashboard_core.vqe's UCCSD/QAOA-style ZZ interactions) generalized
|
|
57
|
+
here to arbitrary mixed X/Y/Z strings, not just Z-strings.
|
|
58
|
+
|
|
59
|
+
This package's rz(theta) = exp(-i*theta/2*Z) (checked directly
|
|
60
|
+
against scipy.linalg.expm when this was written, not assumed from
|
|
61
|
+
convention) -- rz(2*angle) on the accumulator qubit therefore gives
|
|
62
|
+
exactly exp(-i*angle*Z) on the accumulated parity.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
pauli_dict : dict
|
|
67
|
+
{qubit: 'X'|'Y'|'Z'}. An empty dict (identity term) returns [].
|
|
68
|
+
angle : float
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
list[tuple]
|
|
73
|
+
Gate tuples ready for DenseSVSimulator.run_circuit /
|
|
74
|
+
QASMParser-compatible circuits.
|
|
75
|
+
"""
|
|
76
|
+
qubits = sorted(pauli_dict.keys())
|
|
77
|
+
if not qubits:
|
|
78
|
+
return []
|
|
79
|
+
ops = []
|
|
80
|
+
for q in qubits:
|
|
81
|
+
letter = pauli_dict[q]
|
|
82
|
+
if letter == 'X':
|
|
83
|
+
ops.append(('h', q))
|
|
84
|
+
elif letter == 'Y':
|
|
85
|
+
ops.append(('sdg', q))
|
|
86
|
+
ops.append(('h', q))
|
|
87
|
+
for i in range(len(qubits) - 1):
|
|
88
|
+
ops.append(('cx', qubits[i], qubits[i + 1]))
|
|
89
|
+
ops.append(('rz', qubits[-1], 2 * angle))
|
|
90
|
+
for i in reversed(range(len(qubits) - 1)):
|
|
91
|
+
ops.append(('cx', qubits[i], qubits[i + 1]))
|
|
92
|
+
for q in qubits:
|
|
93
|
+
letter = pauli_dict[q]
|
|
94
|
+
if letter == 'X':
|
|
95
|
+
ops.append(('h', q))
|
|
96
|
+
elif letter == 'Y':
|
|
97
|
+
ops.append(('h', q))
|
|
98
|
+
ops.append(('s', q))
|
|
99
|
+
return ops
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def trotter_evolve_ops(terms, t, n_steps, order=1):
|
|
103
|
+
"""Trotter product formula for exp(-i*H*t), H = sum_k c_k*P_k.
|
|
104
|
+
|
|
105
|
+
order=1 (default): [prod_k exp(-i*c_k*P_k*(t/n_steps))]^n_steps.
|
|
106
|
+
Term order within one step follows `terms`' own order, identical
|
|
107
|
+
every repetition (not re-randomized per step).
|
|
108
|
+
|
|
109
|
+
order=2: Strang/symmetric splitting -- each step is a forward half-
|
|
110
|
+
angle pass through `terms` followed by a backward half-angle pass
|
|
111
|
+
through `terms` reversed, [prod_k exp(-i*c_k*P_k*dt/2)] *
|
|
112
|
+
[prod_k(reversed) exp(-i*c_k*P_k*dt/2)], repeated n_steps times.
|
|
113
|
+
Quadratically more accurate than order=1 for the same n_steps (see
|
|
114
|
+
module docstring), at 2x the gate count per step.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
terms : list[(float, dict)]
|
|
119
|
+
(coefficient, pauli_dict) pairs, e.g. from
|
|
120
|
+
dense_evolution.pauli_hamiltonian_to_matrix's own term format,
|
|
121
|
+
or dense_evolution.majorana_pauli_terms products.
|
|
122
|
+
t : float
|
|
123
|
+
Total evolution time.
|
|
124
|
+
n_steps : int
|
|
125
|
+
Number of Trotter steps -- higher is more accurate and more
|
|
126
|
+
gates, the standard Trotter accuracy/cost tradeoff.
|
|
127
|
+
order : int
|
|
128
|
+
1 (default) or 2 -- see above.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
list[tuple]
|
|
133
|
+
Gate tuples for the whole Trotterized evolution.
|
|
134
|
+
"""
|
|
135
|
+
if order not in (1, 2):
|
|
136
|
+
raise ValueError(f"order must be 1 or 2, got {order}")
|
|
137
|
+
dt = t / n_steps
|
|
138
|
+
if order == 1:
|
|
139
|
+
step_ops = []
|
|
140
|
+
for c, pdict in terms:
|
|
141
|
+
step_ops.extend(pauli_rotation_ops(pdict, c * dt))
|
|
142
|
+
else:
|
|
143
|
+
half_dt = dt / 2
|
|
144
|
+
step_ops = []
|
|
145
|
+
for c, pdict in terms:
|
|
146
|
+
step_ops.extend(pauli_rotation_ops(pdict, c * half_dt))
|
|
147
|
+
for c, pdict in reversed(terms):
|
|
148
|
+
step_ops.extend(pauli_rotation_ops(pdict, c * half_dt))
|
|
149
|
+
return step_ops * n_steps
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def continuous_pulse_evolve(psi0, hamiltonian_fn, coeffs_t, dt, observable_fn=None):
|
|
153
|
+
"""Evolve a statevector under a time-dependent Hamiltonian via
|
|
154
|
+
jax.lax.scan, generalized out of a pattern first written ad hoc for a
|
|
155
|
+
real time-dependent pulse (Dense-Evolution-Discovery's
|
|
156
|
+
germanium_iswap_validation.py, exact_final_state/exact_final_state_general
|
|
157
|
+
-- a 56ns raised-cosine baseband iSWAP pulse, arXiv:2608.16716). That
|
|
158
|
+
script's own Trotterized-gate-circuit version of the same pulse
|
|
159
|
+
(build_pulse_circuit) instead builds a plain Python list of gate tuples,
|
|
160
|
+
one exp(-i*H*dt) per slice via pauli_rotation_ops -- fine for producing a
|
|
161
|
+
circuit a discrete-gate simulator can run, but not what this function is
|
|
162
|
+
for: this evolves the statevector directly, slice by slice, entirely
|
|
163
|
+
inside JAX, with no Python-side list that grows with the number of
|
|
164
|
+
slices (the O(1)-per-step scan carry is the whole point -- many slices
|
|
165
|
+
for a finely-resolved pulse cost compile time, not accumulating Python
|
|
166
|
+
memory).
|
|
167
|
+
|
|
168
|
+
Not specific to any one Hamiltonian, qubit count, or pulse shape --
|
|
169
|
+
`hamiltonian_fn` supplies the (possibly qubit-count-dependent) operator
|
|
170
|
+
for a given instantaneous coefficient, and `coeffs_t` can be any sampled
|
|
171
|
+
time-dependent profile (a smooth pulse envelope, a sudden burst, a
|
|
172
|
+
constant array for a time-independent Hamiltonian, etc.).
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
psi0 : array_like
|
|
177
|
+
Initial statevector, shape (2**n_qubits,).
|
|
178
|
+
hamiltonian_fn : callable
|
|
179
|
+
coeff -> Hamiltonian matrix, shape (2**n_qubits, 2**n_qubits), for
|
|
180
|
+
that instant's coefficient. Called once per entry of `coeffs_t`
|
|
181
|
+
under jax.lax.scan, so it must be JAX-traceable.
|
|
182
|
+
coeffs_t : array_like
|
|
183
|
+
Per-slice instantaneous coefficient, one entry per time slice
|
|
184
|
+
(e.g. a peak amplitude times a sampled pulse envelope). The
|
|
185
|
+
evolution applies exp(-i*hamiltonian_fn(coeff)*dt) for each entry,
|
|
186
|
+
in order.
|
|
187
|
+
dt : float
|
|
188
|
+
Duration of one slice (coeffs_t is assumed sampled on a uniform
|
|
189
|
+
grid of this spacing -- same convention as the germanium
|
|
190
|
+
experiment's dt=0.05 ns midpoint/linspace sampling).
|
|
191
|
+
observable_fn : callable, optional
|
|
192
|
+
If given, applied to the statevector after each slice; the stacked
|
|
193
|
+
per-slice results are returned as `trajectory` (mirrors the
|
|
194
|
+
experiment's own step_record, used there to plot |01>/|10>
|
|
195
|
+
occupation probability over the pulse). If omitted, `trajectory`
|
|
196
|
+
is None and only the final state is computed.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
final_psi : jnp.ndarray
|
|
201
|
+
trajectory : jnp.ndarray or None
|
|
202
|
+
"""
|
|
203
|
+
def step(psi, coeff):
|
|
204
|
+
H_t = hamiltonian_fn(coeff)
|
|
205
|
+
U_step = expm(-1j * H_t * dt)
|
|
206
|
+
next_psi = jnp.dot(U_step, psi)
|
|
207
|
+
y = observable_fn(next_psi) if observable_fn is not None else None
|
|
208
|
+
return next_psi, y
|
|
209
|
+
|
|
210
|
+
final_psi, trajectory = jax.lax.scan(step, jnp.asarray(psi0), jnp.asarray(coeffs_t))
|
|
211
|
+
return final_psi, trajectory
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def continuous_dissipative_evolve(rho0, channel_fn, params_t, observable_fn=None):
|
|
215
|
+
"""Evolve a density matrix through a time-dependent open-system (CPTP)
|
|
216
|
+
channel via jax.lax.scan -- the dissipative counterpart of
|
|
217
|
+
`continuous_pulse_evolve`, which only ever does unitary exp(-i*H*dt)
|
|
218
|
+
steps on a pure state.
|
|
219
|
+
|
|
220
|
+
Needed because not every real time-dependent physical event is coherent.
|
|
221
|
+
E.g. a cosmic-ray/gamma impact on a superconducting qubit chip (real
|
|
222
|
+
data: McEwen et al., arXiv:2104.05219) produces a burst of quasiparticles
|
|
223
|
+
that transiently collapses the chip's effective T1 -- a rise (~10us to a
|
|
224
|
+
first plateau, ~1ms to near-saturation) followed by a ~25-30ms
|
|
225
|
+
exponential decay back to baseline, measured directly, not modeled as a
|
|
226
|
+
static before/after depolarizing parameter. That is dissipation with a
|
|
227
|
+
time-varying rate, which cannot be expressed as a coefficient inside a
|
|
228
|
+
Hermitian Hamiltonian and passed to `continuous_pulse_evolve` -- it has
|
|
229
|
+
to act on rho through an actual CPTP map at each instant.
|
|
230
|
+
|
|
231
|
+
`channel_fn` supplies that per-slice CPTP map (e.g.
|
|
232
|
+
`dense_evolution.global_depolarizing_channel`, or any other Kraus
|
|
233
|
+
channel taking a time-varying parameter), so this function is not
|
|
234
|
+
specific to any one noise mechanism, exactly like `continuous_pulse_evolve`
|
|
235
|
+
is not specific to any one Hamiltonian.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
rho0 : array_like
|
|
240
|
+
Initial density matrix, shape (dim, dim).
|
|
241
|
+
channel_fn : callable
|
|
242
|
+
(rho, param) -> rho_next, a single-slice CPTP map. Called once per
|
|
243
|
+
entry of `params_t` under jax.lax.scan, so it must be
|
|
244
|
+
JAX-traceable.
|
|
245
|
+
params_t : array_like
|
|
246
|
+
Per-slice instantaneous channel parameter (e.g. a depolarizing/
|
|
247
|
+
decay probability sampled on a time grid reproducing a measured
|
|
248
|
+
event's rise-and-decay profile).
|
|
249
|
+
observable_fn : callable, optional
|
|
250
|
+
If given, applied to rho after each slice; the stacked per-slice
|
|
251
|
+
results are returned as `trajectory`. If omitted, `trajectory` is
|
|
252
|
+
None and only the final density matrix is computed.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
final_rho : jnp.ndarray
|
|
257
|
+
trajectory : jnp.ndarray or None
|
|
258
|
+
"""
|
|
259
|
+
def step(rho, param):
|
|
260
|
+
rho_next = channel_fn(rho, param)
|
|
261
|
+
y = observable_fn(rho_next) if observable_fn is not None else None
|
|
262
|
+
return rho_next, y
|
|
263
|
+
|
|
264
|
+
final_rho, trajectory = jax.lax.scan(step, jnp.asarray(rho0), jnp.asarray(params_t))
|
|
265
|
+
return final_rho, trajectory
|