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,95 @@
1
+ from typing import Tuple
2
+
3
+ import numpy as np
4
+ import jax.numpy as jnp
5
+
6
+ from .guard import _device_memory_budget_bytes, HAS_JAX
7
+
8
+ __all__ = ["MemoryChunker", "get_dynamic_chunk"]
9
+
10
+
11
+ def get_dynamic_chunk(dtype_target) -> int:
12
+ available_bytes, _source = _device_memory_budget_bytes()
13
+ safe_bytes = available_bytes * 0.85
14
+ if HAS_JAX and dtype_target is jnp.complex128:
15
+ bpe = 16
16
+ elif dtype_target is np.complex128:
17
+ bpe = 16
18
+ else:
19
+ bpe = 8
20
+ max_elements = safe_bytes / bpe
21
+ max_bits = int(np.floor(np.log2(max(max_elements, 2.0))))
22
+ # Upper bound raised 27 -> 30 (prog.txt / user request): the real
23
+ # constraint is already _device_memory_budget_bytes's own VRAM/RAM
24
+ # reading (a 27-bit chunk is only 2.1GB regardless of dtype -- any
25
+ # GPU with more than ~2.5GB free was being capped well below what it
26
+ # could actually hold). 30 keeps a ceiling at all (avoids ever
27
+ # returning a chunk_size_bits so large a single chunk's own indexing
28
+ # arithmetic elsewhere risks int32 overflow) without arbitrarily
29
+ # discarding real headroom on 16GB+ GPUs (a Kaggle T4/P100 at
30
+ # ~0.85*16GB/16B per complex128 element already computes max_bits
31
+ # near 30 on its own -- this line was the thing throwing that away).
32
+ return max(16, min(max_bits, 30))
33
+
34
+
35
+ def _dtype_for_qubits(n_qubits: int):
36
+ xp = jnp if HAS_JAX else np
37
+ return xp.complex64 if n_qubits > 26 else xp.complex128
38
+
39
+
40
+ # ─────────────────────────────────────────────────────────────────────────────────
41
+ # MemoryChunker (chunk1)
42
+ # ─────────────────────────────────────────────────────────────────────────────────
43
+
44
+ class MemoryChunker:
45
+ """
46
+ Geometry calculator for chunked simulation.
47
+
48
+ Attributes
49
+ ----------
50
+ n_qubits int — requested logical qubit count
51
+ dtype — numpy/jax dtype for the statevector
52
+ chunk_size_bits int — safe qubit-width that fits in RAM
53
+ num_chunks int — number of statevector chunks required
54
+ chunk_dim int — 2 ** chunk_size_bits
55
+ """
56
+
57
+ def __init__(self, n_qubits: int):
58
+ # Looked up through the package's own top-level attribute, not
59
+ # this module's local `get_dynamic_chunk` name directly -- tests
60
+ # (and any caller) override chunk sizing via
61
+ # `dense_evolution.chunk.get_dynamic_chunk = ...`, the package's
62
+ # public attribute; a deferred `from . import` re-reads that
63
+ # attribute at call time instead of capturing this module's own
64
+ # function object once at import time, so an override actually
65
+ # takes effect on the next MemoryChunker() constructed.
66
+ from . import get_dynamic_chunk as _get_dynamic_chunk
67
+ self.n_qubits = n_qubits
68
+ self.dtype = _dtype_for_qubits(n_qubits)
69
+ self.chunk_size_bits = _get_dynamic_chunk(self.dtype)
70
+
71
+ if self.n_qubits <= self.chunk_size_bits:
72
+ self.num_chunks = 1
73
+ self.chunk_dim = 2 ** self.n_qubits
74
+ else:
75
+ self.num_chunks = 2 ** (self.n_qubits - self.chunk_size_bits)
76
+ self.chunk_dim = 2 ** self.chunk_size_bits
77
+
78
+ def geometry(self) -> Tuple[int, int, int]:
79
+ """(num_chunks, chunk_dim, chunk_size_bits)"""
80
+ return self.num_chunks, self.chunk_dim, self.chunk_size_bits
81
+
82
+ def memory_mb(self) -> float:
83
+ """Estimated RAM per chunk in MB."""
84
+ bpe = np.dtype(self.dtype).itemsize
85
+ return (self.chunk_dim * bpe) / (1024 * 1024)
86
+
87
+ def __repr__(self) -> str:
88
+ return (
89
+ f"MemoryChunker(n_qubits={self.n_qubits}, "
90
+ f"num_chunks={self.num_chunks}, "
91
+ f"chunk_dim={self.chunk_dim}, "
92
+ f"chunk_size_bits={self.chunk_size_bits}, "
93
+ f"dtype={self.dtype}, "
94
+ f"mem_per_chunk={self.memory_mb():.2f} MB)"
95
+ )
@@ -0,0 +1,190 @@
1
+ import gc
2
+ from typing import Tuple
3
+
4
+ import psutil
5
+
6
+ # JAX is now a mandatory dependency (see registry.py) -- kept as an
7
+ # import-time flag for the dual-path code below, which stays as-is.
8
+ import jax
9
+ HAS_JAX = True
10
+
11
+ __all__ = ["MemoryPressureError", "SafeMemoryGuard"]
12
+
13
+
14
+ def _device_memory_budget_bytes(device=None) -> Tuple[float, str]:
15
+ """Real, dynamic memory budget -- reads the ACTIVE compute device's
16
+ own memory via JAX's device.memory_stats() (bytes_limit -
17
+ bytes_in_use) when available, falling back to host RAM via
18
+ psutil.virtual_memory() otherwise (some backends, e.g. plain CPU,
19
+ return None from memory_stats() -- confirmed directly, so on CPU
20
+ this is identical to the old psutil-only behavior). get_dynamic_chunk
21
+ and SafeMemoryGuard used to read psutil.virtual_memory()
22
+ UNCONDITIONALLY, which is only correct when the compute device IS the
23
+ host (true on CPU, false on GPU/TPU) -- sizing chunks off host RAM
24
+ while the data actually lives in smaller GPU VRAM is exactly why
25
+ Chunk's own README benchmark ("~2GB constant RAM regardless of qubit
26
+ count") holds on CPU but silently mis-sized chunks on GPU, causing
27
+ MemoryPressureError/real OOM well before the GPU's own VRAM was
28
+ actually exhausted. Verified on a real Colab T4 GPU: before this fix,
29
+ chunk_size_bits was computed from host RAM (often >11GB free on
30
+ Colab) while the data lived in the T4's 11-15GB VRAM pool -- after
31
+ this fix, device.memory_stats() correctly reports VRAM instead.
32
+
33
+ Returns (available_bytes, source_description) -- the raw number, not
34
+ a safety-margined one; callers apply their own margin."""
35
+ if device is None:
36
+ device = jax.devices()[0] if HAS_JAX else None
37
+ stats = device.memory_stats() if device is not None and hasattr(device, "memory_stats") else None
38
+ if stats and stats.get("bytes_limit"):
39
+ available = stats["bytes_limit"] - stats.get("bytes_in_use", 0)
40
+ return float(available), f"device.memory_stats() on {device}"
41
+ vm = psutil.virtual_memory()
42
+ return float(vm.available), "psutil (host RAM) -- device.memory_stats() unavailable"
43
+
44
+
45
+ def _device_total_bytes(device=None) -> float:
46
+ """Companion to _device_memory_budget_bytes for the TOTAL (not
47
+ available) capacity -- used by SafeMemoryGuard to compute free_pct
48
+ against the right pool (device VRAM on GPU/TPU, host RAM on CPU)."""
49
+ if device is None:
50
+ device = jax.devices()[0] if HAS_JAX else None
51
+ stats = device.memory_stats() if device is not None and hasattr(device, "memory_stats") else None
52
+ if stats and stats.get("bytes_limit"):
53
+ return float(stats["bytes_limit"])
54
+ return float(psutil.virtual_memory().total)
55
+
56
+
57
+ # ─────────────────────────────────────────────────────────────────────────────────
58
+ # SafeMemoryGuard — Anti-OOM block
59
+ # ─────────────────────────────────────────────────────────────────────────────────
60
+
61
+ class MemoryPressureError(RuntimeError):
62
+ """
63
+ Raised when available system RAM drops below the configured safety threshold.
64
+ Catches the condition *before* the allocator attempts and crashes with
65
+ jaxlib.xla_extension.XlaRuntimeError: RESOURCE_EXHAUSTED.
66
+ """
67
+ pass
68
+
69
+
70
+ class SafeMemoryGuard:
71
+ """
72
+ Monitors system RAM before every high-memory operation and blocks execution
73
+ if free RAM falls below ``threshold_pct`` of total physical memory.
74
+ """
75
+
76
+ _WARN_MULTIPLIER = 2.0
77
+ # Below this, check_allocation's own margin-after-allocation math
78
+ # reduces to "is ambient free RAM currently below threshold_pct?",
79
+ # independent of required_mb -- for a request this small that's not
80
+ # protecting against anything, since real machines don't run out of
81
+ # memory over a request this size regardless of ambient load. Sized
82
+ # to comfortably cover a dense statevector up to 24 qubits at
83
+ # complex128 (2**24 * 16 bytes = 268.4 MB) with a real margin -- the
84
+ # same 24-qubit figure dashboard_core.system_limits already treats as
85
+ # MPS's own RAM-independent ceiling, and well inside the 16-27 qubit
86
+ # range dense_evolution.chunk.get_dynamic_chunk considers safe on any
87
+ # machine (27 qubits / 2 GB is that function's own practical ceiling
88
+ # for a single dense block). An 8 GB minimum-spec machine can always
89
+ # absorb an allocation this size without real risk, even while
90
+ # running low on RAM from unrelated processes. A 2-qubit statevector
91
+ # (64 bytes) used to be refused on a machine sitting at 8% free RAM
92
+ # from unrelated processes, which was not a real anti-OOM save, just
93
+ # a false positive from a threshold designed for actual multi-GB
94
+ # allocations. Only bypasses when available_mb is itself comfortably
95
+ # above this floor too (see check_allocation below) -- a machine
96
+ # truly down to its last few hundred MB still gets the real check.
97
+ _NEGLIGIBLE_MB = 512.0
98
+
99
+ def __init__(self, threshold_pct: float = 0.15, gc_before_check: bool = True):
100
+ if not 0.0 < threshold_pct < 1.0:
101
+ raise ValueError(f"threshold_pct must be in (0, 1), got {threshold_pct}")
102
+ self.threshold_pct = threshold_pct
103
+ self.gc_before_check = gc_before_check
104
+ # Device VRAM total on GPU/TPU, host RAM total on CPU (identical
105
+ # to the old psutil-only value there) -- see
106
+ # _device_memory_budget_bytes's docstring for why this matters.
107
+ self._total_mb = _device_total_bytes() / (1024 * 1024)
108
+
109
+ def status(self) -> dict:
110
+ available_bytes, _source = _device_memory_budget_bytes()
111
+ available_mb = available_bytes / (1024 * 1024)
112
+ free_pct = available_mb / self._total_mb if self._total_mb > 0 else 0.0
113
+ return {
114
+ "total_mb" : self._total_mb,
115
+ "available_mb": available_mb,
116
+ "used_pct" : (1.0 - free_pct) * 100.0,
117
+ "free_pct" : free_pct * 100.0,
118
+ "safe" : free_pct >= self.threshold_pct,
119
+ }
120
+
121
+ def check(self, context: str = "") -> None:
122
+ if self.gc_before_check:
123
+ gc.collect()
124
+
125
+ s = self.status()
126
+ tag = f"[{context}] " if context else ""
127
+ free_frac = s["free_pct"] / 100.0
128
+
129
+ if not s["safe"]:
130
+ raise MemoryPressureError(
131
+ f"\n{'─'*60}\n"
132
+ f" {tag}MEMORIA CRITICA — simulazione bloccata\n"
133
+ f" Disponibile : {s['available_mb']:.0f} MB "
134
+ f"({s['free_pct']:.1f}% libera)\n"
135
+ f" Soglia : {self.threshold_pct * 100:.0f}% "
136
+ f"({self._total_mb * self.threshold_pct:.0f} MB)\n"
137
+ f" Azione : liberare RAM o ridurre n_qubits / chunk_size.\n"
138
+ f"{'─'*60}"
139
+ )
140
+
141
+ warn_threshold = self.threshold_pct * self._WARN_MULTIPLIER
142
+ if free_frac < warn_threshold:
143
+ print(
144
+ f" [WARN] {tag}RAM bassa: {s['available_mb']:.0f} MB liberi "
145
+ f"({s['free_pct']:.1f}%) — soglia critica al "
146
+ f"{self.threshold_pct * 100:.0f}%."
147
+ )
148
+
149
+ def check_allocation(self, required_mb: float, context: str = "") -> None:
150
+ """Like check(), but sized: verifies that *required_mb* can be
151
+ allocated while still leaving threshold_pct free afterwards —
152
+ check() alone only looks at RAM free *right now*, independent of
153
+ what's about to be allocated (needed for Chunk's multi-chunk path,
154
+ where several chunk-sized simulators are held in RAM at once)."""
155
+ if self.gc_before_check:
156
+ gc.collect()
157
+
158
+ s = self.status()
159
+ if required_mb < self._NEGLIGIBLE_MB and s["available_mb"] > self._NEGLIGIBLE_MB:
160
+ return
161
+
162
+ tag = f"[{context}] " if context else ""
163
+ available_after_mb = s["available_mb"] - required_mb
164
+ free_frac_after = available_after_mb / self._total_mb if self._total_mb > 0 else 0.0
165
+
166
+ if available_after_mb < 0 or free_frac_after < self.threshold_pct:
167
+ import socket
168
+ raise MemoryPressureError(
169
+ f"\n{'─'*60}\n"
170
+ f" {tag}MEMORIA INSUFFICIENTE per l'allocazione richiesta\n"
171
+ f" Macchina : {socket.gethostname()} ({self._total_mb / 1024:.1f} GB RAM totali)\n"
172
+ f" Richiesti : {required_mb:.0f} MB\n"
173
+ f" Disponibile : {s['available_mb']:.0f} MB ({s['free_pct']:.1f}% libera)\n"
174
+ f" Dopo alloc. : {available_after_mb:.0f} MB ({free_frac_after * 100:.1f}% libera)\n"
175
+ f" Soglia : {self.threshold_pct * 100:.0f}% libera dopo l'allocazione\n"
176
+ f" Azione : ridurre n_qubits, liberare RAM, oppure passare\n"
177
+ f" allow_disk_overflow=True a Chunk() per continuare\n"
178
+ f" oltre la RAM disponibile usando lo storage su disco\n"
179
+ f" (vedi disk_overflow.py).\n"
180
+ f"{'─'*60}"
181
+ )
182
+
183
+ def __repr__(self) -> str:
184
+ s = self.status()
185
+ return (
186
+ f"SafeMemoryGuard("
187
+ f"threshold={self.threshold_pct*100:.0f}%, "
188
+ f"available={s['available_mb']:.0f} MB / {s['free_pct']:.1f}% free, "
189
+ f"safe={s['safe']})"
190
+ )