dense-evolution 8.3.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. dashboard_core/__init__.py +115 -0
  2. dashboard_core/_gate_tables.py +30 -0
  3. dashboard_core/band_structure.py +71 -0
  4. dashboard_core/circuit_builder_component.py +232 -0
  5. dashboard_core/circuit_diagram.py +216 -0
  6. dashboard_core/crypto_protocols.py +77 -0
  7. dashboard_core/engine.py +326 -0
  8. dashboard_core/graphical_builder.py +114 -0
  9. dashboard_core/hamiltonians.py +593 -0
  10. dashboard_core/mass_decomposition_tool.py +47 -0
  11. dashboard_core/mitigation.py +343 -0
  12. dashboard_core/native_hf_diagnostics.py +62 -0
  13. dashboard_core/noise_tools.py +125 -0
  14. dashboard_core/qasm_library.py +233 -0
  15. dashboard_core/qmmm.py +16 -0
  16. dashboard_core/rag_tool.py +45 -0
  17. dashboard_core/state_visuals.py +288 -0
  18. dashboard_core/system_limits.py +60 -0
  19. dashboard_core/vector_healing.py +102 -0
  20. dashboard_core/visuals.py +158 -0
  21. dashboard_core/vqe.py +533 -0
  22. dashboard_core/wormhole.py +580 -0
  23. dense_evolution/__init__.py +114 -0
  24. dense_evolution/autodiff.py +10 -0
  25. dense_evolution/backends/__init__.py +5 -0
  26. dense_evolution/backends/chunk/__init__.py +37 -0
  27. dense_evolution/backends/chunk/_engine_imports.py +57 -0
  28. dense_evolution/backends/chunk/circuit_chunker.py +55 -0
  29. dense_evolution/backends/chunk/core.py +432 -0
  30. dense_evolution/backends/chunk/disk_overflow.py +232 -0
  31. dense_evolution/backends/chunk/geometry.py +95 -0
  32. dense_evolution/backends/chunk/guard.py +190 -0
  33. dense_evolution/backends/chunk/kernels.py +531 -0
  34. dense_evolution/backends/mps.py +1569 -0
  35. dense_evolution/backends/statevector.py +616 -0
  36. dense_evolution/chunk.py +25 -0
  37. dense_evolution/circuits/__init__.py +20 -0
  38. dense_evolution/circuits/compiler.py +488 -0
  39. dense_evolution/circuits/diagram.py +94 -0
  40. dense_evolution/circuits/gates.py +91 -0
  41. dense_evolution/circuits/parser.py +632 -0
  42. dense_evolution/circuits/qft.py +66 -0
  43. dense_evolution/circuits/random_circuit.py +85 -0
  44. dense_evolution/circuits/registry.py +74 -0
  45. dense_evolution/circuits/topology.py +79 -0
  46. dense_evolution/circuits/trotter.py +265 -0
  47. dense_evolution/circuits/uccsd.py +275 -0
  48. dense_evolution/cli.py +199 -0
  49. dense_evolution/compiler.py +9 -0
  50. dense_evolution/config.py +49 -0
  51. dense_evolution/drawing.py +10 -0
  52. dense_evolution/entropy.py +9 -0
  53. dense_evolution/fermions.py +9 -0
  54. dense_evolution/gates.py +9 -0
  55. dense_evolution/harrison_tb.py +16 -0
  56. dense_evolution/healing.py +18 -0
  57. dense_evolution/interop/__init__.py +18 -0
  58. dense_evolution/interop/qiskit_pennylane.py +406 -0
  59. dense_evolution/measurement.py +10 -0
  60. dense_evolution/mitigation/__init__.py +54 -0
  61. dense_evolution/mitigation/healing.py +215 -0
  62. dense_evolution/mitigation/kl_divergence.py +93 -0
  63. dense_evolution/mitigation/magic_entropy.py +163 -0
  64. dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
  65. dense_evolution/mitigation/renyi.py +168 -0
  66. dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
  67. dense_evolution/mitigation/zne.py +990 -0
  68. dense_evolution/mps.py +9 -0
  69. dense_evolution/native_hf/__init__.py +26 -0
  70. dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
  71. dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
  72. dense_evolution/native_hf/assembly.py +304 -0
  73. dense_evolution/native_hf/basis.py +117 -0
  74. dense_evolution/native_hf/boys.py +35 -0
  75. dense_evolution/native_hf/bridge.py +112 -0
  76. dense_evolution/native_hf/cartesian.py +64 -0
  77. dense_evolution/native_hf/coulomb.py +196 -0
  78. dense_evolution/native_hf/differentiable.py +53 -0
  79. dense_evolution/native_hf/gaussians.py +79 -0
  80. dense_evolution/native_hf/kinetic.py +52 -0
  81. dense_evolution/native_hf/libcint_bridge.py +167 -0
  82. dense_evolution/native_hf/overlap.py +91 -0
  83. dense_evolution/native_hf/scf.py +404 -0
  84. dense_evolution/noise/__init__.py +79 -0
  85. dense_evolution/noise/coherent_attack.py +264 -0
  86. dense_evolution/noise/cosmic_ray.py +61 -0
  87. dense_evolution/noise/density_matrix_channels.py +78 -0
  88. dense_evolution/noise/differentiable.py +66 -0
  89. dense_evolution/noise/kraus/__init__.py +6 -0
  90. dense_evolution/noise/kraus/amplitude_damping.py +47 -0
  91. dense_evolution/noise/kraus/bitflip.py +22 -0
  92. dense_evolution/noise/kraus/combined.py +16 -0
  93. dense_evolution/noise/kraus/depolarizing.py +47 -0
  94. dense_evolution/noise/kraus/ideal.py +10 -0
  95. dense_evolution/noise/kraus/phaseflip.py +21 -0
  96. dense_evolution/noise/kraus_channels.py +285 -0
  97. dense_evolution/noise/oscillating.py +32 -0
  98. dense_evolution/noise/pink.py +80 -0
  99. dense_evolution/observables.py +11 -0
  100. dense_evolution/parser.py +9 -0
  101. dense_evolution/physics/__init__.py +27 -0
  102. dense_evolution/physics/entropy.py +161 -0
  103. dense_evolution/physics/fermions.py +322 -0
  104. dense_evolution/physics/observables.py +523 -0
  105. dense_evolution/physics/qec.py +1113 -0
  106. dense_evolution/physics/spectral.py +143 -0
  107. dense_evolution/physics/states.py +43 -0
  108. dense_evolution/protocols/__init__.py +27 -0
  109. dense_evolution/protocols/bb84.py +133 -0
  110. dense_evolution/protocols/di_qkd_ghz.py +199 -0
  111. dense_evolution/protocols/dicka_protocol2.py +124 -0
  112. dense_evolution/qec.py +20 -0
  113. dense_evolution/qft.py +9 -0
  114. dense_evolution/qmmm/__init__.py +13 -0
  115. dense_evolution/qmmm/ase_bridge.py +97 -0
  116. dense_evolution/qmmm/forces.py +388 -0
  117. dense_evolution/qmmm/propagation.py +80 -0
  118. dense_evolution/qmmm/region.py +137 -0
  119. dense_evolution/random_circuit.py +15 -0
  120. dense_evolution/registry.py +9 -0
  121. dense_evolution/simulator.py +10 -0
  122. dense_evolution/solvers/__init__.py +19 -0
  123. dense_evolution/solvers/autodiff.py +169 -0
  124. dense_evolution/solvers/harrison_tb.py +189 -0
  125. dense_evolution/solvers/vhd_tb.py +187 -0
  126. dense_evolution/states.py +9 -0
  127. dense_evolution/topology.py +9 -0
  128. dense_evolution/trotter.py +9 -0
  129. dense_evolution/utils/__init__.py +13 -0
  130. dense_evolution/utils/drawing.py +101 -0
  131. dense_evolution/utils/mass_decomposition.py +246 -0
  132. dense_evolution/utils/measurement.py +94 -0
  133. dense_evolution/vhd_tb.py +16 -0
  134. dense_evolution-8.3.0.dist-info/METADATA +366 -0
  135. dense_evolution-8.3.0.dist-info/RECORD +165 -0
  136. dense_evolution-8.3.0.dist-info/WHEEL +5 -0
  137. dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
  138. dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
  139. dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
  140. ia_utils/__init__.py +0 -0
  141. ia_utils/adversarial_vector_attack.py +196 -0
  142. ia_utils/rag.py +288 -0
  143. ia_utils/vector_healing.py +399 -0
  144. local_site/__init__.py +0 -0
  145. local_site/app/__init__.py +0 -0
  146. local_site/app/server.py +1009 -0
  147. mcp_server/__init__.py +0 -0
  148. mcp_server/client.py +324 -0
  149. mcp_server/config.py +32 -0
  150. mcp_server/models.py +347 -0
  151. mcp_server/molecules.py +71 -0
  152. mcp_server/server.py +119 -0
  153. mcp_server/tools/__init__.py +0 -0
  154. mcp_server/tools/chemistry_tools.py +225 -0
  155. mcp_server/tools/circuit_tools.py +83 -0
  156. mcp_server/tools/crypto_tools.py +66 -0
  157. mcp_server/tools/mitigation_tools.py +81 -0
  158. mcp_server/tools/noise_tools.py +60 -0
  159. mcp_server/tools/retrieval_tools.py +44 -0
  160. mcp_server/tools/system_tools.py +149 -0
  161. mcp_server/tools/wormhole_tools.py +142 -0
  162. mcp_server/utils/__init__.py +0 -0
  163. mcp_server/utils/cache.py +55 -0
  164. mcp_server/utils/images.py +67 -0
  165. mcp_server/utils/truncation.py +38 -0
@@ -0,0 +1,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