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
mcp_server/models.py ADDED
@@ -0,0 +1,347 @@
1
+ """Pydantic input schemas for every dense_evolution_mcp tool, extracted
2
+ from server.py so tool logic and schema definitions can change
3
+ independently. Field descriptions stay here (not moved into tool
4
+ docstrings) since MCP clients read them directly off the schema to build
5
+ their own UI/validation -- the docstrings still carry the longer
6
+ explanation for a human or agent reading the tool's full documentation.
7
+ """
8
+ from typing import List, Optional
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
11
+
12
+
13
+ class ListMoleculesInput(BaseModel):
14
+ model_config = ConfigDict(extra="forbid")
15
+ mapping: str = Field(
16
+ default="jordan_wigner",
17
+ description="Fermion-to-qubit mapping: 'jordan_wigner' or 'bravyi_kitaev'. Both represent "
18
+ "the identical physical Hamiltonian (same spectrum) in a different qubit basis.",
19
+ )
20
+
21
+
22
+ class BuildCircuitInput(BaseModel):
23
+ model_config = ConfigDict(extra="forbid")
24
+ n_qubits: int = Field(..., ge=1, description="Number of qubits in the circuit.")
25
+ ops: list = Field(
26
+ ..., description="List of graphical-builder gate operations to convert into OpenQASM. "
27
+ "Get valid gate names from dense_evolution_list_gates first."
28
+ )
29
+
30
+
31
+ class RunCircuitInput(BaseModel):
32
+ model_config = ConfigDict(extra="forbid")
33
+ qasm: str = Field(..., min_length=1, description="OpenQASM 2.0 circuit source to simulate.")
34
+ shots: int = Field(default=1000, ge=1, le=1_000_000, description="Number of measurement shots for the counts histogram.")
35
+ seed: int = Field(default=42, description="Random seed for sampling/noise reproducibility.")
36
+ noise_model: str = Field(default="ideal", description="Noise model name from dense_evolution_list_noise_models, or 'ideal'.")
37
+ noise_p: float = Field(default=0.0, ge=0.0, le=1.0, description="Noise channel error probability (ignored if noise_model='ideal').")
38
+ backend: str = Field(default="dense", description="'dense' (exact statevector, up to the safe qubit ceiling) or 'mps' "
39
+ "(matrix-product-state, approximate top-k states, for larger circuits).")
40
+ top_k: int = Field(
41
+ default=25, ge=1, le=500,
42
+ description="How many of the largest-magnitude statevector amplitudes / highest-probability "
43
+ "basis states to include in the response (the rest are summarized by 'total_*'/'shown' "
44
+ "counts, not dropped from the kernel's own computation). Raise this if you need to see "
45
+ "further down the tail; lower it to save context on a circuit with many comparably-sized "
46
+ "amplitudes.",
47
+ )
48
+ include_visualizations: bool = Field(
49
+ default=False,
50
+ description="If true, also render and save circuit/histogram/Q-sphere/Bloch PNGs to disk and "
51
+ "return their file paths. Leave false unless you actually need to view an image -- "
52
+ "rendering costs extra time and the paths are not useful without viewing them.",
53
+ )
54
+
55
+ @model_validator(mode="after")
56
+ def _noise_p_requires_a_noise_model(self):
57
+ # prog.txt Sezione 3, punto 8: noise_p > 0 with noise_model='ideal'
58
+ # was silently ignored by the kernel (ideal = no channel applied at
59
+ # all) -- catch it here so the caller sees why their noise had no
60
+ # effect, instead of a passing-but-surprising ideal-circuit result.
61
+ # (The other invariant prog.txt names for this model -- backend='mps'
62
+ # + include_visualizations + n_qubits>24 -- needs the parsed qubit
63
+ # count, which only the kernel has for a raw QASM string; not
64
+ # checkable here without parsing QASM locally too.)
65
+ if self.noise_model == "ideal" and self.noise_p != 0.0:
66
+ raise ValueError(
67
+ f"noise_p={self.noise_p} has no effect with noise_model='ideal' (ideal means no "
68
+ "noise channel is applied at all) -- set noise_model to a real model name from "
69
+ "dense_evolution_list_noise_models, or leave noise_p at its default 0.0."
70
+ )
71
+ return self
72
+
73
+
74
+ class MoleculeEnergyInput(BaseModel):
75
+ model_config = ConfigDict(extra="forbid")
76
+ name: str = Field(
77
+ ..., description="Catalog molecule -- short id (e.g. 'H2', 'LiH', 'HeH+') or the full catalog "
78
+ "name. See dense_evolution_list_molecules."
79
+ )
80
+ mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
81
+
82
+
83
+ class MixMoleculesInput(BaseModel):
84
+ model_config = ConfigDict(extra="forbid")
85
+ name_a: str = Field(..., description="First catalog molecule -- short id or full catalog name.")
86
+ name_b: str = Field(..., description="Second catalog molecule -- short id or full catalog name. "
87
+ "Must have the same qubit count as name_a.")
88
+ weight_a: float = Field(default=0.5, description="Weight of the first Hamiltonian in the mix.")
89
+ weight_b: float = Field(default=0.5, description="Weight of the second Hamiltonian in the mix.")
90
+ mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
91
+
92
+ # prog.txt's third invariant for this model (same qubit count between
93
+ # name_a and name_b) is NOT added here: it needs a molecule-catalog
94
+ # lookup, which is an async kernel request (molecules.py's cache) --
95
+ # Pydantic's own validators are synchronous. Today that check happens
96
+ # only server-side, inside the kernel's /api/hamiltonian/mix endpoint
97
+ # (see chemistry_tools.py::dense_evolution_mix_molecules's own
98
+ # docstring) -- a mismatched pair is still rejected, just one round
99
+ # trip later than a local schema check could catch it.
100
+
101
+
102
+ class CustomMoleculeInput(BaseModel):
103
+ model_config = ConfigDict(extra="forbid")
104
+ symbols: list = Field(..., min_length=1, description="Atomic symbols, e.g. ['H', 'H', 'O'].")
105
+ geometry: list = Field(..., min_length=1, description="[[x, y, z], ...] coordinates in Angstrom, one row per symbol.")
106
+ charge: int = Field(default=0, description="Total molecular charge.")
107
+ mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
108
+
109
+
110
+ class EnergyScanInput(BaseModel):
111
+ model_config = ConfigDict(extra="forbid")
112
+ symbols: list = Field(..., min_length=1, description="Atomic symbols shared by every point in the scan, e.g. ['H', 'H'].")
113
+ geometries: list = Field(
114
+ ..., min_length=1, max_length=50,
115
+ description="List of [[x,y,z], ...] geometries (Angstrom) to evaluate, one per scan point -- "
116
+ "e.g. a bond-length or angle sweep. Each geometry must have the same number of rows as `symbols`. "
117
+ "Capped at 50 points per call to keep one request's kernel load bounded.",
118
+ )
119
+ charge: int = Field(default=0, description="Molecular charge, shared by every point.")
120
+ mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
121
+ labels: Optional[list] = Field(
122
+ default=None,
123
+ description="Optional label per point (e.g. bond lengths in Angstrom: [0.4, 0.5, ...]) shown "
124
+ "alongside each result. Defaults to the point's index (0, 1, 2, ...) if omitted. Must be the "
125
+ "same length as `geometries` if given.",
126
+ )
127
+
128
+
129
+ class RunVqeInput(BaseModel):
130
+ model_config = ConfigDict(extra="forbid")
131
+ name: Optional[str] = Field(default=None, description="Catalog molecule -- short id or full catalog name. "
132
+ "Provide this OR symbols+geometry, not both.")
133
+ symbols: Optional[list] = Field(default=None, description="Atomic symbols for a custom molecule.")
134
+ geometry: Optional[list] = Field(default=None, description="[[x, y, z], ...] in Angstrom for a custom molecule.")
135
+ charge: int = Field(default=0, description="Molecular charge (custom molecule only).")
136
+ ansatz_type: str = Field(
137
+ default="hardware_efficient",
138
+ description="'hardware_efficient' (generic n_layers-deep RY+CNOT template) or 'uccsd' "
139
+ "(chemically-motivated, fewer parameters but much deeper circuits per iteration).",
140
+ )
141
+ n_layers: int = Field(default=8, ge=1, description="Ansatz depth (hardware_efficient only).")
142
+ maxiter: int = Field(default=200, ge=1, le=5000, description="Maximum Adam optimizer iterations.")
143
+ step_size: float = Field(default=0.1, gt=0, description="Adam optimizer learning rate.")
144
+ beta1: float = Field(default=0.9, description="Adam optimizer beta1.")
145
+ beta2: float = Field(default=0.999, description="Adam optimizer beta2.")
146
+ seed: int = Field(default=0, description="Random seed for the initial variational parameters.")
147
+
148
+ @model_validator(mode="after")
149
+ def _name_xor_custom_molecule(self):
150
+ has_name = self.name is not None
151
+ has_custom = self.symbols is not None or self.geometry is not None
152
+ if has_name and has_custom:
153
+ raise ValueError("provide `name` OR `symbols`+`geometry`, not both.")
154
+ if not has_name and not has_custom:
155
+ raise ValueError("provide either `name` (a catalog molecule) or `symbols`+`geometry` (a custom one).")
156
+ if has_custom and (self.symbols is None or self.geometry is None):
157
+ raise ValueError("a custom molecule needs both `symbols` and `geometry`, not just one.")
158
+ return self
159
+
160
+
161
+ class QmmmForcesInput(BaseModel):
162
+ model_config = ConfigDict(extra="forbid")
163
+ name: str = Field(..., description="Catalog molecule -- short id or full catalog name. See dense_evolution_list_molecules.")
164
+ mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
165
+
166
+
167
+ class MdTrajectoryInput(BaseModel):
168
+ model_config = ConfigDict(extra="forbid")
169
+ name: str = Field(..., description="Catalog molecule -- short id or full catalog name. See dense_evolution_list_molecules.")
170
+ n_steps: int = Field(default=20, ge=1, le=200, description="Number of MD steps (capped at 200 to bound request cost).")
171
+ dt_fs: float = Field(default=0.5, gt=0, description="Timestep in femtoseconds.")
172
+ mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
173
+ recompute_electronic_state: bool = Field(
174
+ default=False,
175
+ description="If true, re-solves real Hartree-Fock at every step (true ab-initio MD, much more "
176
+ "expensive; n_steps capped at 30 in that case). If false, holds the initial "
177
+ "electronic state fixed throughout (accurate only close to the starting geometry).",
178
+ )
179
+
180
+
181
+ class MitigateZneInput(BaseModel):
182
+ model_config = ConfigDict(extra="forbid")
183
+ qasm: str = Field(..., min_length=1, description="OpenQASM circuit to run under noise and mitigate.")
184
+ pauli_string: str = Field(..., description="Pauli observable to measure and mitigate, e.g. 'ZZI'.")
185
+ noise_model: str = Field(..., description="Noise model name -- see dense_evolution_list_noise_models.")
186
+ noise_p: float = Field(..., ge=0.0, le=1.0, description="Base noise channel error probability.")
187
+ seed: int = Field(default=42, description="Random seed for the stochastic Kraus draws.")
188
+ extrapolation_method: str = Field(
189
+ default="richardson",
190
+ description="'richardson' (exact, through 1x/2x/3x noise_p) or 'polynomial' "
191
+ "(degree-2 least-squares fit through 5 noise scales).",
192
+ )
193
+
194
+
195
+ class MitigateDensityMatrixInput(BaseModel):
196
+ model_config = ConfigDict(extra="forbid")
197
+ qasm: str = Field(..., min_length=1, description="OpenQASM circuit to run under noise and mitigate.")
198
+ noise_model: str = Field(..., description="Noise model name -- see dense_evolution_list_noise_models.")
199
+ noise_p: float = Field(..., ge=0.0, le=1.0, description="Base noise channel error probability.")
200
+ seed: int = Field(default=42, description="Random seed for the Monte-Carlo density-matrix estimate.")
201
+
202
+
203
+ class CosmicRayBurstInput(BaseModel):
204
+ model_config = ConfigDict(extra="forbid")
205
+ baseline_gamma: float = Field(..., ge=0.0, le=1.0, description="Undisturbed per-slice decay probability.")
206
+ times_us: Optional[List[float]] = Field(
207
+ default=None,
208
+ description="Time points since impact, in microseconds. Defaults to the impact instant, the "
209
+ "~10us and ~1ms rise checkpoints, and a point well into the ~25ms recovery.",
210
+ )
211
+
212
+
213
+ class OscillatingNoiseInput(BaseModel):
214
+ model_config = ConfigDict(extra="forbid")
215
+ base_p: float = Field(..., ge=0.0, le=1.0, description="Noise probability around which p_eff oscillates.")
216
+ freq: float = Field(..., gt=0.0, description="Oscillation frequency parameter (see dense_evolution.oscillating_p_eff).")
217
+ amp: float = Field(..., description="Oscillation amplitude, as a fraction of base_p.")
218
+ factors: Optional[List[float]] = Field(
219
+ default=None, description="Scale-factor points to evaluate p_eff at. Defaults to one full period.",
220
+ )
221
+
222
+
223
+ class DensityMatrixChannelInput(BaseModel):
224
+ model_config = ConfigDict(extra="forbid")
225
+ qasm: str = Field(..., min_length=1, description="OpenQASM circuit whose ideal density matrix the channel is applied to.")
226
+ channel: str = Field(..., description="'global_depolarizing' (whole-register, SPAM-style) or "
227
+ "'amplitude_damping' (single-qubit only, asymmetric energy relaxation).")
228
+ param: float = Field(..., ge=0.0, le=1.0, description="Channel strength: p for global_depolarizing, gamma for amplitude_damping.")
229
+
230
+
231
+ class VectorHealingInput(BaseModel):
232
+ model_config = ConfigDict(extra="forbid")
233
+ vectors: list = Field(..., description="(n_steps, dim) sequence of equal-length numeric rows to heal -- "
234
+ "e.g. a VQE parameter/energy trajectory, MD telemetry, or any other noisy vector "
235
+ "sequence. NaN/Inf entries are sanitized automatically.")
236
+ radius_baseline: int | None = Field(
237
+ default=None,
238
+ description="Fixed radius for the local baseline window used to judge each step. If omitted, "
239
+ "computed adaptively as min(20, max(3, n_steps // 3)).",
240
+ )
241
+
242
+
243
+ class WormholeSelectInstanceInput(BaseModel):
244
+ model_config = ConfigDict(extra="forbid")
245
+ n_majorana: int = Field(default=8, ge=4, description="Number of Majorana modes per side (must be even). "
246
+ "8 matches arXiv:2604.10090's own experiment (4 qubits/side).")
247
+ k_terms: int = Field(default=10, ge=1, description="Number of sparse 4-Majorana coupling terms to keep, "
248
+ "out of C(n_majorana,4) possible -- 10 matches the paper's K=10.")
249
+ J: float = Field(default=1.4142135623730951, description="SYK coupling strength (paper's J=sqrt(2)).")
250
+ n_candidates: int = Field(default=200, ge=1, le=2000, description="Number of random seeds to screen.")
251
+ target_commuting: int = Field(default=34, description="Target commuting-pair count out of C(k_terms,2) pairs "
252
+ "-- 34 (out of 45) is the paper's own selected instance's ratio for K=10.")
253
+
254
+
255
+ class WormholeTeleportationInput(BaseModel):
256
+ model_config = ConfigDict(extra="forbid")
257
+ n_majorana: int = Field(default=8, ge=4, description="Number of Majorana modes per side (must be even).")
258
+ k_terms: int = Field(default=10, ge=1, description="Number of sparse 4-Majorana coupling terms per side.")
259
+ J: float = Field(default=1.4142135623730951, description="SYK coupling strength (paper's J=sqrt(2)).")
260
+ mu: float = Field(default=12.0, description="L-R coupling strength exp(i*mu*V). Sign matters -- the "
261
+ "protocol's signature is the difference in teleported mutual information between "
262
+ "+mu and -mu (call twice, or use dense_evolution_wormhole_scan for a batch sweep).")
263
+ t0: float = Field(default=0.3, description="Pre-coupling evolution time under H_L+H_R.")
264
+ t1: float = Field(default=0.6, description="Post-coupling evolution time under H_L+H_R. The known signal "
265
+ "peak for seed=61/n_majorana=8/k_terms=10 is at t1=0.60.")
266
+ seed: int = Field(default=61, description="SYK instance seed -- use dense_evolution_wormhole_select_instance "
267
+ "to find a good one for other (n_majorana, k_terms) combinations; 61 is the verified "
268
+ "match for the defaults.")
269
+ with_message: bool = Field(default=True, description="Inject the reference qubit's message into the L "
270
+ "register (swap). False = no message injected, a baseline/control run.")
271
+ backend: str = Field(default="exact", description="'exact' (matrix exponentiation, cheap, exact) or "
272
+ "'trotter' (real Trotterized gate circuit, closer to actual hardware execution, "
273
+ "verified to reproduce the exact backend's result closely).")
274
+ n_steps_evolution: int = Field(default=8, ge=1, description="Trotter steps for the t0/t1 evolution (trotter backend only).")
275
+ n_steps_coupling: int = Field(default=16, ge=1, description="Trotter steps for the mu coupling (trotter backend only).")
276
+
277
+
278
+ class WormholeScanInput(BaseModel):
279
+ model_config = ConfigDict(extra="forbid")
280
+ n_majorana: int = Field(default=8, ge=4, description="Number of Majorana modes per side (must be even).")
281
+ k_terms: int = Field(default=10, ge=1, description="Number of sparse 4-Majorana coupling terms per side.")
282
+ J: float = Field(default=1.4142135623730951, description="SYK coupling strength (paper's J=sqrt(2)).")
283
+ mu_magnitude: float = Field(default=12.0, ge=0, description="Both +mu_magnitude and -mu_magnitude are "
284
+ "run at every t1 point -- this is the wormhole signal's defining comparison.")
285
+ t0: float = Field(default=0.3, description="Pre-coupling evolution time under H_L+H_R, shared by every point.")
286
+ t1_values: list = Field(..., min_length=1, max_length=20, description="List of post-coupling evolution "
287
+ "times to sweep. Capped at 20 points (each point runs backend twice, +mu and "
288
+ "-mu, sequentially -- unlike dense_evolution_energy_scan this is NOT cheap: "
289
+ "each single teleportation call takes several seconds, so a full sweep can "
290
+ "take a few minutes; see the tool's own docstring).")
291
+ seed: int = Field(default=61, description="SYK instance seed -- use dense_evolution_wormhole_select_instance first.")
292
+ with_message: bool = Field(default=True, description="Inject the reference qubit's message into L.")
293
+ backend: str = Field(default="exact", description="'exact' or 'trotter' -- see dense_evolution_wormhole_teleportation.")
294
+ n_steps_evolution: int = Field(default=8, ge=1, description="Trotter steps for t0/t1 evolution (trotter backend only).")
295
+ n_steps_coupling: int = Field(default=16, ge=1, description="Trotter steps for the mu coupling (trotter backend only).")
296
+
297
+
298
+ class NativeHfDiagnosticsInput(BaseModel):
299
+ model_config = ConfigDict(extra="forbid")
300
+ symbols: list = Field(..., min_length=1, description="Atomic symbols, e.g. ['H', 'H', 'O'].")
301
+ geometry: list = Field(..., min_length=1, description="[[x, y, z], ...] coordinates in Angstrom, one row per symbol.")
302
+ charge: int = Field(default=0, description="Total molecular charge.")
303
+ active_electrons: Optional[int] = Field(default=None, description="Active-space electron count, if restricting the active space.")
304
+ active_orbitals: Optional[int] = Field(default=None, description="Active-space orbital count, if restricting the active space.")
305
+
306
+
307
+ class RagSearchInput(BaseModel):
308
+ model_config = ConfigDict(extra="forbid")
309
+ documents: list = Field(..., min_length=1, description="[[text, source], ...] documents to index -- each a 2-element list of the document text and a source label (e.g. filename).")
310
+ query: str = Field(..., min_length=1, description="Search query -- natural-language for semantic search, or the literal/regex pattern when exact=True.")
311
+ top: int = Field(default=5, ge=1, description="Number of top results to return (semantic search only, ignored when exact=True).")
312
+ rerank: bool = Field(default=True, description="If true, pools TF-IDF and dense-embedding candidates then re-scores with a cross-encoder. If false, plain TF-IDF ranking only. Ignored when exact=True.")
313
+ exact: bool = Field(default=False, description="If true, substring/regex search over raw chunk text instead of semantic search -- for finding a specific known phrase or value semantic ranking buries.")
314
+ regex: bool = Field(default=False, description="Only with exact=True: treat query as a regex (case-sensitive) instead of a literal substring (case-insensitive).")
315
+ max_hits: int = Field(default=10, ge=1, description="Only with exact=True: stop after this many matches.")
316
+ context: int = Field(default=300, ge=0, description="Only with exact=True: characters of context to show around each match.")
317
+
318
+
319
+ class MassDecompositionInput(BaseModel):
320
+ model_config = ConfigDict(extra="forbid")
321
+ formula: str = Field(..., min_length=1, description="Molecular formula string, e.g. 'C6H12O6'.")
322
+ target_mass: float = Field(..., description="Mass difference to check for chemical reachability, e.g. a peak-pair difference from a real spectrum.")
323
+ max_mass: Optional[float] = Field(default=None, description="Upper bound for the search space; defaults to target_mass itself if omitted.")
324
+
325
+
326
+ class Bb84Input(BaseModel):
327
+ model_config = ConfigDict(extra="forbid")
328
+ n_rounds: int = Field(..., ge=1, description="Number of BB84 rounds to run.")
329
+ p_channel: float = Field(default=0.0, ge=0.0, le=1.0, description="Isotropic depolarizing channel error probability.")
330
+ eve: bool = Field(default=False, description="If true, simulates an intercept-resend eavesdropping attack instead of a clean channel.")
331
+ seed: Optional[int] = Field(default=None, description="Random seed for reproducibility.")
332
+
333
+
334
+ class DiQkdGhzInput(BaseModel):
335
+ model_config = ConfigDict(extra="forbid")
336
+ n_rounds: int = Field(..., ge=1, description="Number of test rounds to run.")
337
+ p_dep: float = Field(default=0.0, ge=0.0, le=1.0, description="Isotropic depolarizing error probability applied to the GHZ(3) state.")
338
+ seed: Optional[int] = Field(default=None, description="Random seed for reproducibility.")
339
+
340
+
341
+ class DickaInput(BaseModel):
342
+ model_config = ConfigDict(extra="forbid")
343
+ n_rounds: int = Field(..., ge=1, description="Number of protocol rounds to run.")
344
+ gamma: float = Field(..., description="Test-round selection probability.")
345
+ beta: float = Field(..., gt=0.75, lt=0.8535533905932737, description="Abort threshold: must sit strictly between the classical bound (0.75) and the quantum max (~0.8536) to meaningfully distinguish a device-independent channel from a classically-bound one.")
346
+ p_dep: float = Field(default=0.0, ge=0.0, le=1.0, description="Isotropic depolarizing error probability.")
347
+ seed: Optional[int] = Field(default=None, description="Random seed for reproducibility.")
@@ -0,0 +1,71 @@
1
+ """Molecule name resolution: short ids <-> the kernel's full catalog keys.
2
+
3
+ The kernel's own catalog keys are long, human-readable strings, e.g.
4
+ "H2 (Idrogeno) - R = 0.7414 A [equilibrio reale]" -- fine for a web page
5
+ label, error-prone for an agent to reproduce verbatim across several tool
6
+ calls (exact punctuation, accented characters, etc.). Rather than change
7
+ the kernel's catalog (that key is also what the published Composer page
8
+ uses), this adapter derives a short id from each key's leading token
9
+ (e.g. "H2", "LiH", "HeH+") and accepts either form everywhere a molecule
10
+ `name` is expected. Derived from the live catalog, not hardcoded, so it
11
+ stays correct if the catalog grows.
12
+
13
+ Cached per mapping via TTLCache (utils/cache.py) instead of a plain dict
14
+ that only ever got reset by test code -- BUG FIX: the pre-Phase-2 cache
15
+ never expired on its own, so a long-running MCP server process would
16
+ never pick up a real catalog change (e.g. after a kernel restart with a
17
+ different build). A failed fetch is also cached briefly, so a tight loop
18
+ of tool calls made while the kernel is down doesn't each wait out their
19
+ own connection/timeout error.
20
+
21
+ Shared by tools/system_tools.py (dense_evolution_list_molecules) and
22
+ tools/chemistry_tools.py (every tool taking a molecule `name`) -- lives
23
+ in its own module rather than either of those, since Phase 3 (prog.txt
24
+ Sezione 3) splits tools by topic and this is domain glue, not a tool
25
+ itself.
26
+ """
27
+ from .client import _request
28
+ from .utils.cache import TTLCache
29
+
30
+ _molecule_catalog_cache = TTLCache(ttl_seconds=300.0, failure_ttl_seconds=10.0)
31
+ # cache value per mapping: (annotated_catalog_list, {short_id_lower: full_key})
32
+
33
+
34
+ def _short_id(full_key: str) -> str:
35
+ return full_key.split(" (")[0].split(" -")[0].strip()
36
+
37
+
38
+ async def _get_annotated_molecule_catalog(mapping: str) -> list:
39
+ """Catalog entries with a short `id` field added. See the cache note
40
+ in this module's docstring."""
41
+ cached = _molecule_catalog_cache.get(mapping)
42
+ if cached is not None:
43
+ return cached[0]
44
+ try:
45
+ catalog = await _request("GET", "/api/hamiltonians", timeout=10.0, params={"mapping": mapping})
46
+ except Exception as e:
47
+ _molecule_catalog_cache.set_failure(mapping, e)
48
+ raise
49
+ aliases = {}
50
+ annotated = []
51
+ for full_key, spec in catalog.items():
52
+ short = _short_id(full_key)
53
+ aliases[short.lower()] = full_key
54
+ annotated.append({"id": short, "full_name": full_key, **spec})
55
+ _molecule_catalog_cache.set(mapping, (annotated, aliases))
56
+ return annotated
57
+
58
+
59
+ async def _resolve_molecule_name(name: str) -> str:
60
+ """Accept either a short id ('H2') or the full catalog key and return
61
+ the full catalog key the kernel expects. Falls back to returning the
62
+ input unchanged if it's neither -- the kernel's own 404 (with the name
63
+ as given) is a clearer error than silently guessing."""
64
+ cached = _molecule_catalog_cache.get("jordan_wigner")
65
+ if cached is None:
66
+ await _get_annotated_molecule_catalog("jordan_wigner")
67
+ cached = _molecule_catalog_cache.get("jordan_wigner")
68
+ _, aliases = cached
69
+ if name in aliases.values():
70
+ return name
71
+ return aliases.get(name.lower(), name)
mcp_server/server.py ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP server for Dense-Evolution's Composer API (dense_evolution_mcp).
4
+
5
+ This is a thin adapter, not a reimplementation: every tool here calls the
6
+ same local FastAPI kernel the published Composer web page talks to
7
+ (local_site/app/server.py, started with `dense-evolution serve`, listening
8
+ on http://127.0.0.1:8800 by default). All computation -- circuit
9
+ simulation, VQE, molecular Hamiltonians, QM/MM forces, ZNE mitigation --
10
+ happens inside dense_evolution/dashboard_core exactly as it does for the
11
+ web UI; this file only exposes those same endpoints as MCP tools so an
12
+ agent can drive them directly instead of a browser.
13
+
14
+ main() auto-starts the kernel if it isn't already running (see
15
+ client.ensure_kernel_running) -- best-effort, and skipped if
16
+ DENSE_EVOLUTION_KERNEL_URL points somewhere non-default. If that fails
17
+ (e.g. the composer extra isn't installed) or you'd rather manage it
18
+ yourself, start it separately:
19
+ pip install dense-evolution[composer]
20
+ dense-evolution serve
21
+ (or `python -m local_site.app.server` from the repo root)
22
+
23
+ Override the kernel URL with the DENSE_EVOLUTION_KERNEL_URL env var if it's
24
+ not on the default host/port.
25
+
26
+ Images (circuit diagrams, histograms, Q-sphere, Bloch vectors) come back
27
+ from the kernel as base64 PNGs meant for a browser <img> tag -- inlining
28
+ that into a tool's text response would flood an agent's context with a
29
+ wall of base64 for a picture it can't even see inline. Instead this
30
+ adapter decodes and writes each one to DENSE_EVOLUTION_MCP_IMAGE_DIR
31
+ (default ~/.dense_evolution_mcp/images) and returns the file path, which
32
+ Claude Code (or any agent with file access) can open directly. Large
33
+ numeric arrays (statevector, probabilities) are similarly truncated to
34
+ their most significant entries rather than dumped in full -- the kernel's
35
+ own response can be tens of thousands of floats for a 20+ qubit circuit.
36
+
37
+ Structure (prog.txt Sezione 3, now complete): settings live in config.py,
38
+ the HTTP client + error handling in client.py, the Pydantic input schemas
39
+ in models.py (Phase 1); image saving/truncation/molecule-catalog caching
40
+ in utils/ and molecules.py (Phase 2); the 32 tools themselves (7 system +
41
+ 2 circuit + 9 chemistry + 4 mitigation + 3 wormhole + 3 noise + 3 crypto +
42
+ 1 retrieval -- counted directly from the imports below, not assumed: a
43
+ `len(...) == N` regression test guards against drift, see
44
+ test_mcp_server.py), split by topic, in tools/ (Phase 3,
45
+ this file). This file's only job is to create
46
+ the MCPServer instance, import each tools/*.py module so its `@mcp.tool`
47
+ decorators register against it, re-export every tool function (so
48
+ `from mcp_server.server import dense_evolution_health` keeps working for
49
+ existing callers, including this repo's own test suite), and provide the
50
+ `main()` console-script entry point.
51
+
52
+ `mcp` is created here BEFORE the `from .tools....` imports below, and
53
+ every tools/*.py module does `from ..server import mcp` -- Python resolves
54
+ this correctly despite looking circular: by the time those imports run,
55
+ `mcp_server.server` is already in `sys.modules` (registered before this
56
+ file's body starts executing) with `mcp` already assigned, so each
57
+ submodule's `from ..server import mcp` finds it immediately. Reordering
58
+ the `mcp = MCPServer(...)` line to after the tool imports would break this.
59
+ """
60
+ from mcp.server import MCPServer
61
+
62
+ mcp = MCPServer("dense_evolution_mcp")
63
+
64
+ from .tools.system_tools import ( # noqa: E402
65
+ dense_evolution_health, dense_evolution_kernel_status, dense_evolution_list_gates,
66
+ dense_evolution_list_molecules, dense_evolution_list_noise_models, dense_evolution_list_presets,
67
+ dense_evolution_system_limits,
68
+ )
69
+ from .tools.circuit_tools import dense_evolution_build_circuit, dense_evolution_run_circuit # noqa: E402
70
+ from .tools.chemistry_tools import ( # noqa: E402
71
+ dense_evolution_custom_molecule_energy, dense_evolution_energy_scan, dense_evolution_mass_decomposition,
72
+ dense_evolution_md_trajectory, dense_evolution_mix_molecules, dense_evolution_molecule_energy,
73
+ dense_evolution_native_hf_diagnostics, dense_evolution_qmmm_forces, dense_evolution_run_vqe,
74
+ )
75
+ from .tools.mitigation_tools import ( # noqa: E402
76
+ dense_evolution_mitigate_coherence, dense_evolution_mitigate_density_matrix,
77
+ dense_evolution_mitigate_zne, dense_evolution_vector_healing,
78
+ )
79
+ from .tools.wormhole_tools import ( # noqa: E402
80
+ dense_evolution_wormhole_scan, dense_evolution_wormhole_select_instance, dense_evolution_wormhole_teleportation,
81
+ )
82
+ from .tools.noise_tools import ( # noqa: E402
83
+ dense_evolution_cosmic_ray_burst, dense_evolution_oscillating_noise, dense_evolution_density_matrix_channel,
84
+ )
85
+ from .tools.crypto_tools import ( # noqa: E402
86
+ dense_evolution_crypto_bb84, dense_evolution_crypto_di_qkd_ghz, dense_evolution_crypto_dicka,
87
+ )
88
+ from .tools.retrieval_tools import dense_evolution_rag_search # noqa: E402
89
+
90
+
91
+ def main():
92
+ """Console-script entry point (`dense-evolution mcp`, see
93
+ dense_evolution/cli.py) -- identical to running this file directly.
94
+ stdio transport: this process is meant to be launched by an MCP
95
+ client (Claude Code, Claude Desktop, ...) as a subprocess, not run
96
+ standalone in a terminal."""
97
+ import asyncio
98
+ from .client import close_shared_client, ensure_kernel_running
99
+ try:
100
+ asyncio.run(ensure_kernel_running())
101
+ except RuntimeError:
102
+ pass # best-effort only -- an event-loop hiccup here must never block startup
103
+ try:
104
+ mcp.run() # pragma: no cover -- blocks on the real stdio transport loop
105
+ finally:
106
+ # Best-effort: mcp.run() has already torn down its own event loop
107
+ # by the time this runs, so close_shared_client() executes under a
108
+ # fresh one here -- fine for aclose() (it just closes the pool),
109
+ # but swallow a RuntimeError anyway rather than let cleanup crash
110
+ # shutdown for what's ultimately a leaked-socket warning, not a
111
+ # correctness issue.
112
+ try:
113
+ asyncio.run(close_shared_client())
114
+ except RuntimeError:
115
+ pass
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
File without changes