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,389 @@
1
+ """Generic data view for batch operations on filtered collections.
2
+
3
+ This module provides the base DataView class that can be specialized
4
+ for different data types (parameters, relations, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import re
11
+ import types
12
+ from collections.abc import Iterator, Sequence
13
+ from typing import TYPE_CHECKING, Self, TypeVar
14
+
15
+ from tqdm.auto import tqdm
16
+
17
+ from diffract.core.constants import (
18
+ PROGRESS_BAR_DELAY_SEC,
19
+ PROGRESS_BAR_MIN_ITEMS,
20
+ )
21
+ from diffract.core.parallel import ParallelContext, map_maybe_parallel
22
+ from diffract.core.utils.exceptions import format_exception_message
23
+
24
+ from .interface import EntityIndex, EntityUID, FieldName
25
+
26
+ if TYPE_CHECKING:
27
+ from .interface import IDataRepository
28
+
29
+ TMetadata = TypeVar("TMetadata")
30
+ TProxy = TypeVar("TProxy")
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ _ERROR_PREVIEW_LIMIT = 10
35
+
36
+
37
+ class DataView[TMetadata, TProxy]:
38
+ """Generic view over a subset of entities owned by a repository.
39
+
40
+ Provides numpy-like view semantics with support for filtering,
41
+ iteration, prefetching, and batch operations. Subclasses can add
42
+ domain-specific filtering methods.
43
+
44
+ Type Parameters:
45
+ TMetadata: The metadata type for entities in this view.
46
+ TProxy: The proxy type for entities in this view.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ *,
52
+ repository: IDataRepository[TMetadata, TProxy],
53
+ uids: list[EntityUID] | None = None,
54
+ ) -> None:
55
+ self._repository = repository
56
+ self._uids: list[EntityUID] | None = list(uids) if uids is not None else None
57
+ self._sorted: bool = False
58
+
59
+ def _ensure_uids(self) -> list[EntityUID]:
60
+ """Ensure UIDs are loaded (lazy load from repository if None)."""
61
+ if self._uids is None:
62
+ self._uids = self._repository.list_uids()
63
+ self._sorted = False
64
+ return self._uids
65
+
66
+ def _sort_in_place(self) -> None:
67
+ """Sort UIDs for deterministic ordering."""
68
+ uids = self._ensure_uids()
69
+ uids.sort()
70
+ self._sorted = True
71
+
72
+ def __len__(self) -> int:
73
+ """Return number of entities in the view."""
74
+ return len(self._ensure_uids())
75
+
76
+ def __iter__(self) -> Iterator[TProxy]:
77
+ """Iterate entity proxies in view order."""
78
+ if not self._sorted:
79
+ self._sort_in_place()
80
+
81
+ for uid in self._ensure_uids():
82
+ yield self._repository.get_proxy(uid)
83
+
84
+ def __getitem__(
85
+ self,
86
+ index: EntityIndex
87
+ | slice
88
+ | EntityUID
89
+ | Sequence[EntityIndex]
90
+ | Sequence[EntityUID],
91
+ ) -> TProxy | Self:
92
+ """Index into the view by position(s) or uid(s)."""
93
+ if not self._sorted:
94
+ self._sort_in_place()
95
+
96
+ uids = self._ensure_uids()
97
+ match index:
98
+ case int():
99
+ return self._repository.get_proxy(uids[index])
100
+ case slice():
101
+ return self.__class__(
102
+ repository=self._repository,
103
+ uids=uids[index],
104
+ )
105
+ case str():
106
+ return self._repository.get_proxy(index)
107
+ case _:
108
+ if isinstance(index, Sequence) and index:
109
+ first = index[0]
110
+ if isinstance(first, int):
111
+ result_uids = [uids[i] for i in index]
112
+ elif isinstance(first, str):
113
+ result_uids = list(index)
114
+ else:
115
+ msg = f"Invalid index type: {type(first)}"
116
+ raise ValueError(msg)
117
+ return self.__class__(repository=self._repository, uids=result_uids)
118
+
119
+ return self.__class__(repository=self._repository, uids=[])
120
+
121
+ def __enter__(self) -> Self:
122
+ """Enter a batch write context for this view's repository storage."""
123
+ self._repository.storage_manager.__enter__()
124
+ return self
125
+
126
+ def __exit__(
127
+ self,
128
+ exc_type: type[BaseException] | None,
129
+ exc_val: BaseException | None,
130
+ exc_tb: types.TracebackType | None,
131
+ ) -> None:
132
+ """Exit the batch write context for this view's repository storage."""
133
+ self._repository.storage_manager.__exit__(exc_type, exc_val, exc_tb)
134
+
135
+ def list_uids(self) -> list[EntityUID]:
136
+ """Return the list of entity UIDs contained in this view."""
137
+ if not self._sorted:
138
+ self._sort_in_place()
139
+ return list(self._ensure_uids())
140
+
141
+ def list_fields_by_uid(
142
+ self,
143
+ *,
144
+ parallel: ParallelContext | None = None,
145
+ ) -> dict[EntityUID, list[FieldName]]:
146
+ """Return mapping uid -> available fields for entities in this view."""
147
+ if not self._sorted:
148
+ self._sort_in_place()
149
+
150
+ uids = self._ensure_uids()
151
+ if not uids:
152
+ return {}
153
+
154
+ def _safe_list_fields(
155
+ uid: EntityUID,
156
+ ) -> tuple[EntityUID, list[FieldName], str | None]:
157
+ try:
158
+ return uid, self._repository.get_proxy(uid).list_fields(), None
159
+ except Exception as e: # noqa: BLE001
160
+ return uid, [], format_exception_message(e)
161
+
162
+ dict_items = map_maybe_parallel(
163
+ uids,
164
+ _safe_list_fields,
165
+ parallel=parallel,
166
+ )
167
+
168
+ total = len(uids)
169
+ field_map: dict[EntityUID, list[FieldName]] = {}
170
+ failures: list[tuple[EntityUID, str]] = []
171
+ for uid, fields, error_type in tqdm(
172
+ dict_items,
173
+ desc="Listing fields...",
174
+ delay=PROGRESS_BAR_DELAY_SEC,
175
+ total=total,
176
+ disable=total < PROGRESS_BAR_MIN_ITEMS,
177
+ ):
178
+ field_map[uid] = fields
179
+ if error_type is not None:
180
+ failures.append((uid, error_type))
181
+
182
+ if failures:
183
+ preview = ", ".join(
184
+ f"{uid}({err})" for uid, err in failures[:_ERROR_PREVIEW_LIMIT]
185
+ )
186
+ suffix = "..." if len(failures) > _ERROR_PREVIEW_LIMIT else ""
187
+ logger.warning(
188
+ "Failed to list fields for %d entities: %s%s",
189
+ len(failures),
190
+ preview,
191
+ suffix,
192
+ )
193
+
194
+ return field_map
195
+
196
+ def prefetch_fields(
197
+ self,
198
+ *,
199
+ fields_by_uid: dict[EntityUID, list[FieldName]] | None = None,
200
+ fields: list[FieldName] | None = None,
201
+ verify_prefetch: bool = False,
202
+ parallel: ParallelContext | None = None,
203
+ ) -> bool:
204
+ """Prefetch specified fields for entities in this view into cache."""
205
+ if fields_by_uid is not None and fields is not None:
206
+ msg = "fields_by_uid and fields are mutually exclusive."
207
+ raise ValueError(msg)
208
+
209
+ if fields is None and fields_by_uid is None:
210
+ return True
211
+
212
+ if fields is not None and not fields:
213
+ return True
214
+
215
+ if fields_by_uid is not None and not fields_by_uid:
216
+ return True
217
+
218
+ if not self._sorted:
219
+ self._sort_in_place()
220
+
221
+ if fields is not None:
222
+ fields_by_uid = dict.fromkeys(self._ensure_uids(), fields)
223
+
224
+ if fields_by_uid is None:
225
+ return True
226
+
227
+ def _prefetch_one(
228
+ uid: EntityUID,
229
+ ) -> tuple[EntityUID, bool, str | None]:
230
+ uid_fields = fields_by_uid.get(uid) if fields_by_uid else []
231
+ if not uid_fields:
232
+ return uid, True, None
233
+ try:
234
+ proxy = self._repository.get_proxy(uid)
235
+ for field_name in uid_fields:
236
+ if not proxy.try_prefetch_field(field_name):
237
+ return uid, False, f"Missing field: {field_name}"
238
+ except Exception as e: # noqa: BLE001
239
+ return uid, False, format_exception_message(e)
240
+ else:
241
+ return uid, True, None
242
+
243
+ results = map_maybe_parallel(
244
+ list(fields_by_uid.keys()),
245
+ _prefetch_one,
246
+ parallel=parallel,
247
+ )
248
+
249
+ total = len(fields_by_uid)
250
+ all_success = True
251
+ failures: list[tuple[EntityUID, str]] = []
252
+
253
+ for uid, success, error_msg in tqdm(
254
+ results,
255
+ desc="Prefetching fields...",
256
+ delay=PROGRESS_BAR_DELAY_SEC,
257
+ total=total,
258
+ disable=total < PROGRESS_BAR_MIN_ITEMS,
259
+ ):
260
+ if not success:
261
+ all_success = False
262
+ if error_msg:
263
+ failures.append((uid, error_msg))
264
+
265
+ if failures:
266
+ preview = ", ".join(
267
+ f"{uid}({err})" for uid, err in failures[:_ERROR_PREVIEW_LIMIT]
268
+ )
269
+ suffix = "..." if len(failures) > _ERROR_PREVIEW_LIMIT else ""
270
+ logger.warning(
271
+ "Failed to prefetch fields for %d entities: %s%s",
272
+ len(failures),
273
+ preview,
274
+ suffix,
275
+ )
276
+
277
+ if verify_prefetch and all_success:
278
+ for uid in fields_by_uid:
279
+ proxy = self._repository.get_proxy(uid)
280
+ for field_name in fields_by_uid[uid]:
281
+ if not proxy.is_field_prefetched(field_name):
282
+ return False
283
+
284
+ return all_success
285
+
286
+ def erase_fields(self, *fields: FieldName) -> None:
287
+ """Remove specified fields from entities in this view."""
288
+ if not fields:
289
+ return
290
+
291
+ if not self._sorted:
292
+ self._sort_in_place()
293
+
294
+ uids = self._ensure_uids()
295
+ total = len(uids)
296
+ for uid in tqdm(
297
+ uids,
298
+ desc="Erasing fields...",
299
+ delay=PROGRESS_BAR_DELAY_SEC,
300
+ total=total,
301
+ disable=total < PROGRESS_BAR_MIN_ITEMS,
302
+ ):
303
+ proxy = self._repository.get_proxy(uid)
304
+ for field_name in fields:
305
+ if proxy.has_field(field_name):
306
+ proxy.erase_field(field_name)
307
+
308
+ def erase_fields_with_regexp(self, *patterns: str) -> None:
309
+ """Remove fields matching regex patterns from entities in this view."""
310
+ if not patterns:
311
+ return
312
+
313
+ if not self._sorted:
314
+ self._sort_in_place()
315
+
316
+ compiled = [re.compile(p) for p in patterns]
317
+
318
+ uids = self._ensure_uids()
319
+ total = len(uids)
320
+ for uid in tqdm(
321
+ uids,
322
+ desc="Erasing fields by pattern...",
323
+ delay=PROGRESS_BAR_DELAY_SEC,
324
+ total=total,
325
+ disable=total < PROGRESS_BAR_MIN_ITEMS,
326
+ ):
327
+ proxy = self._repository.get_proxy(uid)
328
+ for field_name in proxy.list_fields():
329
+ if any(p.fullmatch(field_name) for p in compiled):
330
+ proxy.erase_field(field_name)
331
+
332
+ def filter_by_fields(
333
+ self,
334
+ *fields: FieldName,
335
+ inverse_mask: bool = False,
336
+ parallel: ParallelContext | None = None,
337
+ ) -> Self:
338
+ """Filter entities by the presence of one or more fields."""
339
+ if not self._sorted:
340
+ self._sort_in_place()
341
+
342
+ uids = self._ensure_uids()
343
+ need_fields = tuple(fields)
344
+ if not need_fields:
345
+ return self.__class__(
346
+ repository=self._repository,
347
+ uids=uids if not inverse_mask else [],
348
+ )
349
+
350
+ def _filter(uid: EntityUID) -> EntityUID | None:
351
+ proxy = self._repository.get_proxy(uid)
352
+ for field in need_fields:
353
+ if not proxy.has_field(field) ^ inverse_mask:
354
+ return None
355
+ return uid
356
+
357
+ filtered_uids = map_maybe_parallel(uids, _filter, parallel=parallel)
358
+
359
+ total = len(uids)
360
+ out = [
361
+ uid
362
+ for uid in tqdm(
363
+ filtered_uids,
364
+ desc="Filtering by fields...",
365
+ delay=PROGRESS_BAR_DELAY_SEC,
366
+ total=total,
367
+ disable=total < PROGRESS_BAR_MIN_ITEMS,
368
+ )
369
+ if uid is not None
370
+ ]
371
+
372
+ return self.__class__(repository=self._repository, uids=out)
373
+
374
+ def clear(self, erase: bool = False) -> None:
375
+ """Clear the view.
376
+
377
+ Args:
378
+ erase: If True, also erase data from storage and cache.
379
+ """
380
+ if erase:
381
+ for uid in self._ensure_uids():
382
+ proxy = self._repository.get_proxy(uid)
383
+ for field_name in proxy.list_fields():
384
+ proxy.erase_field(field_name)
385
+ if self._uids is not None:
386
+ self._uids.clear()
387
+ else:
388
+ self._uids = []
389
+ self._sorted = True
@@ -0,0 +1,27 @@
1
+ """Export and formatting utilities for computation results.
2
+
3
+ This package provides a unified interface to export results from parameter
4
+ collections into multiple output formats using pluggable formatters.
5
+
6
+ Features:
7
+ - Single entry point via ResultExporter
8
+ - Protocols for formatters and exporters
9
+ - DI container wiring for default formatter selection
10
+
11
+ Example:
12
+ >>> from diffract.core.export import ResultExporter
13
+ >>> exporter = ResultExporter()
14
+ >>> out = exporter.export_results(
15
+ ... "weights", parameters=collection, formatter=formatter
16
+ ... )
17
+ """
18
+
19
+ from .exporters import ResultExporter
20
+ from .interface import IResultExporter, IResultFormatter, StructuredExportResult
21
+
22
+ __all__ = [
23
+ "IResultExporter",
24
+ "IResultFormatter",
25
+ "ResultExporter",
26
+ "StructuredExportResult",
27
+ ]
@@ -0,0 +1,47 @@
1
+ """Dependency injection container for exporting results.
2
+
3
+ Provides providers for selecting a formatter and exporting results using
4
+ ResultExporter. The default export format is read from config.default_export_format.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+ from dependency_injector import containers, providers
13
+
14
+ from .exporters import ResultExporter
15
+ from .formatters.registry import get_formatter
16
+
17
+
18
+ def _export_with_formatter(
19
+ *args: Any,
20
+ exporter: ResultExporter,
21
+ export_format: str,
22
+ formatter_factory: Callable[[str], Any],
23
+ **kwargs: Any,
24
+ ) -> Any:
25
+ """Export results using dynamically selected formatter."""
26
+ return exporter.export_results(
27
+ *args,
28
+ formatter=formatter_factory(export_format),
29
+ **kwargs,
30
+ )
31
+
32
+
33
+ class ExportContainer(containers.DeclarativeContainer):
34
+ """Container for export-related dependencies."""
35
+
36
+ config = providers.Configuration()
37
+
38
+ result_formatter = providers.Factory(get_formatter)
39
+
40
+ result_exporter = providers.Singleton(ResultExporter)
41
+
42
+ export_results = providers.Factory(
43
+ _export_with_formatter,
44
+ exporter=result_exporter,
45
+ export_format=config.default_export_format,
46
+ formatter_factory=result_formatter.provider,
47
+ )
@@ -0,0 +1,191 @@
1
+ """Exporter implementation for collecting and formatting results.
2
+
3
+ The ResultExporter gathers requested fields from both parameter and aggregate
4
+ repositories, then delegates formatting to a provided IResultFormatter implementation.
5
+
6
+ Behavior:
7
+ - Requires at least one field name to export.
8
+ - Best-effort prefetch; continues if prefetch fails.
9
+ - Returns formatter-specific output with separate scalars and aggregates.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from diffract.core.parallel import ParallelContext
18
+ from diffract.core.utils.exceptions import format_exception_message
19
+
20
+ from .interface import AggregateData, IResultExporter, IResultFormatter, ResultData
21
+
22
+ if TYPE_CHECKING:
23
+ from diffract.core.data.nn.aggregates.view import AggregateView
24
+ from diffract.core.data.nn.params.interface import IParameterView
25
+ from diffract.session.field_cache import SessionFieldCache
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class SessionExportError(Exception):
31
+ """Base exception for session export errors."""
32
+
33
+
34
+ class ResultExporter(IResultExporter):
35
+ """Main implementation of result export functionality."""
36
+
37
+ def export_results(
38
+ self,
39
+ *fields: tuple[str, ...],
40
+ parameters: IParameterView,
41
+ aggregates: AggregateView | None = None,
42
+ formatter: IResultFormatter,
43
+ parallel: ParallelContext | None = None,
44
+ field_cache: SessionFieldCache | None = None,
45
+ ) -> Any:
46
+ """Export requested fields from parameter and aggregate repositories.
47
+
48
+ Args:
49
+ *fields: Field names to export.
50
+ parameters: Parameter collection to read scalar values from.
51
+ aggregates: Aggregate view to read contextual/aggregation values from.
52
+ formatter: Formatter used to convert results to target format.
53
+ parallel: Optional per-method parallel context for collection operations.
54
+ field_cache: Optional session-level cache for field availability.
55
+ If provided and valid, skips expensive list_fields_by_uid() call.
56
+
57
+ Returns:
58
+ Formatter-specific output object (typically StructuredExportResult).
59
+
60
+ Raises:
61
+ ValueError: If no fields are specified.
62
+ """
63
+ if not fields:
64
+ msg = "At least one field must be specified"
65
+ raise ValueError(msg)
66
+
67
+ param_results = self._collect_parameter_results(
68
+ *fields, parameters=parameters, parallel=parallel, field_cache=field_cache
69
+ )
70
+ aggregate_results = self._collect_aggregate_results(
71
+ *fields, aggregates=aggregates, parallel=parallel
72
+ )
73
+
74
+ return formatter.format_results(param_results, aggregate_results, fields)
75
+
76
+ def _collect_parameter_results(
77
+ self,
78
+ *fields: tuple[str, ...],
79
+ parameters: IParameterView,
80
+ parallel: ParallelContext | None,
81
+ field_cache: SessionFieldCache | None = None,
82
+ ) -> ResultData:
83
+ """Collect results from parameter repository.
84
+
85
+ Args:
86
+ *fields: Field names to collect (exact match only, no regex patterns).
87
+ parameters: Parameter collection to iterate over.
88
+ parallel: Optional per-method parallel context for collection operations.
89
+ field_cache: Optional session-level cache for field availability.
90
+ If provided and valid, skips expensive list_fields_by_uid() call.
91
+
92
+ Returns:
93
+ Nested mapping with metadata and field values per parameter.
94
+ """
95
+ results: ResultData = {}
96
+ fields_set = set(fields)
97
+
98
+ if field_cache is not None and field_cache.is_valid:
99
+ cached_fields_by_uid = field_cache.get()
100
+ required_fields = set(parameters.list_uids())
101
+ to_receive = tuple(set(required_fields) - set(cached_fields_by_uid.keys()))
102
+ received = parameters[to_receive].list_fields_by_uid(parallel=parallel)
103
+ field_cache.update(received)
104
+ cached_fields_by_uid = field_cache.get()
105
+ fields_by_uid = {uid: cached_fields_by_uid[uid] for uid in required_fields}
106
+ else:
107
+ fields_by_uid = parameters.list_fields_by_uid(parallel=parallel)
108
+ if field_cache is not None:
109
+ field_cache.set(fields_by_uid)
110
+
111
+ to_receive: dict[str, list[str]] = {}
112
+ for uid, fields_available in fields_by_uid.items():
113
+ to_receive[uid] = [f for f in fields_available if f in fields_set]
114
+
115
+ try:
116
+ parameters.prefetch_fields(fields_by_uid=to_receive, parallel=parallel)
117
+ except Exception as e: # noqa: BLE001
118
+ logger.debug(
119
+ "Prefetch failed during export: %s",
120
+ format_exception_message(e),
121
+ exc_info=True,
122
+ )
123
+
124
+ for param in parameters:
125
+ param_results: dict[str, Any] = {}
126
+ for field in to_receive[param.meta.uid]:
127
+ param_results[field] = param.get_field(field, auto_prefetch=False)
128
+
129
+ results[param.meta.uid] = {
130
+ "metadata": {
131
+ "name": param.meta.name,
132
+ "model_id": param.meta.model_id,
133
+ "parameter_type": param.meta.ptype.name,
134
+ **param.meta.other_meta,
135
+ },
136
+ "fields": param_results,
137
+ }
138
+
139
+ return results
140
+
141
+ def _collect_aggregate_results(
142
+ self,
143
+ *fields: tuple[str, ...],
144
+ aggregates: AggregateView | None,
145
+ parallel: ParallelContext | None,
146
+ ) -> AggregateData:
147
+ """Collect results from aggregate repository.
148
+
149
+ Args:
150
+ *fields: Base field names to collect.
151
+ aggregates: Aggregate view to iterate over.
152
+ parallel: Optional per-method parallel context (unused currently).
153
+
154
+ Returns:
155
+ List of aggregate records with field name, context, and value.
156
+ """
157
+ if aggregates is None or not aggregates:
158
+ return []
159
+
160
+ fields_set = set(fields)
161
+ results: AggregateData = []
162
+
163
+ # Filter aggregates by requested field names
164
+ filtered_view = aggregates.filter_by_field_name(*fields)
165
+
166
+ # Prefetch values
167
+ try:
168
+ filtered_view.prefetch_fields(fields=["value"], parallel=parallel)
169
+ except Exception as e: # noqa: BLE001
170
+ logger.debug(
171
+ "Prefetch failed during aggregate export: %s",
172
+ format_exception_message(e),
173
+ exc_info=True,
174
+ )
175
+
176
+ for aggregate in filtered_view:
177
+ if aggregate.meta.field_name not in fields_set:
178
+ continue
179
+ if not aggregate.has_field("value"):
180
+ continue
181
+
182
+ results.append(
183
+ {
184
+ "field": aggregate.meta.field_name,
185
+ "context_models": aggregate.meta.context_models,
186
+ "context_params": aggregate.meta.context_params,
187
+ "value": aggregate.get_field("value"),
188
+ }
189
+ )
190
+
191
+ return results
@@ -0,0 +1,23 @@
1
+ """Formatters for exporting results to different output formats.
2
+
3
+ Includes JSON, dictionary, and optional pandas/polars DataFrame formatters along
4
+ with a small registry/factory for runtime selection by name.
5
+ """
6
+
7
+ from .dict_formatter import DictFormatter
8
+ from .json_formatter import JsonFormatter
9
+ from .list_formatter import ListFormatter
10
+ from .pandas_formatter import PandasFormatter # type: ignore[F401]
11
+ from .polars_formatter import PolarsFormatter # type: ignore[F401]
12
+ from .registry import FORMATTERS, get_formatter, register_formatter
13
+
14
+ __all__ = [
15
+ "FORMATTERS",
16
+ "DictFormatter",
17
+ "JsonFormatter",
18
+ "ListFormatter",
19
+ "PandasFormatter",
20
+ "PolarsFormatter",
21
+ "get_formatter",
22
+ "register_formatter",
23
+ ]