diffract-core 0.2.1__py3-none-any.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 (193) hide show
  1. diffract/__init__.py +52 -0
  2. diffract/configs/fast_speed_without_disk.ini +48 -0
  3. diffract/configs/hybrid.ini +74 -0
  4. diffract/configs/sqlite.ini +62 -0
  5. diffract/containers.py +405 -0
  6. diffract/core/__init__.py +23 -0
  7. diffract/core/cache/__init__.py +24 -0
  8. diffract/core/cache/containers.py +93 -0
  9. diffract/core/cache/interface.py +117 -0
  10. diffract/core/cache/redis_manager.py +464 -0
  11. diffract/core/cache/simple_manager.py +478 -0
  12. diffract/core/compute/__init__.py +46 -0
  13. diffract/core/compute/config.py +48 -0
  14. diffract/core/compute/containers.py +81 -0
  15. diffract/core/compute/decorator.py +125 -0
  16. diffract/core/compute/exceptions.py +31 -0
  17. diffract/core/compute/execution/__init__.py +14 -0
  18. diffract/core/compute/execution/_types.py +70 -0
  19. diffract/core/compute/execution/aggregation.py +74 -0
  20. diffract/core/compute/execution/aggregation_runner.py +362 -0
  21. diffract/core/compute/execution/enums.py +38 -0
  22. diffract/core/compute/execution/executor.py +125 -0
  23. diffract/core/compute/execution/parameter_runner.py +125 -0
  24. diffract/core/compute/execution/restrictions.py +61 -0
  25. diffract/core/compute/execution/strategy.py +118 -0
  26. diffract/core/compute/execution/utils.py +112 -0
  27. diffract/core/compute/extensions/__init__.py +0 -0
  28. diffract/core/compute/extensions/power_law.py +1051 -0
  29. diffract/core/compute/extensions/rmt.py +150 -0
  30. diffract/core/compute/extensions/utils.py +36 -0
  31. diffract/core/compute/field_signature.py +183 -0
  32. diffract/core/compute/kernels/__init__.py +48 -0
  33. diffract/core/compute/kernels/alignment.py +106 -0
  34. diffract/core/compute/kernels/heavy_tailed.py +394 -0
  35. diffract/core/compute/kernels/marchenko_pastur.py +99 -0
  36. diffract/core/compute/kernels/mat_decomposition.py +101 -0
  37. diffract/core/compute/kernels/mat_properties.py +53 -0
  38. diffract/core/compute/kernels/model_quality.py +27 -0
  39. diffract/core/compute/kernels/norms.py +88 -0
  40. diffract/core/compute/kernels/ranks.py +35 -0
  41. diffract/core/compute/kernels/tracy_widom.py +36 -0
  42. diffract/core/compute/metadata.py +67 -0
  43. diffract/core/compute/registry.py +485 -0
  44. diffract/core/constants.py +147 -0
  45. diffract/core/data/__init__.py +32 -0
  46. diffract/core/data/interface.py +304 -0
  47. diffract/core/data/metadata/__init__.py +15 -0
  48. diffract/core/data/metadata/containers.py +60 -0
  49. diffract/core/data/metadata/interface.py +208 -0
  50. diffract/core/data/metadata/sqlite_index.py +604 -0
  51. diffract/core/data/nn/__init__.py +43 -0
  52. diffract/core/data/nn/aggregates/__init__.py +35 -0
  53. diffract/core/data/nn/aggregates/metadata.py +96 -0
  54. diffract/core/data/nn/aggregates/proxy.py +57 -0
  55. diffract/core/data/nn/aggregates/repository.py +87 -0
  56. diffract/core/data/nn/aggregates/view.py +307 -0
  57. diffract/core/data/nn/containers.py +86 -0
  58. diffract/core/data/nn/extractors/__init__.py +49 -0
  59. diffract/core/data/nn/extractors/base.py +300 -0
  60. diffract/core/data/nn/extractors/factory.py +234 -0
  61. diffract/core/data/nn/extractors/flax.py +112 -0
  62. diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
  63. diffract/core/data/nn/extractors/handlers/base.py +110 -0
  64. diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
  65. diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
  66. diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
  67. diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
  68. diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
  69. diffract/core/data/nn/extractors/interface.py +56 -0
  70. diffract/core/data/nn/extractors/numpy.py +94 -0
  71. diffract/core/data/nn/extractors/onnx.py +108 -0
  72. diffract/core/data/nn/extractors/tensorflow.py +99 -0
  73. diffract/core/data/nn/extractors/torch.py +204 -0
  74. diffract/core/data/nn/params/__init__.py +27 -0
  75. diffract/core/data/nn/params/interface.py +402 -0
  76. diffract/core/data/nn/params/metadata.py +110 -0
  77. diffract/core/data/nn/params/proxy.py +93 -0
  78. diffract/core/data/nn/params/repository.py +45 -0
  79. diffract/core/data/nn/params/schema.py +82 -0
  80. diffract/core/data/nn/params/view.py +393 -0
  81. diffract/core/data/proxy.py +246 -0
  82. diffract/core/data/repository.py +227 -0
  83. diffract/core/data/utils.py +33 -0
  84. diffract/core/data/view.py +389 -0
  85. diffract/core/export/__init__.py +27 -0
  86. diffract/core/export/containers.py +47 -0
  87. diffract/core/export/exporters.py +191 -0
  88. diffract/core/export/formatters/__init__.py +23 -0
  89. diffract/core/export/formatters/base.py +68 -0
  90. diffract/core/export/formatters/dict_formatter.py +38 -0
  91. diffract/core/export/formatters/json_formatter.py +58 -0
  92. diffract/core/export/formatters/list_formatter.py +41 -0
  93. diffract/core/export/formatters/pandas_formatter.py +86 -0
  94. diffract/core/export/formatters/polars_formatter.py +86 -0
  95. diffract/core/export/formatters/registry.py +84 -0
  96. diffract/core/export/interface.py +105 -0
  97. diffract/core/parallel/__init__.py +21 -0
  98. diffract/core/parallel/containers.py +57 -0
  99. diffract/core/parallel/execution.py +91 -0
  100. diffract/core/parallel/runtime.py +91 -0
  101. diffract/core/storage/__init__.py +35 -0
  102. diffract/core/storage/base_manager.py +330 -0
  103. diffract/core/storage/containers.py +122 -0
  104. diffract/core/storage/hdf5_manager.py +856 -0
  105. diffract/core/storage/hybrid_manager.py +218 -0
  106. diffract/core/storage/interface.py +222 -0
  107. diffract/core/storage/metadata.py +89 -0
  108. diffract/core/storage/ram_manager.py +159 -0
  109. diffract/core/storage/sqlite_manager.py +1062 -0
  110. diffract/core/storage/zarr_manager.py +992 -0
  111. diffract/core/utils/__init__.py +45 -0
  112. diffract/core/utils/build.py +46 -0
  113. diffract/core/utils/exceptions.py +11 -0
  114. diffract/core/utils/hashing.py +90 -0
  115. diffract/core/utils/imports.py +292 -0
  116. diffract/core/utils/math.py +15 -0
  117. diffract/py.typed +0 -0
  118. diffract/session/__init__.py +24 -0
  119. diffract/session/errors.py +17 -0
  120. diffract/session/field_cache.py +216 -0
  121. diffract/session/namespaces/__init__.py +15 -0
  122. diffract/session/namespaces/compute/__init__.py +219 -0
  123. diffract/session/namespaces/models/__init__.py +282 -0
  124. diffract/session/namespaces/models/parameters/__init__.py +133 -0
  125. diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
  126. diffract/session/namespaces/results/__init__.py +378 -0
  127. diffract/session/namespaces/results/eraser.py +129 -0
  128. diffract/session/namespaces/results/injester.py +249 -0
  129. diffract/session/namespaces/utils/__init__.py +141 -0
  130. diffract/session/namespaces/utils/merger.py +295 -0
  131. diffract/session/namespaces/viz/__init__.py +91 -0
  132. diffract/session/namespaces/viz/_utils.py +14 -0
  133. diffract/session/namespaces/viz/box.py +207 -0
  134. diffract/session/namespaces/viz/grid.py +160 -0
  135. diffract/session/namespaces/viz/heatmap.py +164 -0
  136. diffract/session/namespaces/viz/scatter.py +202 -0
  137. diffract/session/namespaces/viz/sparkline.py +270 -0
  138. diffract/session/namespaces/viz/violin.py +212 -0
  139. diffract/session/session.py +260 -0
  140. diffract/session/utils.py +81 -0
  141. diffract/viz/__init__.py +42 -0
  142. diffract/viz/data/__init__.py +34 -0
  143. diffract/viz/data/detection.py +57 -0
  144. diffract/viz/data/extraction.py +117 -0
  145. diffract/viz/data/filtering.py +57 -0
  146. diffract/viz/data/ordering.py +129 -0
  147. diffract/viz/data/provider.py +99 -0
  148. diffract/viz/data/types.py +98 -0
  149. diffract/viz/plots/__init__.py +37 -0
  150. diffract/viz/plots/base/__init__.py +20 -0
  151. diffract/viz/plots/base/axis.py +219 -0
  152. diffract/viz/plots/base/coloraxis.py +133 -0
  153. diffract/viz/plots/base/configurator.py +18 -0
  154. diffract/viz/plots/base/jitter.py +368 -0
  155. diffract/viz/plots/base/line.py +197 -0
  156. diffract/viz/plots/base/marker.py +278 -0
  157. diffract/viz/plots/base/overlay.py +14 -0
  158. diffract/viz/plots/base/plot.py +110 -0
  159. diffract/viz/plots/base/update.py +50 -0
  160. diffract/viz/plots/boxplot.py +283 -0
  161. diffract/viz/plots/cluster.py +374 -0
  162. diffract/viz/plots/heatmap.py +168 -0
  163. diffract/viz/plots/scatter.py +233 -0
  164. diffract/viz/plots/sparkline.py +405 -0
  165. diffract/viz/plots/subplots/__init__.py +32 -0
  166. diffract/viz/plots/subplots/coloraxis.py +339 -0
  167. diffract/viz/plots/subplots/factory.py +713 -0
  168. diffract/viz/plots/subplots/grid.py +214 -0
  169. diffract/viz/plots/subplots/layout.py +370 -0
  170. diffract/viz/plots/subplots/spec.py +39 -0
  171. diffract/viz/plots/violin.py +298 -0
  172. diffract/viz/renderer.py +513 -0
  173. diffract/viz/styling/__init__.py +78 -0
  174. diffract/viz/styling/palettes/__init__.py +20 -0
  175. diffract/viz/styling/palettes/bundle.py +16 -0
  176. diffract/viz/styling/palettes/color.py +83 -0
  177. diffract/viz/styling/palettes/dashes.py +27 -0
  178. diffract/viz/styling/palettes/symbols.py +42 -0
  179. diffract/viz/styling/resolvers/__init__.py +11 -0
  180. diffract/viz/styling/resolvers/categorical.py +68 -0
  181. diffract/viz/styling/resolvers/color.py +77 -0
  182. diffract/viz/styling/resolvers/numeric.py +108 -0
  183. diffract/viz/styling/sources.py +27 -0
  184. diffract/viz/styling/theme/__init__.py +29 -0
  185. diffract/viz/styling/theme/applier.py +114 -0
  186. diffract/viz/styling/theme/components.py +66 -0
  187. diffract/viz/styling/theme/presets.py +58 -0
  188. diffract/viz/styling/theme/theme.py +36 -0
  189. diffract_core-0.2.1.dist-info/METADATA +530 -0
  190. diffract_core-0.2.1.dist-info/RECORD +193 -0
  191. diffract_core-0.2.1.dist-info/WHEEL +4 -0
  192. diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
  193. diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,478 @@
1
+ """In-memory LRU cache manager implementation.
2
+
3
+ This module provides a pure Python in-memory cache manager that implements
4
+ the ICacheManager protocol. It uses OrderedDict for LRU tracking and
5
+ pickle for size estimation, making it suitable for development and testing
6
+ environments where Redis is not available.
7
+
8
+ Features:
9
+ - Process-local LRU cache with configurable memory limits
10
+ - Optional TTL support for cache entries
11
+ - Thread-safe via GIL (single-threaded access)
12
+ - Pickle-based size estimation for memory accounting
13
+ - No external dependencies
14
+
15
+ Limitations:
16
+ - Process-local only (not suitable for multi-process production)
17
+ - Approximate memory accounting using pickle size
18
+ - Single-threaded performance characteristics
19
+
20
+ Example:
21
+ >>> manager = SimpleLRUCacheManager(max_memory_mb=128, ttl_seconds=3600)
22
+ >>> manager.set_field("user123", "profile", user_data)
23
+ >>> profile = manager.get_field("user123", "profile")
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import pickle
29
+ import threading
30
+ import time
31
+ from collections import OrderedDict
32
+ from typing import Any
33
+
34
+ from .interface import UID, ICacheManager
35
+
36
+
37
+ class SimpleLRUCacheManager(ICacheManager):
38
+ """In-memory LRU cache manager with TTL support and thread-safety.
39
+
40
+ Implements ICacheManager using pure Python data structures for caching.
41
+ Uses OrderedDict to maintain LRU order and pickle serialization for
42
+ memory size calculation with overhead accounting.
43
+
44
+ This implementation is designed for single-process applications and
45
+ development environments. For production multi-process setups,
46
+ use RedisLRUCacheManager instead.
47
+
48
+ Features:
49
+ - Thread-safe operations via internal locking
50
+ - Accurate memory accounting with overhead factor
51
+ - Efficient field-based indexing for fast lookups
52
+ - Automatic cleanup of expired entries
53
+ - LRU eviction with configurable memory limits
54
+
55
+ Attributes:
56
+ _max_bytes: Maximum cache size in bytes.
57
+ _ttl: Time-to-live for cache entries in seconds.
58
+ _key_prefix: Prefix for cache keys.
59
+ _store: Main storage mapping keys to (data, timestamp) tuples.
60
+ _lru: OrderedDict tracking LRU order of keys.
61
+ _field_index: Index mapping field names to sets of UIDs.
62
+ _current_bytes: Current estimated cache size in bytes.
63
+ _lock: Thread lock for thread-safe operations.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ *,
69
+ max_memory_mb: int = 256,
70
+ ttl_seconds: int | None = None,
71
+ key_prefix: str = "diffract:cache:",
72
+ memory_overhead_factor: float = 1.1,
73
+ ) -> None:
74
+ """Initialize the simple LRU cache manager.
75
+
76
+ Args:
77
+ max_memory_mb: Maximum cache size in MB (must be positive).
78
+ ttl_seconds: Time-to-live for entries in seconds (None = no expiry).
79
+ key_prefix: Prefix for cache keys to avoid collisions.
80
+ memory_overhead_factor: Factor to account for Python object overhead
81
+ (dict entries, OrderedDict entries, string keys, metadata).
82
+
83
+ Raises:
84
+ ValueError: If max_memory_mb is not positive or memory_overhead_factor <= 0.
85
+ """
86
+ if max_memory_mb <= 0:
87
+ msg = "max_memory_mb must be positive"
88
+ raise ValueError(msg)
89
+
90
+ if memory_overhead_factor <= 0:
91
+ msg = "memory_overhead_factor must be positive"
92
+ raise ValueError(msg)
93
+
94
+ self._max_bytes = max_memory_mb * 1024 * 1024
95
+ self._ttl = ttl_seconds
96
+ self._key_prefix = key_prefix
97
+ self._memory_overhead_factor = memory_overhead_factor
98
+
99
+ self._store: dict[str, tuple[bytes, float]] = {}
100
+ self._lru: OrderedDict[str, None] = OrderedDict()
101
+ self._field_index: dict[str, set[str]] = {}
102
+ self._current_bytes = 0
103
+ self._lock = threading.Lock()
104
+
105
+ def _make_key(self, obj_uid: UID, field_name: str) -> str:
106
+ """Generate cache key for object field.
107
+
108
+ Args:
109
+ obj_uid: Unique identifier for the cached object.
110
+ field_name: Name of the field.
111
+
112
+ Returns:
113
+ String cache key.
114
+ """
115
+ return f"{self._key_prefix}{obj_uid}:{field_name}"
116
+
117
+ def _calculate_entry_size(self, key: str, data: bytes) -> int:
118
+ """Calculate estimated memory size of a cache entry.
119
+
120
+ Accounts for key string, data bytes, timestamp (float), tuple overhead,
121
+ and Python object overhead using a multiplier factor.
122
+
123
+ Args:
124
+ key: Cache key string.
125
+ data: Pickled data bytes.
126
+
127
+ Returns:
128
+ Estimated memory size in bytes.
129
+ """
130
+ # Base size: key length + data length + timestamp (8 bytes for float)
131
+ base_size = len(key.encode("utf-8")) + len(data) + 8
132
+ # Apply overhead factor for Python object overhead
133
+ return int(base_size * self._memory_overhead_factor)
134
+
135
+ def _extract_uid_field(self, key: str) -> tuple[str, str] | None:
136
+ """Extract UID and field name from cache key.
137
+
138
+ Args:
139
+ key: Cache key string.
140
+
141
+ Returns:
142
+ Tuple of (uid, field_name) or None if key format is invalid.
143
+ """
144
+ if not key.startswith(self._key_prefix):
145
+ return None
146
+ suffix = key[len(self._key_prefix) :]
147
+ if ":" not in suffix:
148
+ return None
149
+ uid, field_name = suffix.split(":", 1)
150
+ return uid, field_name
151
+
152
+ def _update_field_index(self, obj_uid: str, field_name: str, adding: bool) -> None:
153
+ """Update the field index when adding or removing entries.
154
+
155
+ Args:
156
+ obj_uid: Object UID.
157
+ field_name: Field name.
158
+ adding: True if adding entry, False if removing.
159
+ """
160
+ if adding:
161
+ if field_name not in self._field_index:
162
+ self._field_index[field_name] = set()
163
+ self._field_index[field_name].add(obj_uid)
164
+ elif field_name in self._field_index:
165
+ self._field_index[field_name].discard(obj_uid)
166
+ if not self._field_index[field_name]:
167
+ del self._field_index[field_name]
168
+
169
+ def _cleanup_expired_entries(self) -> None:
170
+ """Clean up expired entries from cache.
171
+
172
+ Iterates through all entries and removes those that have expired.
173
+ This is called periodically to prevent accumulation of expired entries.
174
+ """
175
+ if self._ttl is None:
176
+ return
177
+
178
+ now = time.time()
179
+ expired_keys = []
180
+
181
+ for key, (_data, ts) in self._store.items():
182
+ if (now - ts) > self._ttl:
183
+ expired_keys.append(key)
184
+
185
+ for key in expired_keys:
186
+ data, _ = self._store.pop(key, (b"", 0.0))
187
+ entry_size = self._calculate_entry_size(key, data)
188
+ self._current_bytes -= entry_size
189
+ self._lru.pop(key, None)
190
+
191
+ # Update field index
192
+ uid_field = self._extract_uid_field(key)
193
+ if uid_field:
194
+ uid, field_name = uid_field
195
+ self._update_field_index(uid, field_name, adding=False)
196
+
197
+ def _expired(self, ts: float) -> bool:
198
+ """Check if a timestamp indicates an expired entry.
199
+
200
+ Args:
201
+ ts: Timestamp to check against current time.
202
+
203
+ Returns:
204
+ True if entry is expired, False otherwise.
205
+ """
206
+ return self._ttl is not None and (time.time() - ts) > self._ttl
207
+
208
+ def _touch_lru(self, key: str) -> None:
209
+ """Update LRU position for a key.
210
+
211
+ Moves key to end of LRU order (most recently used position).
212
+ If key doesn't exist in LRU tracking, adds it.
213
+
214
+ Args:
215
+ key: Cache key to update LRU position for.
216
+ """
217
+ if key in self._lru:
218
+ self._lru.move_to_end(key)
219
+ else:
220
+ self._lru[key] = None
221
+
222
+ def _evict_until_fit(self) -> None:
223
+ """Evict least recently used entries until under memory limit.
224
+
225
+ Removes entries from the beginning of LRU order until the current
226
+ cache size is within the configured memory limit.
227
+ """
228
+ while self._current_bytes > self._max_bytes and self._lru:
229
+ oldest_key, _ = self._lru.popitem(last=False)
230
+ data, _ = self._store.pop(oldest_key, (b"", 0.0))
231
+ entry_size = self._calculate_entry_size(oldest_key, data)
232
+ self._current_bytes -= entry_size
233
+
234
+ # Update field index
235
+ uid_field = self._extract_uid_field(oldest_key)
236
+ if uid_field:
237
+ uid, field_name = uid_field
238
+ self._update_field_index(uid, field_name, adding=False)
239
+
240
+ def has_field(self, obj_uid: UID, field_name: str) -> bool:
241
+ """Check if an object has a specific field in cache.
242
+
243
+ Args:
244
+ obj_uid: Unique identifier for the cached object.
245
+ field_name: Name of the field to check.
246
+
247
+ Returns:
248
+ True if the field exists and is not expired, False otherwise.
249
+ """
250
+ key = self._make_key(obj_uid, field_name)
251
+ with self._lock:
252
+ item = self._store.get(key)
253
+
254
+ if not item:
255
+ return False
256
+
257
+ data, ts = item
258
+ if self._expired(ts):
259
+ entry_size = self._calculate_entry_size(key, data)
260
+ self._store.pop(key, None)
261
+ self._lru.pop(key, None)
262
+ self._current_bytes -= entry_size
263
+ self._update_field_index(obj_uid, field_name, adding=False)
264
+ return False
265
+
266
+ return True
267
+
268
+ def get_field(self, obj_uid: UID, field_name: str) -> Any:
269
+ """Retrieve a field value from cache.
270
+
271
+ Args:
272
+ obj_uid: Unique identifier for the cached object.
273
+ field_name: Name of the field to retrieve.
274
+
275
+ Returns:
276
+ The cached value or None if field doesn't exist or is expired.
277
+ """
278
+ key = self._make_key(obj_uid, field_name)
279
+ with self._lock:
280
+ item = self._store.get(key)
281
+
282
+ if not item:
283
+ return None
284
+
285
+ data, ts = item
286
+
287
+ if self._expired(ts):
288
+ entry_size = self._calculate_entry_size(key, data)
289
+ self._store.pop(key, None)
290
+ self._lru.pop(key, None)
291
+ self._current_bytes -= entry_size
292
+ self._update_field_index(obj_uid, field_name, adding=False)
293
+ return None
294
+
295
+ self._touch_lru(key)
296
+
297
+ try:
298
+ return pickle.loads(data) # noqa: S301
299
+ except (
300
+ ModuleNotFoundError,
301
+ ImportError,
302
+ AttributeError,
303
+ ValueError,
304
+ pickle.UnpicklingError,
305
+ EOFError,
306
+ ):
307
+ # Cache may contain pickles produced by a different Python/numpy
308
+ # version. Treat as a cache miss and delete the corrupted entry.
309
+ entry_size = self._calculate_entry_size(key, data)
310
+ self._store.pop(key, None)
311
+ self._lru.pop(key, None)
312
+ self._current_bytes -= entry_size
313
+ self._update_field_index(obj_uid, field_name, adding=False)
314
+ return None
315
+
316
+ def list_objs_has_field(self, field_name: str) -> list[UID]:
317
+ """List all object UIDs that have a specific field.
318
+
319
+ Uses field index for efficient lookup when available, falling back
320
+ to iteration through all keys with cleanup of expired entries.
321
+
322
+ Args:
323
+ field_name: Name of the field to search for.
324
+
325
+ Returns:
326
+ List of UIDs for objects that have the specified field.
327
+ """
328
+ with self._lock:
329
+ # Use field index for efficient lookup if available
330
+ if field_name in self._field_index:
331
+ # Clean up expired entries for this field
332
+ valid_uids = []
333
+ for uid in self._field_index[field_name]:
334
+ key = self._make_key(uid, field_name)
335
+ item = self._store.get(key)
336
+ if item:
337
+ data, ts = item
338
+ if not self._expired(ts):
339
+ valid_uids.append(uid)
340
+ else:
341
+ # Clean up expired entry
342
+ entry_size = self._calculate_entry_size(key, data)
343
+ self._store.pop(key, None)
344
+ self._lru.pop(key, None)
345
+ self._current_bytes -= entry_size
346
+ # Update index if some entries were expired
347
+ if len(valid_uids) != len(self._field_index[field_name]):
348
+ self._field_index[field_name] = set(valid_uids)
349
+ return valid_uids
350
+
351
+ # Fallback: iterate through all keys
352
+ suffix = f":{field_name}"
353
+ result: list[str] = []
354
+ now = time.time()
355
+
356
+ # Iterate over snapshot to avoid mutation during iteration
357
+ for key in tuple(self._store.keys()):
358
+ if not key.endswith(suffix) or not key.startswith(self._key_prefix):
359
+ continue
360
+
361
+ data, ts = self._store.get(key, (b"", 0.0))
362
+ if self._ttl is not None and (now - ts) > self._ttl:
363
+ entry_size = self._calculate_entry_size(key, data)
364
+ self._store.pop(key, None)
365
+ self._lru.pop(key, None)
366
+ self._current_bytes -= entry_size
367
+ # Update field index
368
+ uid_field = self._extract_uid_field(key)
369
+ if uid_field:
370
+ uid, _ = uid_field
371
+ self._update_field_index(uid, field_name, adding=False)
372
+ continue
373
+
374
+ uid = key[len(self._key_prefix) : -len(suffix)]
375
+ if uid:
376
+ result.append(uid)
377
+
378
+ return result
379
+
380
+ def set_field(self, obj_uid: UID, field_name: str, value: Any) -> None:
381
+ """Store a field value in cache.
382
+
383
+ Args:
384
+ obj_uid: Unique identifier for the cached object.
385
+ field_name: Name of the field to store.
386
+ value: Value to cache (must be pickle-serializable).
387
+ """
388
+ key = self._make_key(obj_uid, field_name)
389
+ data = pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)
390
+ entry_size = self._calculate_entry_size(key, data)
391
+
392
+ with self._lock:
393
+ # Remove old entry size if updating existing key
394
+ old = self._store.get(key)
395
+ if old:
396
+ old_data, _ = old
397
+ old_entry_size = self._calculate_entry_size(key, old_data)
398
+ self._current_bytes -= old_entry_size
399
+
400
+ self._store[key] = (data, time.time())
401
+ self._current_bytes += entry_size
402
+ self._touch_lru(key)
403
+ self._update_field_index(obj_uid, field_name, adding=True)
404
+
405
+ # Periodic cleanup of expired entries (every 100 operations)
406
+ if len(self._store) % 100 == 0:
407
+ self._cleanup_expired_entries()
408
+
409
+ self._evict_until_fit()
410
+
411
+ def erase_field(self, obj_uid: UID, field_name: str) -> None:
412
+ """Remove a specific field from cache.
413
+
414
+ Args:
415
+ obj_uid: Unique identifier for the cached object.
416
+ field_name: Name of the field to remove.
417
+ """
418
+ key = self._make_key(obj_uid, field_name)
419
+ with self._lock:
420
+ item = self._store.pop(key, None)
421
+ if item:
422
+ data, _ = item
423
+ entry_size = self._calculate_entry_size(key, data)
424
+ self._current_bytes -= entry_size
425
+ self._update_field_index(obj_uid, field_name, adding=False)
426
+ self._lru.pop(key, None)
427
+
428
+ def erase_field_for_all(self, field_name: str) -> None:
429
+ """Remove a field from all cached objects.
430
+
431
+ Args:
432
+ field_name: Name of the field to remove from all objects.
433
+ """
434
+ with self._lock:
435
+ # Use field index for efficient removal if available
436
+ if field_name in self._field_index:
437
+ uids_to_remove = list(self._field_index[field_name])
438
+ for uid in uids_to_remove:
439
+ key = self._make_key(uid, field_name)
440
+ item = self._store.pop(key, None)
441
+ if item:
442
+ data, _ = item
443
+ entry_size = self._calculate_entry_size(key, data)
444
+ self._current_bytes -= entry_size
445
+ self._lru.pop(key, None)
446
+ # Clear the field index entry
447
+ del self._field_index[field_name]
448
+ else:
449
+ # Fallback to iteration if index not available
450
+ suffix = f":{field_name}"
451
+ # Iterate over snapshot to avoid mutation during iteration
452
+ for key in tuple(self._store.keys()):
453
+ if key.endswith(suffix) and key.startswith(self._key_prefix):
454
+ data, _ = self._store.pop(key, (b"", 0.0))
455
+ entry_size = self._calculate_entry_size(key, data)
456
+ self._current_bytes -= entry_size
457
+ self._lru.pop(key, None)
458
+ # Update field index
459
+ uid_field = self._extract_uid_field(key)
460
+ if uid_field:
461
+ uid, _ = uid_field
462
+ self._update_field_index(uid, field_name, adding=False)
463
+
464
+ def clear(self) -> None:
465
+ """Remove all cached data.
466
+
467
+ Completely empties the cache and resets memory usage counters.
468
+ """
469
+ with self._lock:
470
+ self._store.clear()
471
+ self._lru.clear()
472
+ self._field_index.clear()
473
+ self._current_bytes = 0
474
+
475
+ def get_available_bytes(self) -> int:
476
+ """Return estimated available cache capacity in bytes."""
477
+ with self._lock:
478
+ return max(0, self._max_bytes - self._current_bytes)
@@ -0,0 +1,46 @@
1
+ """Compute subsystem public API and shortcuts.
2
+
3
+ This package exposes the kernel registry and executor along with common
4
+ enumerations and decorators used across the compute pipeline.
5
+
6
+ Example:
7
+ >>> from diffract.core.compute import KernelExecutor, kernel
8
+ >>> with KernelExecutor(registry) as ex:
9
+ ... ex.execute("my_kernel", params)
10
+ """
11
+
12
+ from diffract.core.compute.execution.enums import (
13
+ KernelApplyLevel,
14
+ KernelExecutionProtocol,
15
+ KernelRestrictions,
16
+ )
17
+ from diffract.core.compute.execution.executor import KernelExecutor
18
+
19
+ from .config import KernelConfig
20
+ from .decorator import kernel, register_default_kernels
21
+ from .exceptions import (
22
+ CircularDependencyError,
23
+ DependencyNotFoundError,
24
+ InconsistentWiring,
25
+ InvalidConfiguration,
26
+ KernelError,
27
+ KernelExecutionError,
28
+ )
29
+ from .registry import KernelRegistry
30
+
31
+ __all__ = [
32
+ "CircularDependencyError",
33
+ "DependencyNotFoundError",
34
+ "InconsistentWiring",
35
+ "InvalidConfiguration",
36
+ "KernelApplyLevel",
37
+ "KernelConfig",
38
+ "KernelError",
39
+ "KernelExecutionError",
40
+ "KernelExecutionProtocol",
41
+ "KernelExecutor",
42
+ "KernelRegistry",
43
+ "KernelRestrictions",
44
+ "kernel",
45
+ "register_default_kernels",
46
+ ]
@@ -0,0 +1,48 @@
1
+ """Kernel configuration management and validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .exceptions import InvalidConfiguration
8
+
9
+
10
+ class KernelConfig:
11
+ """Configuration container with defaults and safe updates.
12
+
13
+ Manages kernel configuration parameters with validation against
14
+ predefined defaults and safe merging of configuration updates.
15
+ """
16
+
17
+ def __init__(self, **kwargs: Any) -> None:
18
+ """Initialize configuration with default values.
19
+
20
+ Args:
21
+ **kwargs: Default configuration parameters.
22
+ """
23
+ self._defaults: dict[str, Any] = dict(kwargs)
24
+ self._values: dict[str, Any] = dict(kwargs)
25
+
26
+ def as_dict(self) -> dict[str, Any]:
27
+ """Return current configuration as a dictionary.
28
+
29
+ Returns:
30
+ Dictionary containing current configuration values.
31
+ """
32
+ return dict(self._values)
33
+
34
+ def update(self, other: KernelConfig) -> None:
35
+ """Update current configuration with another KernelConfig.
36
+
37
+ Args:
38
+ other: KernelConfig instance containing updates.
39
+
40
+ Raises:
41
+ InvalidConfiguration: If unknown configuration keys are present.
42
+ """
43
+ upd = other.as_dict()
44
+ invalid = set(upd.keys()) - set(self._defaults.keys())
45
+ if invalid:
46
+ msg = f"Invalid kernel configuration parameters: {sorted(invalid)}"
47
+ raise InvalidConfiguration(msg)
48
+ self._values.update(upd)
@@ -0,0 +1,81 @@
1
+ """Dependency injection container for compute subsystem.
2
+
3
+ Provides providers for the kernel registry, default kernel registration,
4
+ and the kernel executor resource. Configuration for the executor is taken
5
+ from the container configuration under the "executor" key.
6
+
7
+ Example:
8
+ >>> container = ComputeContainer()
9
+ >>> with container.kernel_executor() as executor:
10
+ ... executor.execute("field_x", collection)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from concurrent.futures import ProcessPoolExecutor
16
+ from typing import ClassVar
17
+
18
+ from dependency_injector import containers, providers
19
+
20
+ from diffract.core.parallel import ParallelContext
21
+ from diffract.core.utils.build import build_with_defaults
22
+
23
+ from .decorator import register_default_kernels
24
+ from .execution import KernelExecutor
25
+ from .registry import KernelRegistry
26
+
27
+
28
+ class ComputeSingletonContainer(containers.DeclarativeContainer):
29
+ """Singleton providers for compute subsystem."""
30
+
31
+ kernel_registry = providers.Singleton(KernelRegistry)
32
+
33
+ register_default_kernels = providers.Callable(
34
+ register_default_kernels, kernel_registry
35
+ )
36
+
37
+
38
+ def _config_to_dict(cfg: dict | None) -> dict:
39
+ """Convert config to dict, returning empty dict if None."""
40
+ return cfg if cfg is not None else {}
41
+
42
+
43
+ class ComputeContainer(containers.DeclarativeContainer):
44
+ """Container for compute-related dependencies.
45
+
46
+ Provides singleton kernel registry, default kernel registration callable,
47
+ and resource-managed kernel executor with configuration support.
48
+ """
49
+
50
+ config = providers.Configuration()
51
+
52
+ compute_singleton = providers.Container(ComputeSingletonContainer)
53
+ parallel = providers.Dependency(instance_of=ParallelContext)
54
+ process_pool = providers.Dependency(instance_of=ProcessPoolExecutor)
55
+ aggregate_repository = providers.Dependency()
56
+
57
+ kernel_registry = providers.Singleton(compute_singleton.kernel_registry)
58
+
59
+ _executor_config = providers.Callable(_config_to_dict, config.executor)
60
+
61
+ kernel_executor = providers.Resource(
62
+ build_with_defaults,
63
+ KernelExecutor,
64
+ _executor_config,
65
+ registry=kernel_registry,
66
+ process_pool=process_pool,
67
+ parallel=parallel,
68
+ aggregate_repository=aggregate_repository,
69
+ )
70
+
71
+
72
+ class ComputeSingletonContainerWiringConfig:
73
+ """Wiring configuration for compute module.
74
+
75
+ Specifies the container class, modules to wire, and packages for
76
+ dependency injection setup in the compute subsystem.
77
+ """
78
+
79
+ container: ClassVar[type[ComputeSingletonContainer]] = ComputeSingletonContainer
80
+ modules: ClassVar[tuple[str, ...]] = ("diffract.core.compute.decorator",)
81
+ packages: ClassVar[tuple[str, ...]] = ()