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,282 @@
1
+ """Model management namespace for Session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from dataclasses import replace
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from dependency_injector.wiring import Provide, inject
11
+
12
+ from diffract.core.constants import TABLE_AGGREGATES, TABLE_PARAMETERS
13
+ from diffract.core.data.nn.extractors.base import ExtractorOverrides, ParameterOverrides
14
+ from diffract.core.utils.exceptions import format_exception_message
15
+ from diffract.session.errors import (
16
+ ModelAlreadyExistsError,
17
+ ModelNotFoundError,
18
+ SessionError,
19
+ )
20
+ from diffract.session.session import Session, SessionContext
21
+
22
+ from .parameters import ParametersNamespace
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Iterable
26
+
27
+ from diffract.core.data.nn.aggregates.repository import AggregateRepository
28
+ from diffract.core.data.nn.extractors.handlers.base import ParameterHandler
29
+ from diffract.core.data.nn.extractors.interface import IParameterExtractor
30
+ from diffract.core.data.nn.params.interface import IParameterRepository
31
+
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ __all__ = [
36
+ "ModelsNamespace",
37
+ "ParametersNamespace",
38
+ ]
39
+
40
+
41
+ class ModelsNamespace:
42
+ """Model management API."""
43
+
44
+ @inject
45
+ def __init__(
46
+ self,
47
+ session_or_context: Session | SessionContext,
48
+ parameter_repository: IParameterRepository = Provide["nn.parameter_repository"],
49
+ aggregate_repository: AggregateRepository = Provide["nn.aggregate_repository"],
50
+ extractor_factory: Callable[..., IParameterExtractor] = Provide[
51
+ "nn.extractor_factory.provider"
52
+ ],
53
+ ) -> None:
54
+ self.__session_or_context = session_or_context
55
+ self.__param_repo = parameter_repository
56
+ self.__agg_repo = aggregate_repository
57
+ self.__extractor_factory = extractor_factory
58
+
59
+ self.parameters = ParametersNamespace(session_or_context)
60
+
61
+ def add(
62
+ self,
63
+ model: Any,
64
+ model_id: str | None = None,
65
+ parameter_overrides: dict[str, ParameterOverrides] | None = None,
66
+ custom_handlers: Iterable[ParameterHandler] | None = None,
67
+ ) -> None:
68
+ """Add a neural network model to the session.
69
+
70
+ Extracts parameters from the provided model and stores them for analysis.
71
+ Parameters are automatically persisted and can be retrieved in future sessions.
72
+
73
+ Args:
74
+ model: Neural network model object (PyTorch, TensorFlow, etc.)
75
+ or a dict mapping parameter names to NumPy arrays.
76
+ model_id: Unique identifier for the model. Auto-generated if None.
77
+ parameter_overrides: Custom parameter extraction overrides by name.
78
+ custom_handlers: Additional parameter handlers for custom parameter types.
79
+
80
+ Raises:
81
+ ModelAlreadyExistsError: If model_id already exists in the session.
82
+ SessionError: If parameter extraction fails or yields no parameters.
83
+ """
84
+ with self.__session_or_context:
85
+ try:
86
+ extractor = self.__extractor_factory(
87
+ model=model,
88
+ overrides=self.__build_extractor_overrides(
89
+ model_id, parameter_overrides
90
+ ),
91
+ custom_handlers=custom_handlers,
92
+ )
93
+
94
+ previous_len = len(self.__param_repo)
95
+ extractor.extract_parameters(self.__param_repo)
96
+ new_len = len(self.__param_repo)
97
+
98
+ self.__session_or_context._field_cache.invalidate()
99
+
100
+ except SessionError:
101
+ raise
102
+ except Exception as e:
103
+ msg = f"Failed to add model: {format_exception_message(e)}"
104
+ raise SessionError(msg) from e
105
+
106
+ if new_len == previous_len:
107
+ msg = (
108
+ f"Model '{model_id or type(model).__name__}' produced no "
109
+ "parameters. Unsupported parameter types are skipped during "
110
+ "extraction; supported inputs are PyTorch, TensorFlow, "
111
+ "Flax, and ONNX models (install the matching extra) and "
112
+ "dicts of 2D floating-point NumPy arrays."
113
+ )
114
+ raise SessionError(msg)
115
+
116
+ logger.info(
117
+ "Added model '%s' with %d parameters",
118
+ model_id or "<auto-generated>",
119
+ new_len - previous_len,
120
+ )
121
+
122
+ def rename(
123
+ self,
124
+ model_id: str,
125
+ new_model_id: str,
126
+ ) -> None:
127
+ """Rename a model in the session.
128
+
129
+ Args:
130
+ model_id: ID of the model to rename.
131
+ new_model_id: New ID for the model.
132
+
133
+ Raises:
134
+ ModelNotFoundError: If model_id does not exist.
135
+ ModelAlreadyExistsError: If new_model_id already exists.
136
+ """
137
+ with self.__session_or_context:
138
+ params = self.__param_repo.create_view()
139
+ model_params = params.filter_by_model_id(model_id)
140
+
141
+ if not model_params:
142
+ msg = f"Model '{model_id}' not found"
143
+ raise ModelNotFoundError(msg)
144
+
145
+ new_model_params = params.filter_by_model_id(new_model_id)
146
+ if new_model_params:
147
+ msg = f"Model '{new_model_id}' already exists"
148
+ raise ModelAlreadyExistsError(msg)
149
+
150
+ param_uids = model_params.list_uids()
151
+
152
+ aggs = self.__agg_repo.create_view()
153
+ model_aggs = aggs.filter_by_context_models(model_id)
154
+ agg_uids = model_aggs.list_uids()
155
+
156
+ with self.__param_repo:
157
+ for uid in param_uids:
158
+ param = params[uid]
159
+ old_meta = param.meta
160
+ new_meta = replace(old_meta, model_id=new_model_id)
161
+
162
+ object.__setattr__(param, "meta", new_meta)
163
+
164
+ meta_dict = new_meta.to_dict()
165
+ meta_dict.pop("uid")
166
+ self.__param_repo.metadata_index.upsert(
167
+ TABLE_PARAMETERS,
168
+ uid=uid,
169
+ **meta_dict,
170
+ )
171
+
172
+ with self.__agg_repo:
173
+ for uid in agg_uids:
174
+ agg = aggs[uid]
175
+ old_meta = agg.meta
176
+ new_context_models = tuple(
177
+ new_model_id if cm == model_id else cm
178
+ for cm in old_meta.context_models
179
+ )
180
+ new_meta = replace(old_meta, context_models=new_context_models)
181
+
182
+ object.__setattr__(agg, "meta", new_meta)
183
+
184
+ meta_dict = new_meta.to_dict()
185
+ meta_dict.pop("uid")
186
+ self.__agg_repo.metadata_index.upsert(
187
+ TABLE_AGGREGATES,
188
+ uid=uid,
189
+ **meta_dict,
190
+ )
191
+
192
+ self.__session_or_context._field_cache.invalidate()
193
+
194
+ logger.info("Renamed model '%s' to '%s'", model_id, new_model_id)
195
+
196
+ def erase(
197
+ self,
198
+ *model_ids: str,
199
+ erase_all: bool = False,
200
+ ) -> None:
201
+ """Erase all parameters and results for specified models.
202
+
203
+ Removes model parameters from both memory and persistent storage.
204
+ Use with caution as this operation cannot be undone.
205
+
206
+ Args:
207
+ *model_ids: Model IDs to erase. Required if erase_all is False.
208
+ erase_all: If True, erases all models. model_ids must be empty.
209
+
210
+ Raises:
211
+ ValueError: If neither model_ids nor erase_all is specified,
212
+ or if both are specified.
213
+ ModelNotFoundError: If any specified model ID is not found.
214
+ """
215
+ with self.__session_or_context:
216
+ if (not model_ids) ^ erase_all:
217
+ if not model_ids:
218
+ msg = "No model_ids provided and erase_all=False"
219
+ else:
220
+ msg = "Cannot specify both model_ids and erase_all=True"
221
+ raise ValueError(msg)
222
+
223
+ if erase_all:
224
+ model_ids = tuple(self.list())
225
+
226
+ params = self.__param_repo.create_view()
227
+ aggs = self.__agg_repo.create_view()
228
+
229
+ erased_uids: list[str] = []
230
+ for model_id in model_ids:
231
+ model_params = params.filter_by_model_id(model_id)
232
+
233
+ if not model_params:
234
+ msg = f"Model '{model_id}' not found"
235
+ raise ModelNotFoundError(msg)
236
+
237
+ model_aggs = aggs.filter_by_context_models(model_id)
238
+
239
+ erased_uids.extend(model_params.list_uids())
240
+ model_params.clear(erase=True)
241
+ if model_aggs:
242
+ model_aggs.clear(erase=True)
243
+
244
+ logger.info("Erased model '%s'", model_id)
245
+
246
+ self.__session_or_context._field_cache.remove_uids(erased_uids)
247
+
248
+ def list(self) -> list[str]:
249
+ """List all model IDs currently in the session.
250
+
251
+ Returns:
252
+ Sorted list of unique model IDs present in the session.
253
+ """
254
+ with self.__session_or_context:
255
+ if isinstance(self.__session_or_context, Session):
256
+ params = self.__param_repo.create_view()
257
+ else:
258
+ params = self.__session_or_context._param_filter_context
259
+ model_ids = {param.meta.model_id for param in params}
260
+ return sorted(model_ids)
261
+
262
+ def __build_extractor_overrides(
263
+ self,
264
+ model_id: str | None,
265
+ parameter_overrides: dict[str, ParameterOverrides] | None,
266
+ ) -> ExtractorOverrides:
267
+ extractor_overrides_kwargs: dict[str, Any] = {}
268
+
269
+ if model_id:
270
+ params = self.__param_repo.create_view()
271
+ model_params = params.filter_by_model_id(model_id)
272
+
273
+ if model_params:
274
+ msg = f"Model with uid {model_id} already exists"
275
+ raise ModelAlreadyExistsError(msg)
276
+
277
+ extractor_overrides_kwargs["model_id"] = model_id
278
+
279
+ if parameter_overrides:
280
+ extractor_overrides_kwargs["parameter_overrides"] = parameter_overrides
281
+
282
+ return ExtractorOverrides(**extractor_overrides_kwargs)
@@ -0,0 +1,133 @@
1
+ """Parameter management namespace for Session models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from dependency_injector.wiring import Provide, inject
10
+
11
+ from diffract.core.parallel import ParallelContext
12
+ from diffract.session.errors import SessionError
13
+ from diffract.session.session import Session, SessionContext
14
+
15
+ from .meta_patcher import MetadataPatcher, MetadataPatchError
16
+
17
+ if TYPE_CHECKING:
18
+ from diffract.core.data.nn.params.interface import IParameterRepository
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ParametersNamespace:
24
+ """Parameter management API for Session models."""
25
+
26
+ @inject
27
+ def __init__(
28
+ self,
29
+ session_or_context: Session | SessionContext,
30
+ parameter_repository: IParameterRepository = Provide["nn.parameter_repository"],
31
+ parallel_context_factory: Callable[[], ParallelContext] = Provide[
32
+ "parallel_singleton.thread_pool_context.provider"
33
+ ],
34
+ ) -> None:
35
+ self.__session_or_context = session_or_context
36
+ self.__param_repo = parameter_repository
37
+ self.__parallel_context_factory = parallel_context_factory
38
+
39
+ def list(
40
+ self,
41
+ *,
42
+ verbose: bool = False,
43
+ ) -> list[dict[str, Any]]:
44
+ """List parameter metadata for inspection.
45
+
46
+ Provides detailed information about parameters currently stored
47
+ in the session, including computed fields and metadata.
48
+
49
+ Returns:
50
+ List of dictionaries containing parameter metadata including
51
+ UID, name, model ID, type, available fields, and custom metadata.
52
+ """
53
+ with self.__session_or_context:
54
+ if isinstance(self.__session_or_context, Session):
55
+ params = self.__param_repo.create_view()
56
+ else:
57
+ params = self.__session_or_context._param_filter_context
58
+
59
+ if not verbose:
60
+ return [
61
+ {
62
+ "uid": param.meta.uid,
63
+ "name": param.meta.name,
64
+ "model_id": param.meta.model_id,
65
+ "parameter_type": param.meta.ptype.name,
66
+ }
67
+ for param in params
68
+ ]
69
+
70
+ if self.__session_or_context._field_cache.is_valid:
71
+ field_cache = self.__session_or_context._field_cache.get()
72
+ else:
73
+ field_cache = params.list_fields_by_uid(
74
+ parallel=self.__parallel_context_factory()
75
+ )
76
+ self.__session_or_context._field_cache.set(field_cache)
77
+
78
+ return [
79
+ {
80
+ "uid": param.meta.uid,
81
+ "name": param.meta.name,
82
+ "model_id": param.meta.model_id,
83
+ "parameter_type": param.meta.ptype.name,
84
+ "available_fields": field_cache.get(param.meta.uid, []),
85
+ "other_meta": param.meta.other_meta,
86
+ }
87
+ for param in params
88
+ ]
89
+
90
+ def patch_meta(
91
+ self,
92
+ *,
93
+ updates: dict[str, Any],
94
+ force: bool = False,
95
+ ) -> None:
96
+ """Update custom metadata (`other_meta`) for selected parameters.
97
+
98
+ This method updates only the `other_meta` portion of parameter metadata.
99
+ Changes are persisted by rewriting the stored `__metadata__` field, so
100
+ subsequent sessions will see the updated metadata.
101
+
102
+ Args:
103
+ updates: Mapping of parameter UID to metadata updates. Each value
104
+ must be a dict of {key: value} pairs to apply to that
105
+ parameter's other_meta.
106
+ Example: {"uid1": {"dataset": "imagenet", "epoch": 10}}
107
+ force: If False (default), raise if any key already exists in
108
+ `other_meta`. If True, overwrite existing keys.
109
+
110
+ Raises:
111
+ SessionError: If conflicts are detected (force=False), invalid keys
112
+ are provided, or updates structure is invalid.
113
+ """
114
+ with self.__session_or_context:
115
+ if not updates:
116
+ return
117
+
118
+ for uid, value in updates.items():
119
+ if not isinstance(value, dict):
120
+ msg = (
121
+ f"Invalid update for uid '{uid}': expected dict[str, Any], "
122
+ f"got {type(value).__name__}"
123
+ )
124
+ raise SessionError(msg)
125
+
126
+ param_view = self.__param_repo.create_view()
127
+ pending = param_view[updates.keys()]
128
+ patcher = MetadataPatcher()
129
+
130
+ try:
131
+ patcher.patch(updates=updates, parameters=pending, force=force)
132
+ except MetadataPatchError as e:
133
+ raise SessionError(str(e)) from e
@@ -0,0 +1,167 @@
1
+ """Parameter management namespace for Session models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import replace
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from tqdm.auto import tqdm
10
+
11
+ from diffract.core.constants import (
12
+ PROGRESS_BAR_DELAY_SEC,
13
+ PROGRESS_BAR_MIN_ITEMS,
14
+ TABLE_PARAMETERS,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from diffract.core.data.nn.params.interface import IParameterView
19
+ from diffract.core.data.nn.params.proxy import ParameterDataProxy
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _ERROR_PREVIEW_LIMIT = 10
24
+
25
+
26
+ class MetadataPatchError(Exception):
27
+ """Error during metadata patching."""
28
+
29
+
30
+ class MetadataPatcher:
31
+ """Utility for patching parameter metadata.
32
+
33
+ Handles conflict detection and applies updates to the other_meta
34
+ portion of parameter metadata.
35
+
36
+ Example:
37
+ >>> patcher = MetadataPatcher()
38
+ >>> patcher.patch(
39
+ ... updates={"uid1": {"dataset": "imagenet", "epoch": 10}},
40
+ ... parameters=session._get_view(),
41
+ ... force=False,
42
+ ... )
43
+ """
44
+
45
+ def patch(
46
+ self,
47
+ *,
48
+ updates: dict[str, dict[str, Any]],
49
+ parameters: IParameterView,
50
+ force: bool = False,
51
+ ) -> None:
52
+ """Patch other_meta with conflict detection.
53
+
54
+ Args:
55
+ updates: Mapping of parameter uid -> {meta_key: value}.
56
+ parameters: Parameter view containing target parameters.
57
+ force: If True, overwrite existing metadata keys.
58
+
59
+ Raises:
60
+ MetadataPatchError: If unknown UIDs or conflicts are detected.
61
+ """
62
+ if not updates:
63
+ logger.debug("patch called with empty updates, skipping")
64
+ return
65
+
66
+ resolved, unknown_uids = self._resolve_parameters(updates, parameters)
67
+
68
+ if unknown_uids:
69
+ preview = ", ".join(sorted(unknown_uids)[:_ERROR_PREVIEW_LIMIT])
70
+ suffix = "..." if len(unknown_uids) > _ERROR_PREVIEW_LIMIT else ""
71
+ raise MetadataPatchError(
72
+ f"Unknown parameter UIDs (not found in session): {preview}{suffix}"
73
+ )
74
+
75
+ conflicts = self._validate_conflicts(updates, resolved, force)
76
+
77
+ if conflicts:
78
+ preview = ", ".join(
79
+ f"{uid}:{k}" for uid, k in conflicts[:_ERROR_PREVIEW_LIMIT]
80
+ )
81
+ suffix = "..." if len(conflicts) > _ERROR_PREVIEW_LIMIT else ""
82
+ raise MetadataPatchError(
83
+ f"Metadata conflicts (existing keys) for {len(conflicts)} entries: "
84
+ f"{preview}{suffix}"
85
+ )
86
+
87
+ self._apply_updates(updates, resolved, parameters)
88
+ logger.info("Patched metadata for %d parameters", len(updates))
89
+
90
+ def _resolve_parameters(
91
+ self,
92
+ updates: dict[str, dict[str, Any]],
93
+ parameters: IParameterView,
94
+ ) -> tuple[dict[str, ParameterDataProxy], list[str]]:
95
+ """Resolve parameter proxies from UIDs."""
96
+ resolved: dict[str, ParameterDataProxy] = {}
97
+ unknown_uids: list[str] = []
98
+
99
+ for uid in updates:
100
+ try:
101
+ resolved[uid] = parameters[uid]
102
+ except KeyError:
103
+ unknown_uids.append(uid)
104
+
105
+ return resolved, unknown_uids
106
+
107
+ def _validate_conflicts(
108
+ self,
109
+ updates: dict[str, dict[str, Any]],
110
+ resolved: dict[str, ParameterDataProxy],
111
+ force: bool,
112
+ ) -> list[tuple[str, str]]:
113
+ """Check for conflicting metadata keys."""
114
+ conflicts: list[tuple[str, str]] = []
115
+
116
+ for uid, meta_map in updates.items():
117
+ param = resolved[uid]
118
+ conflicts.extend(
119
+ (param.meta.uid, key)
120
+ for key in meta_map
121
+ if (not force) and (key in param.meta.other_meta)
122
+ )
123
+
124
+ return conflicts
125
+
126
+ def _apply_updates(
127
+ self,
128
+ updates: dict[str, dict[str, Any]],
129
+ resolved: dict[str, ParameterDataProxy],
130
+ parameters: IParameterView,
131
+ ) -> None:
132
+ """Apply metadata updates to parameters.
133
+
134
+ Updates other_meta in both the proxy and the metadata index.
135
+ """
136
+ total = len(updates)
137
+ repository = parameters._repository
138
+
139
+ with repository:
140
+ for uid, meta_map in tqdm(
141
+ updates.items(),
142
+ desc="Updating metadata...",
143
+ delay=PROGRESS_BAR_DELAY_SEC,
144
+ disable=total < PROGRESS_BAR_MIN_ITEMS,
145
+ total=total,
146
+ ):
147
+ param = resolved[uid]
148
+ old_meta = param.meta
149
+
150
+ # Build updated other_meta
151
+ new_other_meta = dict(old_meta.other_meta)
152
+ new_other_meta.update(meta_map)
153
+
154
+ # Create new metadata with updated other_meta
155
+ new_meta = replace(old_meta, other_meta=new_other_meta)
156
+
157
+ # Update the proxy's meta reference (frozen, so we use
158
+ # object.__setattr__)
159
+ object.__setattr__(param, "meta", new_meta)
160
+
161
+ new_meta_dict = new_meta.to_dict()
162
+ new_meta_dict.pop("uid")
163
+ repository.metadata_index.upsert(
164
+ TABLE_PARAMETERS,
165
+ uid=uid,
166
+ **new_meta_dict,
167
+ )