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,149 @@
1
+ """Tools: kernel status and catalogs (presets, gates, noise models,
2
+ molecules). Registered against the shared `mcp` instance created in
3
+ server.py -- see that module's docstring for why importing `mcp` back
4
+ from there (rather than the other way around) is safe despite looking
5
+ circular."""
6
+ import json
7
+
8
+ from .. import client
9
+ from ..client import _request, catch_errors
10
+ from ..config import READ_ONLY_IDEMPOTENT
11
+ from ..models import ListMoleculesInput
12
+ from ..molecules import _get_annotated_molecule_catalog, _molecule_catalog_cache
13
+ from ..server import mcp
14
+ from ..utils import images as _images
15
+
16
+
17
+ @mcp.tool(name="dense_evolution_health", annotations={"title": "Check Dense Evolution kernel status", **READ_ONLY_IDEMPOTENT})
18
+ @catch_errors
19
+ async def dense_evolution_health() -> str:
20
+ """Check whether the local Dense Evolution Composer kernel is running and reachable.
21
+
22
+ Always call this first if unsure whether the kernel is up -- every other
23
+ tool in this server depends on it. Returns the kernel's dense_evolution
24
+ version, hostname, and free/total RAM (useful to sanity-check qubit-count
25
+ limits before requesting a large simulation).
26
+
27
+ Returns:
28
+ str: JSON with {status, dense_evolution_version, hostname,
29
+ total_ram_gb, available_ram_gb, ram_percent_free}, plus a
30
+ `version_mismatch` warning if the kernel's dense_evolution differs
31
+ from the one importable in this MCP adapter's own environment (a
32
+ stale kernel process left running across a `pip install --upgrade
33
+ dense-evolution` silently serves the old version's behavior --
34
+ this makes that visible instead of a confusing missing-feature
35
+ report), or an "Error: ..." string if the kernel is not running.
36
+ """
37
+ result = await _request("GET", "/api/health", timeout=5.0)
38
+ mismatch = client.version_mismatch_message(result)
39
+ if mismatch is not None:
40
+ result["version_mismatch"] = mismatch
41
+ return json.dumps(result, indent=2)
42
+
43
+
44
+ @mcp.tool(name="dense_evolution_system_limits", annotations={"title": "Get max safe qubit count", **READ_ONLY_IDEMPOTENT})
45
+ @catch_errors
46
+ async def dense_evolution_system_limits() -> str:
47
+ """Get the maximum qubit count this machine can currently simulate with
48
+ a dense statevector, computed from actual free RAM right now (not a
49
+ fixed constant) -- call before requesting a large `dense_evolution_run_circuit`.
50
+
51
+ Returns:
52
+ str: JSON describing the current safe qubit ceiling for the dense backend.
53
+ """
54
+ return json.dumps(await _request("GET", "/api/system_limits", timeout=5.0), indent=2)
55
+
56
+
57
+ @mcp.tool(name="dense_evolution_kernel_status", annotations={"title": "Inspect this MCP adapter's own local state", **READ_ONLY_IDEMPOTENT})
58
+ @catch_errors
59
+ async def dense_evolution_kernel_status() -> str:
60
+ """Report this MCP adapter's own local configuration and diagnostics --
61
+ distinct from dense_evolution_health, which only proxies the kernel's
62
+ own /api/health. Useful for debugging the adapter process itself:
63
+ which kernel URL it's pointed at, whether that kernel currently
64
+ answers, how many circuit/histogram/Q-sphere PNGs have piled up on
65
+ disk (see the module docstring on why images are saved to disk
66
+ instead of inlined), and how warm the molecule-catalog cache is.
67
+
68
+ Returns:
69
+ str: JSON with {kernel_url, kernel_reachable, image_output_dir,
70
+ image_count, image_max_files, molecule_cache_entries}.
71
+ """
72
+ try:
73
+ await _request("GET", "/api/health", timeout=5.0)
74
+ reachable = True
75
+ except Exception:
76
+ reachable = False
77
+
78
+ image_dir = _images.IMAGE_OUTPUT_DIR
79
+ image_count = len(list(image_dir.glob("*.png"))) if image_dir.exists() else 0
80
+
81
+ return json.dumps({
82
+ "kernel_url": client.KERNEL_URL,
83
+ "kernel_reachable": reachable,
84
+ "image_output_dir": str(image_dir),
85
+ "image_count": image_count,
86
+ "image_max_files": _images.IMAGE_MAX_FILES,
87
+ "molecule_cache_entries": len(_molecule_catalog_cache),
88
+ }, indent=2)
89
+
90
+
91
+ @mcp.tool(name="dense_evolution_list_presets", annotations={"title": "List preset OpenQASM circuits", **READ_ONLY_IDEMPOTENT})
92
+ @catch_errors
93
+ async def dense_evolution_list_presets() -> str:
94
+ """List the built-in example OpenQASM circuits bundled with the Composer
95
+ (e.g. Bell state, GHZ, QFT) -- useful as ready-made input for
96
+ `dense_evolution_run_circuit` without writing QASM by hand.
97
+
98
+ Returns:
99
+ str: JSON mapping preset names to their OpenQASM source text.
100
+ """
101
+ return json.dumps(await _request("GET", "/api/presets", timeout=5.0), indent=2)
102
+
103
+
104
+ @mcp.tool(name="dense_evolution_list_gates", annotations={"title": "List available quantum gates", **READ_ONLY_IDEMPOTENT})
105
+ @catch_errors
106
+ async def dense_evolution_list_gates() -> str:
107
+ """List every gate the graphical circuit builder (and therefore
108
+ `dense_evolution_build_circuit`) supports, with their display metadata.
109
+
110
+ Returns:
111
+ str: JSON gate palette (name, symbol, qubit arity, etc. per gate).
112
+ """
113
+ return json.dumps(await _request("GET", "/api/palette", timeout=5.0), indent=2)
114
+
115
+
116
+ @mcp.tool(name="dense_evolution_list_noise_models", annotations={"title": "List available noise models", **READ_ONLY_IDEMPOTENT})
117
+ @catch_errors
118
+ async def dense_evolution_list_noise_models() -> str:
119
+ """List the real Kraus-channel noise models available for
120
+ `dense_evolution_run_circuit` and the mitigation tools (e.g.
121
+ depolarizing, amplitude damping, bit-flip).
122
+
123
+ Returns:
124
+ str: JSON mapping noise model names to their parameters/description.
125
+ """
126
+ return json.dumps(await _request("GET", "/api/noise_models", timeout=5.0), indent=2)
127
+
128
+
129
+ @mcp.tool(name="dense_evolution_list_molecules", annotations={"title": "List catalog molecules", **READ_ONLY_IDEMPOTENT})
130
+ @catch_errors
131
+ async def dense_evolution_list_molecules(params: ListMoleculesInput) -> str:
132
+ """List every molecule in the built-in Hartree-Fock catalog, each with
133
+ its real qubit count under the requested mapping. Use this to find valid
134
+ `name` values for `dense_evolution_molecule_energy`, `_mix_molecules`,
135
+ `_run_vqe`, `_qmmm_forces`, `_md_trajectory`, and `_energy_scan`.
136
+
137
+ Each entry includes a short `id` (e.g. "H2", "LiH", "HeH+") derived from
138
+ the catalog's full descriptive name -- pass either form to any tool that
139
+ takes a molecule `name`; the short id is easier to copy exactly across
140
+ multiple tool calls than the full "H2 (Idrogeno) - R = 0.7414 A
141
+ [equilibrio reale]"-style string.
142
+
143
+ Args:
144
+ params (ListMoleculesInput): mapping -- 'jordan_wigner' (default) or 'bravyi_kitaev'.
145
+
146
+ Returns:
147
+ str: JSON list of {id, full_name, symbols, geometry, charge, n_qubits} per molecule.
148
+ """
149
+ return json.dumps(await _get_annotated_molecule_catalog(params.mapping), indent=2)
@@ -0,0 +1,142 @@
1
+ """Tools: traversable-wormhole-inspired quantum teleportation (SYK
2
+ model)."""
3
+ import json
4
+
5
+ from ..client import _request, catch_errors
6
+ from ..config import COMPUTE
7
+ from ..models import WormholeScanInput, WormholeSelectInstanceInput, WormholeTeleportationInput
8
+ from ..server import mcp
9
+
10
+
11
+ @mcp.tool(name="dense_evolution_wormhole_select_instance",
12
+ annotations={"title": "Select a good SYK instance for wormhole teleportation", **COMPUTE})
13
+ @catch_errors
14
+ async def dense_evolution_wormhole_select_instance(params: WormholeSelectInstanceInput) -> str:
15
+ """Find a binary-sparse-SYK random-instance seed suitable for the
16
+ traversable-wormhole-inspired teleportation protocol
17
+ (`dense_evolution_wormhole_teleportation`).
18
+
19
+ A uniformly-random seed does NOT reliably show the protocol's
20
+ sign-dependent teleportation signal -- verified directly across many
21
+ seeds (some give a clean peak, some the wrong sign for most of a
22
+ sweep, some are flat noise). arXiv:2604.10090 didn't use an arbitrary
23
+ instance either: they picked one "selected for favorable commutation
24
+ properties" among their chosen terms. This tool reproduces that same
25
+ selection criterion -- screening `n_candidates` seeds by their exact
26
+ commuting/anticommuting term-pair count and returning the one closest
27
+ to `target_commuting` -- rather than trusting a random seed.
28
+
29
+ Always call this before `dense_evolution_wormhole_teleportation` /
30
+ `dense_evolution_wormhole_scan` unless you already have a known-good
31
+ seed (e.g. 61, for the defaults n_majorana=8/k_terms=10/target=34,
32
+ the exact match found and used throughout this project's own
33
+ verification -- see research/wormhole_syk.md).
34
+
35
+ Args:
36
+ params (WormholeSelectInstanceInput): n_majorana, k_terms, J,
37
+ n_candidates, target_commuting.
38
+
39
+ Returns:
40
+ str: JSON {seed, n_majorana, k_terms, commuting, anticommuting,
41
+ target_commuting} -- pass `seed` straight into
42
+ dense_evolution_wormhole_teleportation / _wormhole_scan.
43
+ """
44
+ return json.dumps(await _request("POST", "/api/wormhole_select_instance", timeout=60.0, json=params.model_dump()), indent=2)
45
+
46
+
47
+ @mcp.tool(name="dense_evolution_wormhole_teleportation",
48
+ annotations={"title": "Run traversable-wormhole-inspired quantum teleportation", **COMPUTE})
49
+ @catch_errors
50
+ async def dense_evolution_wormhole_teleportation(params: WormholeTeleportationInput) -> str:
51
+ """Run one point of the real traversable-wormhole-inspired quantum
52
+ teleportation protocol (Gao-Jafferis-Wall theory, arXiv:2604.10090)
53
+ on a binary sparse SYK model: two coupled chaotic Hamiltonians (L,R),
54
+ a message injected into L via a reference-qubit pair (P,Q), a real
55
+ bilinear L-R coupling exp(i*mu*V), and a readout that is NOT a
56
+ single-qubit expectation value (which the no-signaling theorem
57
+ forbids from ever showing this signal) but the mutual information
58
+ between the reference qubit P and a qubit read out from R.
59
+
60
+ Returns a single mutual-information value for the given mu -- the
61
+ physically meaningful result is the *difference* between a positive-
62
+ and negative-mu run at the same (n_majorana, k_terms, seed, t0, t1).
63
+ Call this tool twice with opposite-sign mu, or use
64
+ dense_evolution_wormhole_scan to sweep many (t1, mu) combinations in
65
+ one batched call.
66
+
67
+ Requires a well-selected seed (see
68
+ dense_evolution_wormhole_select_instance) -- an arbitrary random seed
69
+ will likely not show a clean signal.
70
+
71
+ Args:
72
+ params (WormholeTeleportationInput): n_majorana, k_terms, J, mu,
73
+ t0, t1, seed, with_message, backend, n_steps_evolution,
74
+ n_steps_coupling.
75
+
76
+ Returns:
77
+ str: JSON {mutual_information_pt, backend, n_majorana, k_terms,
78
+ mu, t0, t1, seed, with_message}.
79
+ """
80
+ return json.dumps(await _request("POST", "/api/wormhole_teleportation", timeout=120.0, json=params.model_dump()), indent=2)
81
+
82
+
83
+ @mcp.tool(name="dense_evolution_wormhole_scan",
84
+ annotations={"title": "Sweep the wormhole teleportation signal over t1", **COMPUTE})
85
+ @catch_errors
86
+ async def dense_evolution_wormhole_scan(params: WormholeScanInput) -> str:
87
+ """Sweep `t1_values` for the traversable-wormhole-inspired
88
+ teleportation protocol, running both +mu_magnitude and -mu_magnitude
89
+ at every point -- one call instead of 2*len(t1_values) separate
90
+ dense_evolution_wormhole_teleportation calls. Returns each point's
91
+ mutual information for both signs plus their difference (mu<0 minus
92
+ mu>0 in this project's own convention), the standard readout for the
93
+ protocol's qualitative signature: a smooth peak in that difference
94
+ across the sweep (known peak for seed=61/n_majorana=8/k_terms=10/
95
+ t0=0.3: around t1≈0.6-0.7).
96
+
97
+ Unlike dense_evolution_energy_scan, points run sequentially, not
98
+ concurrently: each single teleportation call does real exact
99
+ diagonalization (or Trotterized circuit execution) of the full joint
100
+ L+R+P+Q system and takes several seconds on its own (verified:
101
+ concurrent calls to this specific endpoint crashed the kernel process
102
+ outright -- a real BLAS/eigh thread-safety issue under this protocol's
103
+ heavier-than-usual concurrent linear algebra, not present in the
104
+ lighter Hamiltonian-diagonalization calls energy_scan batches). A
105
+ full 20-point sweep can take several minutes; start with fewer points
106
+ to gauge cost.
107
+
108
+ A point that fails does not abort the rest of the sweep; its error is
109
+ reported alongside the successful points.
110
+
111
+ Args:
112
+ params (WormholeScanInput): n_majorana, k_terms, J, mu_magnitude,
113
+ t0, t1_values (list, max 20 points), seed, with_message,
114
+ backend, n_steps_evolution, n_steps_coupling.
115
+
116
+ Returns:
117
+ str: JSON with:
118
+ {
119
+ "n_points": int,
120
+ "results": [{"t1": float, "mu_positive": float, "mu_negative": float, "delta": float} |
121
+ {"t1": float, "error": str}, ...],
122
+ "peak": {"t1": ..., "delta": ...} | null # point with the largest delta, successful points only
123
+ }
124
+ """
125
+ async def _one_point(t1):
126
+ base = dict(
127
+ n_majorana=params.n_majorana, k_terms=params.k_terms, J=params.J, t0=params.t0, t1=t1,
128
+ seed=params.seed, with_message=params.with_message, backend=params.backend,
129
+ n_steps_evolution=params.n_steps_evolution, n_steps_coupling=params.n_steps_coupling,
130
+ )
131
+ try:
132
+ pos = await _request("POST", "/api/wormhole_teleportation", timeout=120.0, json={**base, "mu": params.mu_magnitude})
133
+ neg = await _request("POST", "/api/wormhole_teleportation", timeout=120.0, json={**base, "mu": -params.mu_magnitude})
134
+ i_pos, i_neg = pos["mutual_information_pt"], neg["mutual_information_pt"]
135
+ return {"t1": t1, "mu_positive": i_pos, "mu_negative": i_neg, "delta": i_neg - i_pos}
136
+ except Exception as e:
137
+ return {"t1": t1, "error": str(e)}
138
+
139
+ results = [await _one_point(t1) for t1 in params.t1_values]
140
+ successful = [r for r in results if "delta" in r]
141
+ peak = max(successful, key=lambda r: r["delta"]) if successful else None
142
+ return json.dumps({"n_points": len(results), "results": results, "peak": peak}, indent=2)
File without changes
@@ -0,0 +1,55 @@
1
+ """A minimal in-memory TTL cache, used to avoid re-fetching the
2
+ (effectively static) molecule catalog on every tool call that needs to
3
+ resolve a molecule name, while still eventually picking up a real catalog
4
+ change (e.g. after a kernel restart with a different build) instead of
5
+ caching forever like the pre-Phase-2 code did (a plain dict, reset only by
6
+ the test suite, never by production code). Also caches FAILURES for a
7
+ short TTL, so a tight loop of tool calls made while the kernel is down
8
+ doesn't each wait out their own full connection/timeout error -- see
9
+ `set_failure`/`get`'s re-raise behavior below.
10
+ """
11
+ import time
12
+ from typing import Any, Optional
13
+
14
+
15
+ class TTLCache:
16
+ def __init__(self, ttl_seconds: float = 300.0, failure_ttl_seconds: float = 10.0):
17
+ self._ttl = ttl_seconds
18
+ self._failure_ttl = failure_ttl_seconds
19
+ # key -> (expires_at, value_or_exception, is_failure)
20
+ self._store: dict[str, tuple[float, Any, bool]] = {}
21
+
22
+ def get(self, key: str):
23
+ """Returns the cached value, or None if missing/expired. Re-raises
24
+ a cached failure (so callers see the same error shape they would
25
+ from a fresh call) instead of returning it as if it were a value."""
26
+ entry = self._store.get(key)
27
+ if entry is None:
28
+ return None
29
+ expires_at, value, is_failure = entry
30
+ if time.monotonic() >= expires_at:
31
+ del self._store[key]
32
+ return None
33
+ if is_failure:
34
+ raise value
35
+ return value
36
+
37
+ def set(self, key: str, value: Any) -> None:
38
+ self._store[key] = (time.monotonic() + self._ttl, value, False)
39
+
40
+ def set_failure(self, key: str, exc: Exception) -> None:
41
+ self._store[key] = (time.monotonic() + self._failure_ttl, exc, True)
42
+
43
+ def invalidate(self, key: Optional[str] = None) -> None:
44
+ """Clears one key, or the whole cache if key is None."""
45
+ if key is None:
46
+ self._store.clear()
47
+ else:
48
+ self._store.pop(key, None)
49
+
50
+ def __len__(self) -> int:
51
+ """Raw entry count, including any not-yet-pruned expired ones --
52
+ entries only drop out on their next get(), not on a timer. Good
53
+ enough for a diagnostics tool (dense_evolution_kernel_status);
54
+ don't rely on this for anything that needs an exact live count."""
55
+ return len(self._store)
@@ -0,0 +1,67 @@
1
+ """Image saving, rotating cleanup, and metadata for the dense_evolution_mcp
2
+ adapter. Images (circuit diagrams, histograms, Q-sphere, Bloch vectors)
3
+ come back from the kernel as base64 PNGs; see the top-level server.py
4
+ module docstring for why they're written to disk instead of inlined.
5
+
6
+ IMAGE_OUTPUT_DIR/IMAGE_MAX_FILES are defined HERE rather than in config.py,
7
+ for the same reason KERNEL_URL/_TEST_TRANSPORT live in client.py rather
8
+ than config.py (see client.py's own docstring): the test suite mutates
9
+ both directly (`mcp_images.IMAGE_OUTPUT_DIR = tmp_path`), and that
10
+ mutation only works if the value's single source of truth lives in the
11
+ same module that reads it -- _prune_old_images/_save_png, which moved here
12
+ in Phase 2 of the refactor (see prog.txt Sezione 3).
13
+
14
+ Each saved PNG also gets an optional sidecar `<name>_<ts>.json` with
15
+ whatever metadata the caller passes in (tool name, qasm, seed, ...) --
16
+ BUG FIX: previously a saved image's filename (`circuit_<timestamp>.png`)
17
+ carried no way to trace it back to the circuit/tool call that produced it,
18
+ so an agent (or a human) looking at ~/.dense_evolution_mcp/images later
19
+ had no way to tell which image was which beyond the generic `name` prefix
20
+ shared by every call of the same kind.
21
+ """
22
+ import base64
23
+ import json
24
+ import os
25
+ import time
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+ IMAGE_OUTPUT_DIR = Path(
30
+ os.environ.get("DENSE_EVOLUTION_MCP_IMAGE_DIR", str(Path.home() / ".dense_evolution_mcp" / "images"))
31
+ )
32
+ # Every include_visualizations=True call writes a new timestamped PNG with
33
+ # no cleanup -- a long-running MCP session (or an agent looping over many
34
+ # circuits) grows this directory without bound. Cap it to the most
35
+ # recently written files; 0 or negative disables pruning entirely.
36
+ IMAGE_MAX_FILES = int(os.environ.get("DENSE_EVOLUTION_MCP_IMAGE_MAX_FILES", "500"))
37
+
38
+
39
+ def _prune_old_images() -> None:
40
+ """Keep at most IMAGE_MAX_FILES PNGs in IMAGE_OUTPUT_DIR, deleting the
41
+ oldest by mtime first (and their metadata sidecar, if any). Read at
42
+ call time (not module import) so tests that monkeypatch
43
+ IMAGE_OUTPUT_DIR/IMAGE_MAX_FILES take effect."""
44
+ if IMAGE_MAX_FILES <= 0:
45
+ return
46
+ files = sorted(IMAGE_OUTPUT_DIR.glob("*.png"), key=lambda p: p.stat().st_mtime)
47
+ excess_count = len(files) - IMAGE_MAX_FILES
48
+ for path in files[:excess_count]:
49
+ path.unlink(missing_ok=True)
50
+ path.with_suffix(".json").unlink(missing_ok=True)
51
+
52
+
53
+ def _save_png(b64_png: Optional[str], name: str, metadata: Optional[dict] = None) -> Optional[str]:
54
+ """Decode a base64 PNG from the kernel and write it to disk, returning
55
+ the path instead of the raw base64 -- see module docstring. If
56
+ `metadata` is given, also writes a `<same-stem>.json` sidecar next to
57
+ the PNG (e.g. {"tool": "dense_evolution_run_circuit", "qasm": ...,
58
+ "seed": ..., "n_qubits": ...})."""
59
+ if not b64_png:
60
+ return None
61
+ IMAGE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
62
+ path = IMAGE_OUTPUT_DIR / f"{name}_{int(time.time() * 1000)}.png"
63
+ path.write_bytes(base64.b64decode(b64_png))
64
+ if metadata is not None:
65
+ path.with_suffix(".json").write_text(json.dumps(metadata, indent=2))
66
+ _prune_old_images()
67
+ return str(path)
@@ -0,0 +1,38 @@
1
+ """Truncation of large numeric arrays (statevector, probabilities) to
2
+ their most significant entries -- a full statevector/probability array can
3
+ be thousands of entries for a 20+ qubit circuit; agents almost always care
4
+ about the dominant amplitudes, not the full dense array.
5
+
6
+ `top_k` is configurable per call (`RunCircuitInput.top_k`, threaded
7
+ through from dense_evolution_run_circuit) rather than a fixed 25 -- BUG
8
+ FIX: the pre-Phase-2 code hardcoded top_k=25 in the function signature
9
+ with no way for a caller to ask for more or fewer entries.
10
+
11
+ Measurement `counts` are deliberately NOT truncated here (a real, open
12
+ question from prog.txt's diagnosis, not silently dropped): `counts` is
13
+ already naturally bounded by `shots` (RunCircuitInput caps that at
14
+ 1,000,000) and by the number of distinct basis states actually observed,
15
+ which for realistic qubit counts is far smaller than a full statevector.
16
+ Truncating it to a top-K + "other" shape would change
17
+ dense_evolution_run_circuit's response schema for every caller, not just
18
+ opt-in behavior -- left for a separate, deliberate decision rather than
19
+ folded into this phase's mechanical extraction.
20
+ """
21
+
22
+
23
+ def _truncate_statevector(rows: list, top_k: int = 25) -> dict:
24
+ sorted_rows = sorted(rows, key=lambda r: -r["abs"])
25
+ return {
26
+ "total_nonzero_amplitudes": len(rows),
27
+ "shown": min(top_k, len(rows)),
28
+ "top_amplitudes_by_magnitude": sorted_rows[:top_k],
29
+ }
30
+
31
+
32
+ def _truncate_probabilities(probs: list, top_k: int = 25) -> dict:
33
+ indexed = sorted(enumerate(probs), key=lambda t: -t[1])[:top_k]
34
+ return {
35
+ "total_basis_states": len(probs),
36
+ "shown": min(top_k, len(probs)),
37
+ "top_states_by_probability": [{"index": i, "probability": p} for i, p in indexed],
38
+ }