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.
- diffract/__init__.py +52 -0
- diffract/configs/fast_speed_without_disk.ini +48 -0
- diffract/configs/hybrid.ini +74 -0
- diffract/configs/sqlite.ini +62 -0
- diffract/containers.py +405 -0
- diffract/core/__init__.py +23 -0
- diffract/core/cache/__init__.py +24 -0
- diffract/core/cache/containers.py +93 -0
- diffract/core/cache/interface.py +117 -0
- diffract/core/cache/redis_manager.py +464 -0
- diffract/core/cache/simple_manager.py +478 -0
- diffract/core/compute/__init__.py +46 -0
- diffract/core/compute/config.py +48 -0
- diffract/core/compute/containers.py +81 -0
- diffract/core/compute/decorator.py +125 -0
- diffract/core/compute/exceptions.py +31 -0
- diffract/core/compute/execution/__init__.py +14 -0
- diffract/core/compute/execution/_types.py +70 -0
- diffract/core/compute/execution/aggregation.py +74 -0
- diffract/core/compute/execution/aggregation_runner.py +362 -0
- diffract/core/compute/execution/enums.py +38 -0
- diffract/core/compute/execution/executor.py +125 -0
- diffract/core/compute/execution/parameter_runner.py +125 -0
- diffract/core/compute/execution/restrictions.py +61 -0
- diffract/core/compute/execution/strategy.py +118 -0
- diffract/core/compute/execution/utils.py +112 -0
- diffract/core/compute/extensions/__init__.py +0 -0
- diffract/core/compute/extensions/power_law.py +1051 -0
- diffract/core/compute/extensions/rmt.py +150 -0
- diffract/core/compute/extensions/utils.py +36 -0
- diffract/core/compute/field_signature.py +183 -0
- diffract/core/compute/kernels/__init__.py +48 -0
- diffract/core/compute/kernels/alignment.py +106 -0
- diffract/core/compute/kernels/heavy_tailed.py +394 -0
- diffract/core/compute/kernels/marchenko_pastur.py +99 -0
- diffract/core/compute/kernels/mat_decomposition.py +101 -0
- diffract/core/compute/kernels/mat_properties.py +53 -0
- diffract/core/compute/kernels/model_quality.py +27 -0
- diffract/core/compute/kernels/norms.py +88 -0
- diffract/core/compute/kernels/ranks.py +35 -0
- diffract/core/compute/kernels/tracy_widom.py +36 -0
- diffract/core/compute/metadata.py +67 -0
- diffract/core/compute/registry.py +485 -0
- diffract/core/constants.py +147 -0
- diffract/core/data/__init__.py +32 -0
- diffract/core/data/interface.py +304 -0
- diffract/core/data/metadata/__init__.py +15 -0
- diffract/core/data/metadata/containers.py +60 -0
- diffract/core/data/metadata/interface.py +208 -0
- diffract/core/data/metadata/sqlite_index.py +604 -0
- diffract/core/data/nn/__init__.py +43 -0
- diffract/core/data/nn/aggregates/__init__.py +35 -0
- diffract/core/data/nn/aggregates/metadata.py +96 -0
- diffract/core/data/nn/aggregates/proxy.py +57 -0
- diffract/core/data/nn/aggregates/repository.py +87 -0
- diffract/core/data/nn/aggregates/view.py +307 -0
- diffract/core/data/nn/containers.py +86 -0
- diffract/core/data/nn/extractors/__init__.py +49 -0
- diffract/core/data/nn/extractors/base.py +300 -0
- diffract/core/data/nn/extractors/factory.py +234 -0
- diffract/core/data/nn/extractors/flax.py +112 -0
- diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
- diffract/core/data/nn/extractors/handlers/base.py +110 -0
- diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
- diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
- diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
- diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
- diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
- diffract/core/data/nn/extractors/interface.py +56 -0
- diffract/core/data/nn/extractors/numpy.py +94 -0
- diffract/core/data/nn/extractors/onnx.py +108 -0
- diffract/core/data/nn/extractors/tensorflow.py +99 -0
- diffract/core/data/nn/extractors/torch.py +204 -0
- diffract/core/data/nn/params/__init__.py +27 -0
- diffract/core/data/nn/params/interface.py +402 -0
- diffract/core/data/nn/params/metadata.py +110 -0
- diffract/core/data/nn/params/proxy.py +93 -0
- diffract/core/data/nn/params/repository.py +45 -0
- diffract/core/data/nn/params/schema.py +82 -0
- diffract/core/data/nn/params/view.py +393 -0
- diffract/core/data/proxy.py +246 -0
- diffract/core/data/repository.py +227 -0
- diffract/core/data/utils.py +33 -0
- diffract/core/data/view.py +389 -0
- diffract/core/export/__init__.py +27 -0
- diffract/core/export/containers.py +47 -0
- diffract/core/export/exporters.py +191 -0
- diffract/core/export/formatters/__init__.py +23 -0
- diffract/core/export/formatters/base.py +68 -0
- diffract/core/export/formatters/dict_formatter.py +38 -0
- diffract/core/export/formatters/json_formatter.py +58 -0
- diffract/core/export/formatters/list_formatter.py +41 -0
- diffract/core/export/formatters/pandas_formatter.py +86 -0
- diffract/core/export/formatters/polars_formatter.py +86 -0
- diffract/core/export/formatters/registry.py +84 -0
- diffract/core/export/interface.py +105 -0
- diffract/core/parallel/__init__.py +21 -0
- diffract/core/parallel/containers.py +57 -0
- diffract/core/parallel/execution.py +91 -0
- diffract/core/parallel/runtime.py +91 -0
- diffract/core/storage/__init__.py +35 -0
- diffract/core/storage/base_manager.py +330 -0
- diffract/core/storage/containers.py +122 -0
- diffract/core/storage/hdf5_manager.py +856 -0
- diffract/core/storage/hybrid_manager.py +218 -0
- diffract/core/storage/interface.py +222 -0
- diffract/core/storage/metadata.py +89 -0
- diffract/core/storage/ram_manager.py +159 -0
- diffract/core/storage/sqlite_manager.py +1062 -0
- diffract/core/storage/zarr_manager.py +992 -0
- diffract/core/utils/__init__.py +45 -0
- diffract/core/utils/build.py +46 -0
- diffract/core/utils/exceptions.py +11 -0
- diffract/core/utils/hashing.py +90 -0
- diffract/core/utils/imports.py +292 -0
- diffract/core/utils/math.py +15 -0
- diffract/py.typed +0 -0
- diffract/session/__init__.py +24 -0
- diffract/session/errors.py +17 -0
- diffract/session/field_cache.py +216 -0
- diffract/session/namespaces/__init__.py +15 -0
- diffract/session/namespaces/compute/__init__.py +219 -0
- diffract/session/namespaces/models/__init__.py +282 -0
- diffract/session/namespaces/models/parameters/__init__.py +133 -0
- diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
- diffract/session/namespaces/results/__init__.py +378 -0
- diffract/session/namespaces/results/eraser.py +129 -0
- diffract/session/namespaces/results/injester.py +249 -0
- diffract/session/namespaces/utils/__init__.py +141 -0
- diffract/session/namespaces/utils/merger.py +295 -0
- diffract/session/namespaces/viz/__init__.py +91 -0
- diffract/session/namespaces/viz/_utils.py +14 -0
- diffract/session/namespaces/viz/box.py +207 -0
- diffract/session/namespaces/viz/grid.py +160 -0
- diffract/session/namespaces/viz/heatmap.py +164 -0
- diffract/session/namespaces/viz/scatter.py +202 -0
- diffract/session/namespaces/viz/sparkline.py +270 -0
- diffract/session/namespaces/viz/violin.py +212 -0
- diffract/session/session.py +260 -0
- diffract/session/utils.py +81 -0
- diffract/viz/__init__.py +42 -0
- diffract/viz/data/__init__.py +34 -0
- diffract/viz/data/detection.py +57 -0
- diffract/viz/data/extraction.py +117 -0
- diffract/viz/data/filtering.py +57 -0
- diffract/viz/data/ordering.py +129 -0
- diffract/viz/data/provider.py +99 -0
- diffract/viz/data/types.py +98 -0
- diffract/viz/plots/__init__.py +37 -0
- diffract/viz/plots/base/__init__.py +20 -0
- diffract/viz/plots/base/axis.py +219 -0
- diffract/viz/plots/base/coloraxis.py +133 -0
- diffract/viz/plots/base/configurator.py +18 -0
- diffract/viz/plots/base/jitter.py +368 -0
- diffract/viz/plots/base/line.py +197 -0
- diffract/viz/plots/base/marker.py +278 -0
- diffract/viz/plots/base/overlay.py +14 -0
- diffract/viz/plots/base/plot.py +110 -0
- diffract/viz/plots/base/update.py +50 -0
- diffract/viz/plots/boxplot.py +283 -0
- diffract/viz/plots/cluster.py +374 -0
- diffract/viz/plots/heatmap.py +168 -0
- diffract/viz/plots/scatter.py +233 -0
- diffract/viz/plots/sparkline.py +405 -0
- diffract/viz/plots/subplots/__init__.py +32 -0
- diffract/viz/plots/subplots/coloraxis.py +339 -0
- diffract/viz/plots/subplots/factory.py +713 -0
- diffract/viz/plots/subplots/grid.py +214 -0
- diffract/viz/plots/subplots/layout.py +370 -0
- diffract/viz/plots/subplots/spec.py +39 -0
- diffract/viz/plots/violin.py +298 -0
- diffract/viz/renderer.py +513 -0
- diffract/viz/styling/__init__.py +78 -0
- diffract/viz/styling/palettes/__init__.py +20 -0
- diffract/viz/styling/palettes/bundle.py +16 -0
- diffract/viz/styling/palettes/color.py +83 -0
- diffract/viz/styling/palettes/dashes.py +27 -0
- diffract/viz/styling/palettes/symbols.py +42 -0
- diffract/viz/styling/resolvers/__init__.py +11 -0
- diffract/viz/styling/resolvers/categorical.py +68 -0
- diffract/viz/styling/resolvers/color.py +77 -0
- diffract/viz/styling/resolvers/numeric.py +108 -0
- diffract/viz/styling/sources.py +27 -0
- diffract/viz/styling/theme/__init__.py +29 -0
- diffract/viz/styling/theme/applier.py +114 -0
- diffract/viz/styling/theme/components.py +66 -0
- diffract/viz/styling/theme/presets.py +58 -0
- diffract/viz/styling/theme/theme.py +36 -0
- diffract_core-0.2.1.dist-info/METADATA +530 -0
- diffract_core-0.2.1.dist-info/RECORD +193 -0
- diffract_core-0.2.1.dist-info/WHEEL +4 -0
- diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
- 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, ...]] = ()
|