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,249 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from diffract.core.data.nn.aggregates.repository import AggregateRepository
8
+ from diffract.core.data.nn.params.interface import IParameterView
9
+ from diffract.core.data.nn.params.proxy import ParameterDataProxy
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _ERROR_PREVIEW_LIMIT = 10
14
+
15
+
16
+ class FieldIngestionError(Exception):
17
+ """Error during field ingestion."""
18
+
19
+
20
+ class FieldIngester:
21
+ """Utility for ingesting precomputed fields into parameters.
22
+
23
+ Validates and writes precomputed field values into parameters.
24
+
25
+ Example:
26
+ >>> ingester = FieldIngester()
27
+ >>> ingester.ingest(
28
+ ... fields_by_uid={"param_uid": {"metric": 0.95, "loss": 0.05}},
29
+ ... parameters=session._get_view(),
30
+ ... force=False,
31
+ ... )
32
+ """
33
+
34
+ def ingest(
35
+ self,
36
+ *,
37
+ fields_by_uid: dict[str, dict[str, Any]],
38
+ parameters: IParameterView,
39
+ force: bool = False,
40
+ ) -> None:
41
+ """Ingest precomputed fields with validation.
42
+
43
+ Args:
44
+ fields_by_uid: Mapping of parameter uid -> {field_name: value}.
45
+ parameters: Parameter view containing target parameters.
46
+ force: If True, overwrite existing field values.
47
+
48
+ Raises:
49
+ FieldIngestionError: If unknown uids or conflicts are detected.
50
+ """
51
+ if not fields_by_uid:
52
+ logger.debug("ingest called with empty input, skipping")
53
+ return
54
+
55
+ resolved, unknown_uids = self._resolve_parameters(fields_by_uid, parameters)
56
+
57
+ if unknown_uids:
58
+ preview = ", ".join(sorted(unknown_uids)[:_ERROR_PREVIEW_LIMIT])
59
+ suffix = "..." if len(unknown_uids) > _ERROR_PREVIEW_LIMIT else ""
60
+ raise FieldIngestionError(
61
+ f"Unknown parameter UIDs (not found in session): {preview}{suffix}"
62
+ )
63
+
64
+ conflicts = self._validate_fields(fields_by_uid, resolved, force)
65
+
66
+ if conflicts:
67
+ preview = ", ".join(
68
+ f"{uid}:{f}" for uid, f in conflicts[:_ERROR_PREVIEW_LIMIT]
69
+ )
70
+ suffix = "..." if len(conflicts) > _ERROR_PREVIEW_LIMIT else ""
71
+ raise FieldIngestionError(
72
+ f"Field conflicts (already exist) for {len(conflicts)} entries: "
73
+ f"{preview}{suffix}"
74
+ )
75
+
76
+ self._write_fields(fields_by_uid, resolved, parameters)
77
+
78
+ total_fields = sum(len(fm) for fm in fields_by_uid.values())
79
+ logger.info(
80
+ "Ingested %d fields for %d parameters", total_fields, len(fields_by_uid)
81
+ )
82
+
83
+ def _resolve_parameters(
84
+ self,
85
+ fields_by_uid: dict[str, dict[str, Any]],
86
+ parameters: IParameterView,
87
+ ) -> tuple[dict[str, ParameterDataProxy], list[str]]:
88
+ """Resolve parameter proxies from UIDs."""
89
+ resolved: dict[str, ParameterDataProxy] = {}
90
+ unknown_uids: list[str] = []
91
+
92
+ for uid in fields_by_uid:
93
+ try:
94
+ resolved[uid] = parameters[uid]
95
+ except KeyError:
96
+ unknown_uids.append(uid)
97
+
98
+ return resolved, unknown_uids
99
+
100
+ def _validate_fields(
101
+ self,
102
+ fields_by_uid: dict[str, dict[str, Any]],
103
+ resolved: dict[str, ParameterDataProxy],
104
+ force: bool,
105
+ ) -> list[tuple[str, str]]:
106
+ """Validate fields for conflict issues."""
107
+ conflicts: list[tuple[str, str]] = []
108
+
109
+ for uid, field_map in fields_by_uid.items():
110
+ param = resolved[uid]
111
+ conflicts.extend(
112
+ (uid, field_name)
113
+ for field_name in field_map
114
+ if (not force) and param.has_field(field_name)
115
+ )
116
+
117
+ return conflicts
118
+
119
+ def _write_fields(
120
+ self,
121
+ fields_by_uid: dict[str, dict[str, Any]],
122
+ resolved: dict[str, ParameterDataProxy],
123
+ parameters: IParameterView,
124
+ ) -> None:
125
+ """Write field values to parameters.
126
+
127
+ Note:
128
+ Uses batch write context from parameters view. If an error occurs
129
+ mid-write, partial writes may be committed depending on storage
130
+ backend. Callers should handle this as a potential partial failure.
131
+ """
132
+ with parameters:
133
+ for uid, field_map in fields_by_uid.items():
134
+ param = resolved[uid]
135
+ for field_name, value in field_map.items():
136
+ param.set_field(field_name, value)
137
+
138
+
139
+ class AggregateIngestionError(Exception):
140
+ """Error during aggregate ingestion."""
141
+
142
+
143
+ class AggregateIngester:
144
+ """Utility for ingesting precomputed aggregate values.
145
+
146
+ Validates and writes precomputed aggregate values into the aggregate repository.
147
+
148
+ Example:
149
+ >>> ingester = AggregateIngester()
150
+ >>> ingester.ingest(
151
+ ... aggregates=[
152
+ ... {
153
+ ... "field_name": "l_overlap",
154
+ ... "context_models": ("m1", "m2"),
155
+ ... "value": arr,
156
+ ... }
157
+ ... ],
158
+ ... repository=aggregate_repository,
159
+ ... force=False,
160
+ ... )
161
+ """
162
+
163
+ def ingest(
164
+ self,
165
+ *,
166
+ aggregates: list[dict[str, Any]],
167
+ repository: AggregateRepository,
168
+ force: bool = False,
169
+ ) -> None:
170
+ """Ingest precomputed aggregate values with validation.
171
+
172
+ Args:
173
+ aggregates: List of aggregate dictionaries, each containing:
174
+ - field_name: Base field name (e.g., "l_overlap").
175
+ - context_models: Tuple/list of model IDs.
176
+ - context_params: Tuple/list of parameter names (optional).
177
+ - value: The computed value to store.
178
+ repository: Target aggregate repository.
179
+ force: If True, overwrite existing aggregate values.
180
+
181
+ Raises:
182
+ AggregateIngestionError: If aggregate structure is invalid or
183
+ conflicts are detected (force=False).
184
+ """
185
+ if not aggregates:
186
+ logger.debug("ingest_aggregates called with empty input, skipping")
187
+ return
188
+
189
+ self._validate_structure(aggregates)
190
+
191
+ if not force:
192
+ conflicts = self._check_conflicts(aggregates, repository)
193
+ if conflicts:
194
+ preview = ", ".join(conflicts[:_ERROR_PREVIEW_LIMIT])
195
+ suffix = "..." if len(conflicts) > _ERROR_PREVIEW_LIMIT else ""
196
+ raise AggregateIngestionError(
197
+ f"Aggregate conflicts detected: {preview}{suffix}"
198
+ )
199
+
200
+ self._write_aggregates(aggregates, repository)
201
+ logger.info("Ingested %d aggregates", len(aggregates))
202
+
203
+ def _validate_structure(self, aggregates: list[dict[str, Any]]) -> None:
204
+ """Validate aggregate structure."""
205
+ required_keys = ("field_name", "context_models", "value")
206
+
207
+ for i, agg in enumerate(aggregates):
208
+ for key in required_keys:
209
+ if key not in agg:
210
+ raise AggregateIngestionError(
211
+ f"Aggregate at index {i} missing '{key}'"
212
+ )
213
+
214
+ def _check_conflicts(
215
+ self,
216
+ aggregates: list[dict[str, Any]],
217
+ repository: AggregateRepository,
218
+ ) -> list[str]:
219
+ """Check for existing aggregates that would conflict."""
220
+ from diffract.core.data.nn.aggregates.metadata import AggregateMetadata
221
+
222
+ existing_uids = set(repository.create_view().list_uids())
223
+ conflicts: list[str] = []
224
+
225
+ for agg in aggregates:
226
+ uid = AggregateMetadata.create_uid_from_context(
227
+ field_name=agg["field_name"],
228
+ context_models=tuple(agg["context_models"]),
229
+ context_params=tuple(agg.get("context_params", [])),
230
+ )
231
+ if uid in existing_uids:
232
+ conflicts.append(uid)
233
+
234
+ return conflicts
235
+
236
+ def _write_aggregates(
237
+ self,
238
+ aggregates: list[dict[str, Any]],
239
+ repository: AggregateRepository,
240
+ ) -> None:
241
+ """Write aggregate values to repository."""
242
+ with repository.storage_manager:
243
+ for agg in aggregates:
244
+ aggregate = repository.get_or_create(
245
+ field_name=agg["field_name"],
246
+ context_models=tuple(agg["context_models"]),
247
+ context_params=tuple(agg.get("context_params", [])),
248
+ )
249
+ aggregate.set_field("value", agg["value"])
@@ -0,0 +1,141 @@
1
+ """Session utility namespace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING
8
+
9
+ from dependency_injector.wiring import Provide, inject
10
+
11
+ from diffract.session.namespaces.utils.merger import (
12
+ AggregateMerger,
13
+ MergeTargetState,
14
+ SessionMerger,
15
+ )
16
+ from diffract.session.session import Session, SessionContext
17
+
18
+ if TYPE_CHECKING:
19
+ from diffract.core.data.nn.aggregates.repository import AggregateRepository
20
+ from diffract.core.data.nn.params.interface import IParameterRepository
21
+ from diffract.core.parallel import ParallelContext
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class UtilsNamespace:
27
+ """Utility operations API for Session."""
28
+
29
+ @inject
30
+ def __init__(
31
+ self,
32
+ session_or_context: Session | SessionContext,
33
+ parameter_repository: IParameterRepository = Provide["nn.parameter_repository"],
34
+ aggregate_repository: AggregateRepository = Provide["nn.aggregate_repository"],
35
+ parallel_context_factory: Callable[[], ParallelContext] = Provide[
36
+ "parallel_singleton.thread_pool_context.provider"
37
+ ],
38
+ ) -> None:
39
+ self.__session_or_context = session_or_context
40
+ self.__param_repo = parameter_repository
41
+ self.__agg_repo = aggregate_repository
42
+ self.__parallel_context_factory = parallel_context_factory
43
+
44
+ def merge_other_session(
45
+ self,
46
+ other_session_or_context: Session | SessionContext,
47
+ fields: list[str] | None = None,
48
+ *,
49
+ verify: bool = True,
50
+ read_budget_bytes: int = 512 * 1024 * 1024,
51
+ ) -> None:
52
+ """Merge parameters from another session into this one.
53
+
54
+ Copies parameters and their computed fields from another session.
55
+ When verify is True, handles conflicts by skipping duplicate fields.
56
+
57
+ Args:
58
+ other_session_or_context: Source session to merge from.
59
+ fields: Specific fields to merge. If None, merges all fields.
60
+ verify: If True, check for conflicts and skip duplicates.
61
+ read_budget_bytes: Maximum bytes to read during merge operation.
62
+ """
63
+ with self.__session_or_context:
64
+ parallel = self.__parallel_context_factory()
65
+ target_state: MergeTargetState | None = None
66
+ if verify:
67
+ target_state = MergeTargetState()
68
+
69
+ models = self.__session_or_context.models
70
+ for parameter_info in models.parameters.list(verbose=True):
71
+ model_id = parameter_info["model_id"]
72
+ name = parameter_info["name"]
73
+ uid = parameter_info["uid"]
74
+ available_fields = parameter_info["available_fields"]
75
+
76
+ if model_id not in target_state.models_mapping:
77
+ target_state.models_mapping[model_id] = set()
78
+
79
+ target_state.models_mapping[model_id].add(name)
80
+ target_state.parameters_fields_mapping[(model_id, name)] = (
81
+ available_fields
82
+ )
83
+ target_state.parameters_uids_mapping[(model_id, name)] = uid
84
+
85
+ if isinstance(other_session_or_context, Session):
86
+ source_context = other_session_or_context.filter()
87
+ else:
88
+ source_context = other_session_or_context
89
+
90
+ source_params = source_context._param_filter_context
91
+ source_aggs = source_context._agg_filter_context
92
+
93
+ # Merge parameters if any exist
94
+ if source_params:
95
+ logger.info(
96
+ "Starting merge from session '%s' (%d parameters)",
97
+ other_session_or_context,
98
+ len(source_params),
99
+ )
100
+ if logger.isEnabledFor(logging.DEBUG):
101
+ logger.debug(
102
+ "Merge source models: %s",
103
+ other_session_or_context.models.list(),
104
+ )
105
+
106
+ source_fields_by_uid = source_params.list_fields_by_uid(
107
+ parallel=parallel
108
+ )
109
+
110
+ merger = SessionMerger(parallel=parallel)
111
+ merger.merge(
112
+ source=source_params,
113
+ source_fields_by_uid=source_fields_by_uid,
114
+ target_repository=self.__param_repo,
115
+ target_state=target_state,
116
+ fields=fields,
117
+ read_budget_bytes=read_budget_bytes,
118
+ )
119
+ else:
120
+ logger.debug("No parameters to merge from source session")
121
+
122
+ # Merge aggregates from source session (independent of parameters)
123
+ if source_aggs:
124
+ logger.info(
125
+ "Starting merge from session '%s' (%d aggregates)",
126
+ other_session_or_context,
127
+ len(source_aggs),
128
+ )
129
+
130
+ aggregate_merger = AggregateMerger()
131
+ aggregate_merger.merge(
132
+ source_aggregates=source_aggs,
133
+ target_repository=self.__agg_repo,
134
+ fields=fields,
135
+ verify=verify,
136
+ )
137
+ else:
138
+ logger.debug("No aggregates to merge from source session")
139
+
140
+ # Invalidate field cache since parameters/fields may have been added
141
+ self.__session_or_context._field_cache.invalidate()
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from tqdm.auto import tqdm
8
+
9
+ from diffract.core.constants import (
10
+ PROGRESS_BAR_DELAY_SEC,
11
+ PROGRESS_BAR_MIN_ITEMS,
12
+ )
13
+ from diffract.core.data.nn.params.proxy import ParameterDataProxy
14
+
15
+ if TYPE_CHECKING:
16
+ from diffract.core.data.nn.aggregates.repository import AggregateRepository
17
+ from diffract.core.data.nn.aggregates.view import AggregateView
18
+ from diffract.core.data.nn.params.interface import (
19
+ IParameterProxy,
20
+ IParameterRepository,
21
+ IParameterView,
22
+ )
23
+ from diffract.core.parallel import ParallelContext
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class MergeTargetState:
30
+ """State of target session for conflict detection during merge."""
31
+
32
+ models_mapping: dict[str, set[str]] = field(default_factory=dict)
33
+ parameters_uids_mapping: dict[tuple[str, str], str] = field(default_factory=dict)
34
+ parameters_fields_mapping: dict[tuple[str, str], list[str]] = field(
35
+ default_factory=dict
36
+ )
37
+
38
+
39
+ class SessionMerger:
40
+ """Utility for merging parameters between sessions.
41
+
42
+ Handles chunked transfer of parameters with prefetching and conflict detection.
43
+
44
+ Example:
45
+ >>> merger = SessionMerger(parallel=parallel_context)
46
+ >>> merger.merge(
47
+ ... source=source_session._get_view(),
48
+ ... source_fields_by_uid=source.list_fields_by_uid(),
49
+ ... target_repository=target_session._parameter_repository,
50
+ ... target_state=target_session._build_merge_target_state(),
51
+ ... fields=["metric", "loss"],
52
+ ... )
53
+ """
54
+
55
+ def __init__(self, parallel: ParallelContext) -> None:
56
+ """Initialize the merger.
57
+
58
+ Args:
59
+ parallel: Parallel context for concurrent operations.
60
+ """
61
+ self._parallel = parallel
62
+
63
+ def merge(
64
+ self,
65
+ *,
66
+ source: IParameterView,
67
+ source_fields_by_uid: dict[str, list[str]],
68
+ target_repository: IParameterRepository,
69
+ target_state: MergeTargetState | None = None,
70
+ fields: list[str] | None = None,
71
+ read_budget_bytes: int = 512 * 1024 * 1024,
72
+ ) -> None:
73
+ """Merge parameters from source view into target repository.
74
+
75
+ Args:
76
+ source: Source parameter view to merge from.
77
+ source_fields_by_uid: Precomputed mapping of uid -> available fields.
78
+ target_repository: Target repository to merge into.
79
+ target_state: Current state of target for conflict detection.
80
+ If None, no conflict checking is performed.
81
+ fields: Specific fields to merge. If None, merges all fields.
82
+ read_budget_bytes: Maximum bytes to read per chunk.
83
+ """
84
+ if not source:
85
+ return
86
+
87
+ total = len(source)
88
+ field_allowlist = set(fields) if fields else None
89
+
90
+ required_fields_by_uid = self._compute_required_fields(
91
+ source, source_fields_by_uid, field_allowlist
92
+ )
93
+
94
+ with tqdm(
95
+ total=total,
96
+ desc="Parameters migration...",
97
+ delay=PROGRESS_BAR_DELAY_SEC,
98
+ disable=total < PROGRESS_BAR_MIN_ITEMS,
99
+ ) as pbar:
100
+ for chunk in source.iter_chunks_by_read_budget(
101
+ budget_bytes=read_budget_bytes,
102
+ required_fields_by_uid=required_fields_by_uid,
103
+ parallel=self._parallel,
104
+ ):
105
+ self._process_chunk(
106
+ chunk=chunk,
107
+ source_fields_by_uid=source_fields_by_uid,
108
+ target_repository=target_repository,
109
+ target_state=target_state,
110
+ field_allowlist=field_allowlist,
111
+ required_fields_by_uid=required_fields_by_uid,
112
+ pbar=pbar,
113
+ )
114
+
115
+ def _compute_required_fields(
116
+ self,
117
+ source: IParameterView,
118
+ source_fields_by_uid: dict[str, list[str]],
119
+ field_allowlist: set[str] | None,
120
+ ) -> dict[str, list[str]]:
121
+ """Compute required fields per parameter for prefetching."""
122
+ required_fields_by_uid: dict[str, list[str]] = {}
123
+
124
+ for param in source:
125
+ uid = param.meta.uid
126
+ available = source_fields_by_uid.get(uid, [])
127
+
128
+ required: set[str] = set()
129
+ for f in available:
130
+ if field_allowlist is not None and f not in field_allowlist:
131
+ continue
132
+ required.add(f)
133
+
134
+ required_fields_by_uid[uid] = sorted(required)
135
+
136
+ return required_fields_by_uid
137
+
138
+ def _process_chunk(
139
+ self,
140
+ *,
141
+ chunk: IParameterView,
142
+ source_fields_by_uid: dict[str, list[str]],
143
+ target_repository: IParameterRepository,
144
+ target_state: MergeTargetState | None,
145
+ field_allowlist: set[str] | None,
146
+ required_fields_by_uid: dict[str, list[str]],
147
+ pbar: Any,
148
+ ) -> None:
149
+ """Process a single chunk of parameters during merge."""
150
+ chunk_required_fields_by_uid = {
151
+ p.meta.uid: required_fields_by_uid.get(p.meta.uid, []) for p in chunk
152
+ }
153
+ chunk.prefetch_fields(
154
+ fields_by_uid=chunk_required_fields_by_uid,
155
+ parallel=self._parallel,
156
+ )
157
+
158
+ with target_repository:
159
+ for pending_parameter in chunk:
160
+ self._transfer_parameter(
161
+ pending_parameter=pending_parameter,
162
+ source_fields_by_uid=source_fields_by_uid,
163
+ target_repository=target_repository,
164
+ target_state=target_state,
165
+ field_allowlist=field_allowlist,
166
+ )
167
+ pbar.update(1)
168
+
169
+ def _transfer_parameter(
170
+ self,
171
+ *,
172
+ pending_parameter: IParameterProxy,
173
+ source_fields_by_uid: dict[str, list[str]],
174
+ target_repository: IParameterRepository,
175
+ target_state: MergeTargetState | None,
176
+ field_allowlist: set[str] | None,
177
+ ) -> None:
178
+ """Transfer a single parameter to the target repository."""
179
+ meta = pending_parameter.meta
180
+ create_new = True
181
+
182
+ if (
183
+ target_state is not None
184
+ and meta.model_id in target_state.models_mapping
185
+ and meta.name in target_state.models_mapping[meta.model_id]
186
+ ):
187
+ uid = target_state.parameters_uids_mapping[(meta.model_id, meta.name)]
188
+ new_parameter = target_repository.get_proxy(uid)
189
+ create_new = False
190
+
191
+ if create_new:
192
+ new_parameter = ParameterDataProxy.create_and_store(
193
+ meta,
194
+ repository=target_repository,
195
+ )
196
+
197
+ available_fields = source_fields_by_uid.get(meta.uid, [])
198
+ for field_name in available_fields:
199
+ if field_allowlist is not None and field_name not in field_allowlist:
200
+ continue
201
+ if (not create_new) and target_state is not None:
202
+ existing_fields = target_state.parameters_fields_mapping.get(
203
+ (meta.model_id, meta.name), []
204
+ )
205
+ if field_name in existing_fields:
206
+ continue
207
+
208
+ new_parameter.set_field(
209
+ field_name,
210
+ pending_parameter.get_field(field_name, auto_prefetch=True),
211
+ )
212
+
213
+
214
+ class AggregateMerger:
215
+ """Utility for merging aggregates between sessions.
216
+
217
+ Handles transfer of aggregate entries with conflict detection.
218
+
219
+ Example:
220
+ >>> merger = AggregateMerger()
221
+ >>> merger.merge(
222
+ ... source_aggregates=source_session._aggregate_repository.create_view(),
223
+ ... target_repository=target_session._aggregate_repository,
224
+ ... model_ids=["model_a"],
225
+ ... fields=["l_overlap"],
226
+ ... verify=True,
227
+ ... )
228
+ """
229
+
230
+ def merge(
231
+ self,
232
+ *,
233
+ source_aggregates: AggregateView,
234
+ target_repository: AggregateRepository,
235
+ model_ids: list[str] | None = None,
236
+ fields: list[str] | None = None,
237
+ verify: bool = True,
238
+ ) -> None:
239
+ """Merge aggregates from source view into target repository.
240
+
241
+ Args:
242
+ source_aggregates: Source aggregate view to merge from.
243
+ target_repository: Target repository to merge into.
244
+ model_ids: Filter source aggregates by context_models.
245
+ fields: Filter source aggregates by field_name.
246
+ verify: If True, skip existing aggregates.
247
+ """
248
+ filtered = source_aggregates
249
+ logger.debug("Merging aggregates: source has %d aggregates", len(filtered))
250
+
251
+ # Filter by model_ids if specified
252
+ if model_ids:
253
+ filtered = filtered.filter_by_context_models(*model_ids)
254
+
255
+ # Filter by fields if specified
256
+ if fields:
257
+ filtered = filtered.filter_by_field_name(*fields)
258
+
259
+ if not filtered:
260
+ logger.debug("No source aggregates to merge after filtering")
261
+ return
262
+
263
+ # Prefetch values from source
264
+ filtered.prefetch_fields(fields=["value"], parallel=None)
265
+
266
+ # Get existing aggregate UIDs in target
267
+ target_uids = set(target_repository.create_view().list_uids())
268
+
269
+ merged_count = 0
270
+ skipped_count = 0
271
+
272
+ with target_repository.storage_manager:
273
+ for source_agg in filtered:
274
+ if source_agg.meta.uid in target_uids and verify:
275
+ skipped_count += 1
276
+ continue
277
+
278
+ # Create or get aggregate in target
279
+ target_agg = target_repository.get_or_create(
280
+ field_name=source_agg.meta.field_name,
281
+ context_models=source_agg.meta.context_models,
282
+ context_params=source_agg.meta.context_params,
283
+ )
284
+
285
+ # Copy value if exists
286
+ if source_agg.has_field("value"):
287
+ target_agg.set_field("value", source_agg.get_field("value"))
288
+ merged_count += 1
289
+
290
+ if merged_count > 0 or skipped_count > 0:
291
+ logger.debug(
292
+ "Aggregates merge: %d copied, %d skipped (existing)",
293
+ merged_count,
294
+ skipped_count,
295
+ )