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