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,260 @@
1
+ """Main Session API for the diffract package.
2
+
3
+ This module implements the primary user interface for neural network parameter
4
+ analysis workflows, providing a high-level API for model management, kernel
5
+ execution, and result retrieval through dependency injection.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import types
12
+ from contextlib import nullcontext
13
+ from pathlib import Path
14
+ from typing import Self
15
+
16
+ from dependency_injector.wiring import Provide, inject
17
+
18
+ from diffract.containers import MainContainer, create_main_container
19
+ from diffract.core.data.nn.aggregates.repository import AggregateRepository
20
+ from diffract.core.data.nn.aggregates.view import AggregateView
21
+ from diffract.core.data.nn.params.interface import IParameterRepository, IParameterView
22
+ from diffract.core.data.nn.params.schema import ParameterType
23
+ from diffract.session.field_cache import SessionFieldCache
24
+ from diffract.session.utils import filter_aggregate_view, filter_parameter_view
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class Session:
30
+ """Main session class for neural network parameter analysis.
31
+
32
+ Provides a high-level interface for adding models, applying computational
33
+ kernels, and retrieving results. Manages the complete lifecycle of parameter
34
+ analysis workflows through dependency injection and modular components.
35
+
36
+ Example:
37
+ >>> session = Session()
38
+ >>> session.models.add(my_model, model_id="bert-base")
39
+ >>> session.compute.apply("frob_norm", "stable_rank")
40
+ >>> results = session.results.export_metrics(
41
+ ... "frob_norm", "stable_rank", export_format="pandas"
42
+ ... )
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ profile: str | None = "ram",
48
+ config_path: str | Path | None = None,
49
+ container: MainContainer | None = None,
50
+ other_session: Session | None = None,
51
+ ) -> None:
52
+ """Initialize a new session.
53
+
54
+ Args:
55
+ profile: Built-in profile name for quick setup:
56
+ - "ram": RAM storage, no persistence (fast experiments)
57
+ - "local": SQLite storage in .diffract/ (persistent, simple)
58
+ - "hybrid": SQLite + HDF5 (persistent, optimized for large arrays)
59
+ config_path: Path to configuration file (YAML/JSON/INI). Takes
60
+ priority over profile if both are provided.
61
+ container: Pre-configured MainContainer instance. If None,
62
+ creates new container using config_path or profile.
63
+ other_session: Another Session instance to copy configuration from.
64
+ """
65
+ from diffract.session.namespaces import (
66
+ ComputeNamespace,
67
+ ModelsNamespace,
68
+ ResultsNamespace,
69
+ UtilsNamespace,
70
+ VizNamespace,
71
+ )
72
+
73
+ # For namespaces
74
+ self._field_cache = SessionFieldCache()
75
+
76
+ if container is None:
77
+ self.__container = create_main_container(config_path, profile=profile)
78
+ else:
79
+ self.__container = container
80
+
81
+ self.__context_depth = 0
82
+
83
+ with self:
84
+ logger.debug("Loading parameters...")
85
+ self.__init_repos()
86
+ logger.info(
87
+ "Session initialized with %d existing parameters, %d aggregates",
88
+ len(self.__param_repo),
89
+ len(self.__agg_repo),
90
+ )
91
+
92
+ self.models = ModelsNamespace(self)
93
+ self.compute = ComputeNamespace(self)
94
+ self.results = ResultsNamespace(self)
95
+ self.viz = VizNamespace(self)
96
+ self.utils = UtilsNamespace(self)
97
+
98
+ if other_session is not None:
99
+ self.utils.merge_other_session(other_session, verify=False)
100
+
101
+ def __enter__(self) -> Self | nullcontext[Self]:
102
+ """Enter session context and initialize resources."""
103
+ self.__context_depth += 1
104
+ if self.__context_depth == 1:
105
+ logger.debug("Init resources called")
106
+ self.__container.init_resources()
107
+ return self
108
+ return nullcontext()
109
+
110
+ def __exit__(
111
+ self,
112
+ exc_type: type[BaseException] | None,
113
+ exc_val: BaseException | None,
114
+ exc_tb: types.TracebackType | None,
115
+ ) -> None:
116
+ """Exit session context and shutdown resources."""
117
+ self.__context_depth -= 1
118
+ if self.__context_depth == 0:
119
+ logger.debug("Shutdown resources called")
120
+ self.__container.shutdown_resources()
121
+
122
+ @inject
123
+ def __init_repos(
124
+ self,
125
+ parameter_repository: IParameterRepository = Provide["nn.parameter_repository"],
126
+ aggregate_repository: AggregateRepository = Provide["nn.aggregate_repository"],
127
+ ) -> None:
128
+ """Inject dependencies from the container."""
129
+ self.__param_repo = parameter_repository
130
+ self.__agg_repo = aggregate_repository
131
+
132
+ def filter(
133
+ self,
134
+ param_ids: list[str] | None = None,
135
+ param_names: list[str] | None = None,
136
+ param_types: list[ParameterType] | None = None,
137
+ model_ids: list[str] | None = None,
138
+ ) -> SessionContext:
139
+ """Create a filtered view of the session's parameters and aggregates.
140
+
141
+ Args:
142
+ param_ids: Parameter IDs to keep, or None for no ID filtering.
143
+ param_names: Parameter names to keep, or None for no name filtering.
144
+ param_types: Parameter types to keep, or None for no type filtering.
145
+ model_ids: Model IDs to keep, or None for no model filtering.
146
+
147
+ Returns:
148
+ A SessionContext scoped to the filtered parameter and aggregate views.
149
+ """
150
+ with self:
151
+ param_view = self.__param_repo.create_view()
152
+ filtered_param_view = filter_parameter_view(
153
+ param_view,
154
+ parameter_ids=param_ids,
155
+ parameter_names=param_names,
156
+ parameter_types=param_types,
157
+ model_ids=model_ids,
158
+ )
159
+
160
+ agg_view = self.__agg_repo.create_view()
161
+ filtered_agg_view = filter_aggregate_view(
162
+ agg_view,
163
+ parameter_names=param_names,
164
+ model_ids=model_ids,
165
+ )
166
+
167
+ return SessionContext(self, filtered_param_view, filtered_agg_view)
168
+
169
+
170
+ class SessionContext:
171
+ """Filtered, scoped view over a session's parameters and aggregates.
172
+
173
+ Wraps a parent Session together with pre-filtered parameter and aggregate
174
+ views, exposing the same namespaces (models, compute, results, viz, utils)
175
+ restricted to the filtered scope. Supports further chained filtering.
176
+ """
177
+
178
+ def __init__(
179
+ self,
180
+ session: Session,
181
+ param_filter_context: IParameterView,
182
+ agg_filter_context: AggregateView,
183
+ ) -> None:
184
+ """Initialize a filtered session context.
185
+
186
+ Args:
187
+ session: The parent Session this context is derived from.
188
+ param_filter_context: Pre-filtered parameter view for this scope.
189
+ agg_filter_context: Pre-filtered aggregate view for this scope.
190
+ """
191
+ from diffract.session.namespaces import (
192
+ ComputeNamespace,
193
+ ModelsNamespace,
194
+ ResultsNamespace,
195
+ UtilsNamespace,
196
+ VizNamespace,
197
+ )
198
+
199
+ self.__session = session
200
+
201
+ # For namespaces
202
+ self._param_filter_context = param_filter_context
203
+ self._agg_filter_context = agg_filter_context
204
+ self._field_cache = session._field_cache
205
+
206
+ self.models = ModelsNamespace(self)
207
+ self.compute = ComputeNamespace(self)
208
+ self.results = ResultsNamespace(self)
209
+ self.viz = VizNamespace(self)
210
+ self.utils = UtilsNamespace(self)
211
+
212
+ def __enter__(self) -> Self | nullcontext[Self]:
213
+ """Enter the underlying session context."""
214
+ return self.__session.__enter__()
215
+
216
+ def __exit__(
217
+ self,
218
+ exc_type: type[BaseException] | None,
219
+ exc_val: BaseException | None,
220
+ exc_tb: types.TracebackType | None,
221
+ ) -> None:
222
+ """Exit the underlying session context."""
223
+ self.__session.__exit__(exc_type, exc_val, exc_tb)
224
+
225
+ def filter(
226
+ self,
227
+ param_ids: list[str] | None = None,
228
+ param_names: list[str] | None = None,
229
+ param_types: list[ParameterType] | None = None,
230
+ model_ids: list[str] | None = None,
231
+ ) -> Self:
232
+ """Further filter this context's parameters and aggregates.
233
+
234
+ Args:
235
+ param_ids: Parameter IDs to keep, or None for no ID filtering.
236
+ param_names: Parameter names to keep, or None for no name filtering.
237
+ param_types: Parameter types to keep, or None for no type filtering.
238
+ model_ids: Model IDs to keep, or None for no model filtering.
239
+
240
+ Returns:
241
+ A new SessionContext scoped to the further-filtered views.
242
+ """
243
+ with self:
244
+ filtered_param_view = filter_parameter_view(
245
+ self._param_filter_context,
246
+ parameter_ids=param_ids,
247
+ parameter_names=param_names,
248
+ parameter_types=param_types,
249
+ model_ids=model_ids,
250
+ )
251
+
252
+ filtered_agg_view = filter_aggregate_view(
253
+ self._agg_filter_context,
254
+ parameter_names=param_names,
255
+ model_ids=model_ids,
256
+ )
257
+
258
+ return SessionContext(
259
+ self.__session, filtered_param_view, filtered_agg_view
260
+ )
@@ -0,0 +1,81 @@
1
+ from diffract.core.data.nn.aggregates.view import AggregateView
2
+ from diffract.core.data.nn.params import ParameterType
3
+ from diffract.core.data.nn.params.interface import IParameterView
4
+
5
+
6
+ def filter_parameter_view(
7
+ view: IParameterView,
8
+ *,
9
+ parameter_ids: list[str] | None = None,
10
+ parameter_names: list[str] | None = None,
11
+ parameter_types: list[ParameterType] | None = None,
12
+ model_ids: list[str] | None = None,
13
+ ) -> IParameterView:
14
+ """Apply the given filters to a parameter view.
15
+
16
+ Each filter is only applied when a non-empty value is provided. Scalar
17
+ values are wrapped into single-element lists before filtering.
18
+
19
+ Args:
20
+ view: The parameter view to filter.
21
+ parameter_ids: Parameter IDs to select, or None to skip ID filtering.
22
+ parameter_names: Parameter names to filter by, or None to skip.
23
+ parameter_types: Parameter types to filter by, or None to skip.
24
+ model_ids: Model IDs to filter by, or None to skip.
25
+
26
+ Returns:
27
+ The filtered parameter view.
28
+ """
29
+ if parameter_names:
30
+ if isinstance(parameter_names, str):
31
+ parameter_names = [parameter_names]
32
+ view = view.filter_by_name(*parameter_names)
33
+
34
+ if parameter_types:
35
+ if isinstance(parameter_types, (str, ParameterType)):
36
+ parameter_types = [parameter_types]
37
+ view = view.filter_by_ptype(*parameter_types)
38
+
39
+ if model_ids:
40
+ if isinstance(model_ids, str):
41
+ model_ids = [model_ids]
42
+ view = view.filter_by_model_id(*model_ids)
43
+
44
+ if parameter_ids:
45
+ if isinstance(parameter_ids, (str, int)):
46
+ parameter_ids = [parameter_ids]
47
+ view = view[parameter_ids]
48
+
49
+ return view
50
+
51
+
52
+ def filter_aggregate_view(
53
+ view: AggregateView,
54
+ *,
55
+ parameter_names: list[str] | None = None,
56
+ model_ids: list[str] | None = None,
57
+ ) -> AggregateView:
58
+ """Apply the given filters to an aggregate view.
59
+
60
+ Each filter is only applied when a non-empty value is provided. Scalar
61
+ values are wrapped into single-element lists before filtering.
62
+
63
+ Args:
64
+ view: The aggregate view to filter.
65
+ parameter_names: Context parameter names to filter by, or None to skip.
66
+ model_ids: Context model IDs to filter by, or None to skip.
67
+
68
+ Returns:
69
+ The filtered aggregate view.
70
+ """
71
+ if parameter_names:
72
+ if isinstance(parameter_names, str):
73
+ parameter_names = [parameter_names]
74
+ view = view.filter_by_context_params(*parameter_names)
75
+
76
+ if model_ids:
77
+ if isinstance(model_ids, str):
78
+ model_ids = [model_ids]
79
+ view = view.filter_by_context_models(*model_ids)
80
+
81
+ return view
@@ -0,0 +1,42 @@
1
+ """Refactored visualization stack for diffract.
2
+
3
+ Submodules that need plotting backends are imported lazily, so the package
4
+ and its data helpers stay importable without the ``viz`` extra installed.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ __all__ = [
10
+ "DARK_THEME",
11
+ "DEFAULT_THEME",
12
+ "MINIMAL_THEME",
13
+ "Plot",
14
+ "Theme",
15
+ "apply_theme",
16
+ "load_theme",
17
+ "plots",
18
+ "render",
19
+ "render_from_config",
20
+ ]
21
+
22
+ _RENDERER_EXPORTS = frozenset({"Plot", "load_theme", "render", "render_from_config"})
23
+ _STYLING_EXPORTS = frozenset(
24
+ {"DARK_THEME", "DEFAULT_THEME", "MINIMAL_THEME", "Theme", "apply_theme"}
25
+ )
26
+
27
+
28
+ def __getattr__(name: str) -> Any:
29
+ if name in _RENDERER_EXPORTS:
30
+ from . import renderer
31
+
32
+ return getattr(renderer, name)
33
+ if name in _STYLING_EXPORTS:
34
+ from . import styling
35
+
36
+ return getattr(styling, name)
37
+ if name == "plots":
38
+ from . import plots
39
+
40
+ return plots
41
+ msg = f"module {__name__!r} has no attribute {name!r}"
42
+ raise AttributeError(msg)
@@ -0,0 +1,34 @@
1
+ from .detection import detect_data_shape, detect_data_type, detect_field_meta
2
+ from .extraction import (
3
+ get_field_data,
4
+ get_field_value,
5
+ get_field_values,
6
+ )
7
+ from .filtering import apply_filter
8
+ from .ordering import Ordering, OrderMode, as_is, by_key, custom, lexicographic, numeric
9
+ from .provider import DataProvider
10
+ from .types import DataShape, DataType, Entry, EntryContext, FieldMeta, FieldRef
11
+
12
+ __all__ = [
13
+ "DataProvider",
14
+ "DataShape",
15
+ "DataType",
16
+ "Entry",
17
+ "EntryContext",
18
+ "FieldMeta",
19
+ "FieldRef",
20
+ "OrderMode",
21
+ "Ordering",
22
+ "apply_filter",
23
+ "as_is",
24
+ "by_key",
25
+ "custom",
26
+ "detect_data_shape",
27
+ "detect_data_type",
28
+ "detect_field_meta",
29
+ "get_field_data",
30
+ "get_field_value",
31
+ "get_field_values",
32
+ "lexicographic",
33
+ "numeric",
34
+ ]
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+
8
+ from .types import DataShape, DataType, FieldMeta
9
+
10
+ RE_NUMBER_PATTERN = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
11
+
12
+
13
+ def is_number_regex(string: str) -> bool:
14
+ """Return True if the string fully matches a numeric pattern."""
15
+ return RE_NUMBER_PATTERN.fullmatch(string) is not None
16
+
17
+
18
+ def detect_field_meta(values: list[Any]) -> FieldMeta:
19
+ """Detect DataType and DataShape from values."""
20
+ data_type = detect_data_type(values)
21
+ data_shape = detect_data_shape(values)
22
+ return FieldMeta(data_type, data_shape)
23
+
24
+
25
+ def detect_data_type(values: list[Any]) -> DataType:
26
+ """Detect if values are numeric or categorical."""
27
+ for v in values:
28
+ if v is None:
29
+ continue
30
+ if isinstance(v, bool):
31
+ return DataType.CATEGORICAL
32
+ if isinstance(v, str):
33
+ if is_number_regex(v):
34
+ continue
35
+ return DataType.CATEGORICAL
36
+ if isinstance(v, (int, float, np.integer, np.floating)):
37
+ continue
38
+ if isinstance(v, np.ndarray):
39
+ if not np.issubdtype(v.dtype, np.number):
40
+ return DataType.CATEGORICAL
41
+ continue
42
+ return DataType.CATEGORICAL
43
+
44
+ return DataType.NUMERIC
45
+
46
+
47
+ def detect_data_shape(values: list[Any]) -> DataShape:
48
+ """Detect if field represents scalar or vector per entry."""
49
+ for v in values:
50
+ if v is None:
51
+ continue
52
+ if isinstance(v, np.ndarray) and v.size > 1:
53
+ return DataShape.VECTOR
54
+ if isinstance(v, (list, tuple)) and len(v) > 1:
55
+ return DataShape.VECTOR
56
+
57
+ return DataShape.SCALAR
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from diffract.core.constants import CONTEXT_SEPARATOR, parse_contextual_field_name
7
+
8
+ from .detection import detect_field_meta
9
+ from .types import DataShape, DataType, Entry, EntryContext
10
+
11
+
12
+ def get_field_value(ctx: EntryContext, field: str) -> Any:
13
+ """Return the value of a field, resolving contextual field variants.
14
+
15
+ Args:
16
+ ctx: Entry context to look the field up in.
17
+ field: Field name to resolve.
18
+
19
+ Returns:
20
+ The direct field value if present, otherwise the best-matching
21
+ contextual field value.
22
+
23
+ Raises:
24
+ ValueError: If no matching field is found in the context.
25
+ """
26
+ value = ctx.fields.get(field)
27
+ if value is not None:
28
+ return value
29
+
30
+ agg_pattern = re.compile(rf"^{re.escape(field)}{re.escape(CONTEXT_SEPARATOR)}.+$")
31
+
32
+ candidates: list[tuple[str, Any]] = []
33
+ for key_, value_ in ctx.fields.items():
34
+ if agg_pattern.match(key_):
35
+ candidates.append((key_, value_))
36
+
37
+ if not candidates:
38
+ raise ValueError(f"Field {field} not found in entry context")
39
+
40
+ return _select_best_contextual_field(candidates, ctx=ctx)
41
+
42
+
43
+ def get_field_values(entries: dict[str, Entry], field: str) -> list[Any]:
44
+ """Return the field value for each entry.
45
+
46
+ Args:
47
+ entries: Mapping of uid to entry.
48
+ field: Field name to extract.
49
+
50
+ Returns:
51
+ List of field values, one per entry.
52
+ """
53
+ values: list[Any] = []
54
+
55
+ for entry in entries.values():
56
+ value = get_field_value(EntryContext.from_entry(entry), field)
57
+ values.append(value)
58
+
59
+ return values
60
+
61
+
62
+ def get_field_data(
63
+ entries: dict[str, Entry], field: str
64
+ ) -> tuple[list[Any], DataType, DataShape]:
65
+ """Return field values along with their detected type and shape.
66
+
67
+ Args:
68
+ entries: Mapping of uid to entry.
69
+ field: Field name to extract.
70
+
71
+ Returns:
72
+ Tuple of (values, data type, data shape).
73
+ """
74
+ values = get_field_values(entries, field)
75
+ meta = detect_field_meta(values)
76
+ return values, meta.data_type, meta.data_shape
77
+
78
+
79
+ def _select_best_contextual_field(
80
+ candidates: list[tuple[str, Any]],
81
+ *,
82
+ ctx: EntryContext,
83
+ ) -> Any:
84
+ """Select best contextual field from multiple candidates.
85
+
86
+ Priority:
87
+ 1. Fields whose context includes current parameter
88
+ 2. Among those, prefer smaller context size (more specific)
89
+ 3. Fallback: deterministic sort by field name
90
+ """
91
+ scored: list[tuple[int, int, str, Any]] = []
92
+
93
+ for field_name, value in candidates:
94
+ _, models, params = parse_contextual_field_name(field_name)
95
+
96
+ model_match = models is None or (
97
+ ctx.model_id is not None and ctx.model_id in models
98
+ )
99
+ param_match = params is None or (
100
+ ctx.parameter_name is not None and ctx.parameter_name in params
101
+ )
102
+
103
+ context_size = (len(models) if models else 0) + (len(params) if params else 0)
104
+
105
+ scored.append(
106
+ (
107
+ 1 if model_match else 0,
108
+ 1 if param_match else 0,
109
+ -context_size,
110
+ field_name,
111
+ value,
112
+ )
113
+ )
114
+
115
+ scored.sort(key=lambda x: (-x[0], -x[1], -x[2], x[3]))
116
+
117
+ return scored[0][4]
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+
7
+ from .extraction import get_field_value
8
+ from .types import Entry, EntryContext
9
+
10
+
11
+ def apply_filter(
12
+ entries: dict[str, Entry],
13
+ value_filter: dict[str, tuple[str, Any]],
14
+ ) -> dict[str, Entry]:
15
+ """Apply value-based filtering to entries."""
16
+ return {
17
+ uid: entry
18
+ for uid, entry in entries.items()
19
+ if _entry_passes_filter(entry, value_filter)
20
+ }
21
+
22
+
23
+ def _entry_passes_filter(
24
+ entry: Entry,
25
+ value_filter: dict[str, tuple[str, Any]],
26
+ ) -> bool:
27
+ """Check if an entry passes all filter conditions."""
28
+ ctx = EntryContext.from_entry(entry)
29
+ for field_name, (op, threshold) in value_filter.items():
30
+ value = get_field_value(ctx, field_name)
31
+ if not _check_condition(value, op, threshold):
32
+ return False
33
+ return True
34
+
35
+
36
+ def _check_condition(value: Any, op: str, threshold: Any) -> bool:
37
+ """Check a single filter condition."""
38
+ if value is None:
39
+ return False
40
+
41
+ if isinstance(value, np.ndarray):
42
+ value = float(np.nanmean(value))
43
+
44
+ ops = {
45
+ ">": lambda a, b: a > b,
46
+ "<": lambda a, b: a < b,
47
+ ">=": lambda a, b: a >= b,
48
+ "<=": lambda a, b: a <= b,
49
+ "==": lambda a, b: a == b,
50
+ "!=": lambda a, b: a != b,
51
+ }
52
+
53
+ op_func = ops.get(op)
54
+ if op_func is None:
55
+ raise ValueError(f"Unknown operator: {op}")
56
+
57
+ return op_func(value, threshold)