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,378 @@
1
+ """Results namespace for Session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from dependency_injector.wiring import Provide, inject
11
+
12
+ from diffract.core.data.nn.params.interface import IParameterRepository
13
+ from diffract.core.export.formatters.registry import get_formatter
14
+ from diffract.core.parallel import ParallelContext
15
+ from diffract.session.errors import KernelNotFoundError, SessionError
16
+ from diffract.session.namespaces.results.eraser import ResultsEraser, ResultsEraserError
17
+ from diffract.session.namespaces.results.injester import (
18
+ AggregateIngester,
19
+ AggregateIngestionError,
20
+ FieldIngester,
21
+ FieldIngestionError,
22
+ )
23
+ from diffract.session.session import Session, SessionContext
24
+
25
+ if TYPE_CHECKING:
26
+ from diffract.core.compute.registry import KernelRegistry
27
+ from diffract.core.data.nn.aggregates.repository import AggregateRepository
28
+ from diffract.core.data.nn.aggregates.view import AggregateView
29
+ from diffract.core.data.nn.params.interface import IParameterView
30
+ from diffract.core.export.exporters import ResultExporter
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class ResultsNamespace:
36
+ """Results export and ingestion API for Session."""
37
+
38
+ @inject
39
+ def __init__(
40
+ self,
41
+ session_or_context: Session | SessionContext,
42
+ parameter_repository: IParameterRepository = Provide["nn.parameter_repository"],
43
+ aggregate_repository: AggregateRepository = Provide["nn.aggregate_repository"],
44
+ kernel_registry: KernelRegistry = Provide["compute.kernel_registry"],
45
+ parallel_context_factory: Callable[[], ParallelContext] = Provide[
46
+ "parallel_singleton.thread_pool_context.provider"
47
+ ],
48
+ results_exporter_factory: Callable[
49
+ [tuple[str, ...], IParameterView, str], Any
50
+ ] = Provide["export.result_exporter.provider"],
51
+ ) -> None:
52
+ self.__session_or_context = session_or_context
53
+ self.__param_repo = parameter_repository
54
+ self.__agg_repo = aggregate_repository
55
+ self.__kernel_registry = kernel_registry
56
+ self.__parallel_context_factory = parallel_context_factory
57
+ self.__results_exporter_factory = results_exporter_factory
58
+
59
+ def export_metrics(
60
+ self,
61
+ *fields: str,
62
+ export_format: str = "dict",
63
+ ) -> Any:
64
+ """Retrieve computation results for specified fields (metrics only).
65
+
66
+ Args:
67
+ *fields: Field names to retrieve.
68
+ export_format: Output format - "dict", "json", "pandas", "polars", "list".
69
+
70
+ Returns:
71
+ Metric results in the specified format.
72
+ """
73
+ with self.__session_or_context:
74
+ if not fields:
75
+ raise ValueError("At least one field must be specified")
76
+
77
+ if isinstance(self.__session_or_context, Session):
78
+ params = self.__param_repo.create_view()
79
+ else:
80
+ params = self.__session_or_context._param_filter_context
81
+
82
+ exporter: ResultExporter = self.__results_exporter_factory()
83
+ param_results = exporter._collect_parameter_results(
84
+ *fields,
85
+ parameters=params,
86
+ parallel=self.__parallel_context_factory(),
87
+ field_cache=self.__session_or_context._field_cache,
88
+ )
89
+
90
+ if export_format == "json":
91
+ return self._dump_json(param_results)
92
+
93
+ formatter = get_formatter(export_format)
94
+ formatted = formatter.format_results(param_results, [], fields)
95
+ return formatted.scalars
96
+
97
+ def export_aggregates(
98
+ self,
99
+ *fields: str,
100
+ export_format: str = "dict",
101
+ ) -> Any:
102
+ """Retrieve aggregation results for specified fields (aggregates only).
103
+
104
+ Args:
105
+ *fields: Field names to retrieve.
106
+ export_format: Output format - "dict", "json", "pandas", "polars", "list".
107
+
108
+ Returns:
109
+ Aggregation results in the specified format.
110
+ """
111
+ with self.__session_or_context:
112
+ if not fields:
113
+ raise ValueError("At least one field must be specified")
114
+
115
+ if isinstance(self.__session_or_context, Session):
116
+ aggs = self.__agg_repo.create_view()
117
+ else:
118
+ aggs = self.__session_or_context._agg_filter_context
119
+
120
+ exporter: ResultExporter = self.__results_exporter_factory()
121
+ aggregate_results = exporter._collect_aggregate_results(
122
+ *fields,
123
+ aggregates=aggs,
124
+ parallel=self.__parallel_context_factory(),
125
+ )
126
+
127
+ if export_format == "json":
128
+ return self._dump_json(aggregate_results)
129
+
130
+ formatter = get_formatter(export_format)
131
+ formatted = formatter.format_results({}, aggregate_results, fields)
132
+ return formatted.aggregates
133
+
134
+ def ingest_metrics(
135
+ self,
136
+ fields_by_uid: dict[str, dict[str, Any]],
137
+ *,
138
+ force: bool = False,
139
+ ) -> None:
140
+ """Ingest precomputed fields into the session via a uid->field mapping.
141
+
142
+ Field names are stored exactly as provided (including contextual suffixes
143
+ like "metric@ctx"). By default, this method is strict and raises if any
144
+ target field already exists; set force=True to overwrite.
145
+
146
+ Args:
147
+ fields_by_uid: Mapping of parameter uid -> {field_name: value}.
148
+ force: If False (default), raise on any existing field conflict.
149
+ If True, overwrite existing field values.
150
+
151
+ Raises:
152
+ SessionError: If unknown uids are provided or conflicts are
153
+ detected (force=False).
154
+ """
155
+ with self.__session_or_context:
156
+ if not fields_by_uid:
157
+ return
158
+
159
+ params = self.__param_repo.create_view()
160
+ filtered_params = params[fields_by_uid.keys()]
161
+
162
+ ingester = FieldIngester()
163
+
164
+ try:
165
+ ingester.ingest(
166
+ fields_by_uid=fields_by_uid,
167
+ parameters=filtered_params,
168
+ force=force,
169
+ )
170
+ except FieldIngestionError as e:
171
+ raise SessionError(str(e)) from e
172
+
173
+ all_fields: set[str] = set()
174
+ for field_dict in fields_by_uid.values():
175
+ all_fields.update(field_dict.keys())
176
+
177
+ self.__session_or_context._field_cache.add_computed_fields(
178
+ affected_uids=fields_by_uid.keys(),
179
+ new_fields=all_fields,
180
+ )
181
+
182
+ def ingest_aggregates(
183
+ self,
184
+ aggregates: list[dict[str, Any]],
185
+ *,
186
+ force: bool = False,
187
+ ) -> None:
188
+ """Ingest precomputed aggregate values into the session.
189
+
190
+ Each aggregate is identified by (field_name, context_models, context_params).
191
+ Useful for importing precomputed aggregation results.
192
+
193
+ Args:
194
+ aggregates: List of aggregate dictionaries, each containing:
195
+ - field_name: Base field name (e.g., "l_overlap").
196
+ - context_models: Tuple/list of model IDs.
197
+ - context_params: Tuple/list of parameter names (optional).
198
+ - value: The computed value to store.
199
+ force: If False (default), raise on existing aggregate conflict.
200
+ If True, overwrite existing values.
201
+
202
+ Raises:
203
+ SessionError: If aggregate structure is invalid or conflicts are
204
+ detected (force=False).
205
+ """
206
+ with self.__session_or_context:
207
+ if not aggregates:
208
+ return
209
+
210
+ ingester = AggregateIngester()
211
+
212
+ try:
213
+ ingester.ingest(
214
+ aggregates=aggregates,
215
+ repository=self.__agg_repo,
216
+ force=force,
217
+ )
218
+ except AggregateIngestionError as e:
219
+ raise SessionError(str(e)) from e
220
+
221
+ def erase(
222
+ self,
223
+ *fields_to_erase: str,
224
+ erase_dependent_also: bool = False,
225
+ erase_all: bool = False,
226
+ ) -> None:
227
+ """Erase computation results for specified fields.
228
+
229
+ Removes computed field data from parameters while preserving the
230
+ parameters themselves. Optionally erases dependent fields that
231
+ rely on the specified fields as inputs.
232
+
233
+ Args:
234
+ *fields_to_erase: Field names to erase. Required if erase_all is False.
235
+ erase_dependent_also: If True, also erases fields that depend on
236
+ the specified fields.
237
+ erase_all: If True, erases all computed fields. fields_to_erase
238
+ must be empty.
239
+
240
+ Raises:
241
+ ValueError: If neither fields_to_erase nor erase_all is specified,
242
+ or if both are specified.
243
+ KernelNotFoundError: If any specified field cannot be produced
244
+ by registered kernels.
245
+ """
246
+ with self.__session_or_context:
247
+ if (not fields_to_erase) ^ erase_all:
248
+ if not fields_to_erase:
249
+ msg = "No fields_to_erase provided and erase_all=False"
250
+ else:
251
+ msg = "Cannot specify both fields_to_erase and erase_all=True"
252
+ raise ValueError(msg)
253
+
254
+ eraser = ResultsEraser(kernel_registry=self.__kernel_registry)
255
+
256
+ try:
257
+ if erase_all:
258
+ fields = set(self.__kernel_registry.list_fields_can_produce())
259
+ else:
260
+ fields = eraser.resolve_fields_to_erase(
261
+ fields_to_erase, erase_dependent_also
262
+ )
263
+ except ResultsEraserError as e:
264
+ raise KernelNotFoundError(str(e)) from e
265
+
266
+ if isinstance(self.__session_or_context, Session):
267
+ params = self.__param_repo.create_view()
268
+ aggs = self.__agg_repo.create_view()
269
+ else:
270
+ params = self.__session_or_context._param_filter_context
271
+ aggs = self.__session_or_context._agg_filter_context
272
+
273
+ eraser.erase(parameters=params, aggregates=aggs, fields=fields)
274
+
275
+ self.__session_or_context._field_cache.remove_fields_by_uids(
276
+ params.list_uids(), fields
277
+ )
278
+
279
+ def export(
280
+ self,
281
+ *fields: str,
282
+ sources: str = "all",
283
+ export_format: str = "dict",
284
+ expand_contextual: bool = True,
285
+ ) -> Any:
286
+ """Unified export of metrics and aggregates.
287
+
288
+ This method provides a single interface for exporting both parameter
289
+ metrics and aggregate values. When both sources are requested,
290
+ aggregate values are merged into parameter entries as contextual fields.
291
+
292
+ Args:
293
+ *fields: Field names to retrieve.
294
+ sources: Data sources to include - "metrics", "aggregates", or "all".
295
+ export_format: Output format - "dict", "json", "pandas", "polars", "list".
296
+ expand_contextual: When True and sources="all", merge aggregate values
297
+ into parameter entries as contextual field names.
298
+
299
+ Returns:
300
+ Results in the specified format.
301
+ """
302
+ if sources not in ("metrics", "aggregates", "all"):
303
+ raise ValueError(
304
+ f"Invalid sources: {sources}. Use 'metrics', 'aggregates', or 'all'."
305
+ )
306
+
307
+ if sources == "metrics":
308
+ return self.export_metrics(*fields, export_format=export_format)
309
+
310
+ if sources == "aggregates":
311
+ return self.export_aggregates(*fields, export_format=export_format)
312
+
313
+ metrics = self.export_metrics(*fields, export_format="dict")
314
+ aggregates = self.export_aggregates(*fields, export_format="dict")
315
+
316
+ if expand_contextual and aggregates:
317
+ self._merge_aggregates_into_metrics(metrics, aggregates)
318
+
319
+ if export_format == "dict":
320
+ return metrics
321
+
322
+ if export_format == "json":
323
+ return self._dump_json(metrics)
324
+
325
+ formatter = get_formatter(export_format)
326
+ formatted = formatter.format_results(metrics, [], fields)
327
+ return formatted.scalars
328
+
329
+ def _merge_aggregates_into_metrics(
330
+ self,
331
+ metrics: dict[str, Any],
332
+ aggregates: list[dict[str, Any]],
333
+ ) -> None:
334
+ """Merge aggregate values into parameter entries as contextual fields."""
335
+ from diffract.core.constants import format_contextual_field_name
336
+
337
+ for agg in aggregates:
338
+ self._merge_single_aggregate(metrics, agg, format_contextual_field_name)
339
+
340
+ @staticmethod
341
+ def _merge_single_aggregate(
342
+ metrics: dict[str, Any],
343
+ agg: dict[str, Any],
344
+ format_fn: Any,
345
+ ) -> None:
346
+ """Merge a single aggregate into matching parameter entries."""
347
+ context_params = agg.get("context_params", ())
348
+ context_models = agg.get("context_models", ())
349
+
350
+ value = agg.get("value")
351
+ if not context_params or value is None:
352
+ return
353
+
354
+ contextual_name = format_fn(
355
+ agg.get("field"),
356
+ models=context_models,
357
+ params=context_params,
358
+ )
359
+
360
+ for uid, entry in metrics.items():
361
+ meta = entry.get("metadata", {})
362
+ param_name = meta.get("name")
363
+ model_id = meta.get("model_id")
364
+
365
+ if not param_name or not model_id:
366
+ continue
367
+
368
+ if param_name in context_params and model_id in context_models:
369
+ metrics[uid].setdefault("fields", {})[contextual_name] = value
370
+
371
+ @staticmethod
372
+ def _dump_json(data: Any) -> str:
373
+ def _json_serializer(obj: object) -> object:
374
+ if hasattr(obj, "tolist"):
375
+ return obj.tolist()
376
+ return str(obj)
377
+
378
+ return json.dumps(data, indent=2, default=_json_serializer)
@@ -0,0 +1,129 @@
1
+ """Results namespace for Session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from collections.abc import Iterable
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from diffract.core.compute.registry import KernelRegistry
12
+ from diffract.core.data.nn.aggregates.view import AggregateView
13
+ from diffract.core.data.nn.params.interface import IParameterView
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _ERROR_PREVIEW_LIMIT = 10
18
+
19
+
20
+ class ResultsEraserError(Exception):
21
+ """Error during results erasure."""
22
+
23
+
24
+ class ResultsEraser:
25
+ """Utility for erasing computed results with dependency resolution.
26
+
27
+ Handles kernel dependency resolution to determine the full set of
28
+ fields to erase.
29
+
30
+ Example:
31
+ >>> eraser = ResultsEraser(kernel_registry=registry)
32
+ >>> fields = eraser.resolve_fields_to_erase(
33
+ ... ["frob_norm"], erase_dependent_also=True
34
+ ... )
35
+ >>> eraser.erase(view=session._get_view(), fields=fields)
36
+ """
37
+
38
+ def __init__(self, kernel_registry: KernelRegistry) -> None:
39
+ """Initialize the eraser.
40
+
41
+ Args:
42
+ kernel_registry: Registry for kernel dependency resolution.
43
+ """
44
+ self._registry = kernel_registry
45
+
46
+ def resolve_fields_to_erase(
47
+ self,
48
+ fields_to_erase: Iterable[str],
49
+ erase_dependent_also: bool,
50
+ ) -> set[str]:
51
+ """Resolve kernel dependencies to get full set of fields to erase.
52
+
53
+ Args:
54
+ fields_to_erase: Initial fields to erase.
55
+ erase_dependent_also: If True, include fields that depend
56
+ on the specified fields.
57
+
58
+ Returns:
59
+ Complete set of fields to erase.
60
+
61
+ Raises:
62
+ ResultsEraserError: If a field cannot be produced by any kernel.
63
+ """
64
+ result: list[str] = []
65
+
66
+ for field_name in fields_to_erase:
67
+ if not self._registry.can_produce_field(field_name):
68
+ msg = f"Registry cannot produce '{field_name}'"
69
+ raise ResultsEraserError(msg)
70
+
71
+ result.append(field_name)
72
+
73
+ if erase_dependent_also:
74
+ result.extend(self._find_dependent_fields(field_name))
75
+
76
+ return set(result)
77
+
78
+ def _find_dependent_fields(self, field_name: str) -> list[str]:
79
+ """Find all fields that depend on the given field."""
80
+ dependent_fields: list[str] = []
81
+
82
+ producer = self._registry.get_kernel_producing_field(field_name)
83
+ all_kernels = self._registry.list_kernels()
84
+
85
+ for other_kernel in all_kernels:
86
+ if other_kernel == producer:
87
+ continue
88
+
89
+ dependencies = self._registry.resolve_dependencies(other_kernel)
90
+ fields_dependencies: list[str] = []
91
+
92
+ for dependency in dependencies:
93
+ fields_dependencies.extend(
94
+ self._registry.get_fields_kernel_require(dependency)
95
+ )
96
+
97
+ if field_name in fields_dependencies:
98
+ dependent_fields.extend(
99
+ self._registry.get_fields_kernel_produce(other_kernel)
100
+ )
101
+
102
+ return dependent_fields
103
+
104
+ def erase(
105
+ self,
106
+ *,
107
+ parameters: IParameterView,
108
+ aggregates: AggregateView | None = None,
109
+ fields: set[str],
110
+ ) -> None:
111
+ """Erase fields from parameters and aggregates.
112
+
113
+ Args:
114
+ parameters: Parameter view to erase scalar fields from.
115
+ aggregates: Aggregate view to erase aggregate entries from.
116
+ fields: Set of base field names to erase.
117
+ """
118
+ logger.debug("Erasing fields: %s", ", ".join(sorted(fields)))
119
+
120
+ # Erase scalar fields from parameters
121
+ re_patterns = [rf"{re.escape(f)}" for f in fields]
122
+ parameters.erase_fields_with_regexp(*re_patterns)
123
+
124
+ # Erase aggregate entries by field_name
125
+ if aggregates is not None:
126
+ aggregates_to_erase = aggregates.filter_by_field_name(*fields)
127
+ if aggregates_to_erase:
128
+ logger.debug("Erasing %d aggregate entries", len(aggregates_to_erase))
129
+ aggregates_to_erase.clear(erase=True)