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.
- diffract/__init__.py +52 -0
- diffract/configs/fast_speed_without_disk.ini +48 -0
- diffract/configs/hybrid.ini +74 -0
- diffract/configs/sqlite.ini +62 -0
- diffract/containers.py +405 -0
- diffract/core/__init__.py +23 -0
- diffract/core/cache/__init__.py +24 -0
- diffract/core/cache/containers.py +93 -0
- diffract/core/cache/interface.py +117 -0
- diffract/core/cache/redis_manager.py +464 -0
- diffract/core/cache/simple_manager.py +478 -0
- diffract/core/compute/__init__.py +46 -0
- diffract/core/compute/config.py +48 -0
- diffract/core/compute/containers.py +81 -0
- diffract/core/compute/decorator.py +125 -0
- diffract/core/compute/exceptions.py +31 -0
- diffract/core/compute/execution/__init__.py +14 -0
- diffract/core/compute/execution/_types.py +70 -0
- diffract/core/compute/execution/aggregation.py +74 -0
- diffract/core/compute/execution/aggregation_runner.py +362 -0
- diffract/core/compute/execution/enums.py +38 -0
- diffract/core/compute/execution/executor.py +125 -0
- diffract/core/compute/execution/parameter_runner.py +125 -0
- diffract/core/compute/execution/restrictions.py +61 -0
- diffract/core/compute/execution/strategy.py +118 -0
- diffract/core/compute/execution/utils.py +112 -0
- diffract/core/compute/extensions/__init__.py +0 -0
- diffract/core/compute/extensions/power_law.py +1051 -0
- diffract/core/compute/extensions/rmt.py +150 -0
- diffract/core/compute/extensions/utils.py +36 -0
- diffract/core/compute/field_signature.py +183 -0
- diffract/core/compute/kernels/__init__.py +48 -0
- diffract/core/compute/kernels/alignment.py +106 -0
- diffract/core/compute/kernels/heavy_tailed.py +394 -0
- diffract/core/compute/kernels/marchenko_pastur.py +99 -0
- diffract/core/compute/kernels/mat_decomposition.py +101 -0
- diffract/core/compute/kernels/mat_properties.py +53 -0
- diffract/core/compute/kernels/model_quality.py +27 -0
- diffract/core/compute/kernels/norms.py +88 -0
- diffract/core/compute/kernels/ranks.py +35 -0
- diffract/core/compute/kernels/tracy_widom.py +36 -0
- diffract/core/compute/metadata.py +67 -0
- diffract/core/compute/registry.py +485 -0
- diffract/core/constants.py +147 -0
- diffract/core/data/__init__.py +32 -0
- diffract/core/data/interface.py +304 -0
- diffract/core/data/metadata/__init__.py +15 -0
- diffract/core/data/metadata/containers.py +60 -0
- diffract/core/data/metadata/interface.py +208 -0
- diffract/core/data/metadata/sqlite_index.py +604 -0
- diffract/core/data/nn/__init__.py +43 -0
- diffract/core/data/nn/aggregates/__init__.py +35 -0
- diffract/core/data/nn/aggregates/metadata.py +96 -0
- diffract/core/data/nn/aggregates/proxy.py +57 -0
- diffract/core/data/nn/aggregates/repository.py +87 -0
- diffract/core/data/nn/aggregates/view.py +307 -0
- diffract/core/data/nn/containers.py +86 -0
- diffract/core/data/nn/extractors/__init__.py +49 -0
- diffract/core/data/nn/extractors/base.py +300 -0
- diffract/core/data/nn/extractors/factory.py +234 -0
- diffract/core/data/nn/extractors/flax.py +112 -0
- diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
- diffract/core/data/nn/extractors/handlers/base.py +110 -0
- diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
- diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
- diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
- diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
- diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
- diffract/core/data/nn/extractors/interface.py +56 -0
- diffract/core/data/nn/extractors/numpy.py +94 -0
- diffract/core/data/nn/extractors/onnx.py +108 -0
- diffract/core/data/nn/extractors/tensorflow.py +99 -0
- diffract/core/data/nn/extractors/torch.py +204 -0
- diffract/core/data/nn/params/__init__.py +27 -0
- diffract/core/data/nn/params/interface.py +402 -0
- diffract/core/data/nn/params/metadata.py +110 -0
- diffract/core/data/nn/params/proxy.py +93 -0
- diffract/core/data/nn/params/repository.py +45 -0
- diffract/core/data/nn/params/schema.py +82 -0
- diffract/core/data/nn/params/view.py +393 -0
- diffract/core/data/proxy.py +246 -0
- diffract/core/data/repository.py +227 -0
- diffract/core/data/utils.py +33 -0
- diffract/core/data/view.py +389 -0
- diffract/core/export/__init__.py +27 -0
- diffract/core/export/containers.py +47 -0
- diffract/core/export/exporters.py +191 -0
- diffract/core/export/formatters/__init__.py +23 -0
- diffract/core/export/formatters/base.py +68 -0
- diffract/core/export/formatters/dict_formatter.py +38 -0
- diffract/core/export/formatters/json_formatter.py +58 -0
- diffract/core/export/formatters/list_formatter.py +41 -0
- diffract/core/export/formatters/pandas_formatter.py +86 -0
- diffract/core/export/formatters/polars_formatter.py +86 -0
- diffract/core/export/formatters/registry.py +84 -0
- diffract/core/export/interface.py +105 -0
- diffract/core/parallel/__init__.py +21 -0
- diffract/core/parallel/containers.py +57 -0
- diffract/core/parallel/execution.py +91 -0
- diffract/core/parallel/runtime.py +91 -0
- diffract/core/storage/__init__.py +35 -0
- diffract/core/storage/base_manager.py +330 -0
- diffract/core/storage/containers.py +122 -0
- diffract/core/storage/hdf5_manager.py +856 -0
- diffract/core/storage/hybrid_manager.py +218 -0
- diffract/core/storage/interface.py +222 -0
- diffract/core/storage/metadata.py +89 -0
- diffract/core/storage/ram_manager.py +159 -0
- diffract/core/storage/sqlite_manager.py +1062 -0
- diffract/core/storage/zarr_manager.py +992 -0
- diffract/core/utils/__init__.py +45 -0
- diffract/core/utils/build.py +46 -0
- diffract/core/utils/exceptions.py +11 -0
- diffract/core/utils/hashing.py +90 -0
- diffract/core/utils/imports.py +292 -0
- diffract/core/utils/math.py +15 -0
- diffract/py.typed +0 -0
- diffract/session/__init__.py +24 -0
- diffract/session/errors.py +17 -0
- diffract/session/field_cache.py +216 -0
- diffract/session/namespaces/__init__.py +15 -0
- diffract/session/namespaces/compute/__init__.py +219 -0
- diffract/session/namespaces/models/__init__.py +282 -0
- diffract/session/namespaces/models/parameters/__init__.py +133 -0
- diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
- diffract/session/namespaces/results/__init__.py +378 -0
- diffract/session/namespaces/results/eraser.py +129 -0
- diffract/session/namespaces/results/injester.py +249 -0
- diffract/session/namespaces/utils/__init__.py +141 -0
- diffract/session/namespaces/utils/merger.py +295 -0
- diffract/session/namespaces/viz/__init__.py +91 -0
- diffract/session/namespaces/viz/_utils.py +14 -0
- diffract/session/namespaces/viz/box.py +207 -0
- diffract/session/namespaces/viz/grid.py +160 -0
- diffract/session/namespaces/viz/heatmap.py +164 -0
- diffract/session/namespaces/viz/scatter.py +202 -0
- diffract/session/namespaces/viz/sparkline.py +270 -0
- diffract/session/namespaces/viz/violin.py +212 -0
- diffract/session/session.py +260 -0
- diffract/session/utils.py +81 -0
- diffract/viz/__init__.py +42 -0
- diffract/viz/data/__init__.py +34 -0
- diffract/viz/data/detection.py +57 -0
- diffract/viz/data/extraction.py +117 -0
- diffract/viz/data/filtering.py +57 -0
- diffract/viz/data/ordering.py +129 -0
- diffract/viz/data/provider.py +99 -0
- diffract/viz/data/types.py +98 -0
- diffract/viz/plots/__init__.py +37 -0
- diffract/viz/plots/base/__init__.py +20 -0
- diffract/viz/plots/base/axis.py +219 -0
- diffract/viz/plots/base/coloraxis.py +133 -0
- diffract/viz/plots/base/configurator.py +18 -0
- diffract/viz/plots/base/jitter.py +368 -0
- diffract/viz/plots/base/line.py +197 -0
- diffract/viz/plots/base/marker.py +278 -0
- diffract/viz/plots/base/overlay.py +14 -0
- diffract/viz/plots/base/plot.py +110 -0
- diffract/viz/plots/base/update.py +50 -0
- diffract/viz/plots/boxplot.py +283 -0
- diffract/viz/plots/cluster.py +374 -0
- diffract/viz/plots/heatmap.py +168 -0
- diffract/viz/plots/scatter.py +233 -0
- diffract/viz/plots/sparkline.py +405 -0
- diffract/viz/plots/subplots/__init__.py +32 -0
- diffract/viz/plots/subplots/coloraxis.py +339 -0
- diffract/viz/plots/subplots/factory.py +713 -0
- diffract/viz/plots/subplots/grid.py +214 -0
- diffract/viz/plots/subplots/layout.py +370 -0
- diffract/viz/plots/subplots/spec.py +39 -0
- diffract/viz/plots/violin.py +298 -0
- diffract/viz/renderer.py +513 -0
- diffract/viz/styling/__init__.py +78 -0
- diffract/viz/styling/palettes/__init__.py +20 -0
- diffract/viz/styling/palettes/bundle.py +16 -0
- diffract/viz/styling/palettes/color.py +83 -0
- diffract/viz/styling/palettes/dashes.py +27 -0
- diffract/viz/styling/palettes/symbols.py +42 -0
- diffract/viz/styling/resolvers/__init__.py +11 -0
- diffract/viz/styling/resolvers/categorical.py +68 -0
- diffract/viz/styling/resolvers/color.py +77 -0
- diffract/viz/styling/resolvers/numeric.py +108 -0
- diffract/viz/styling/sources.py +27 -0
- diffract/viz/styling/theme/__init__.py +29 -0
- diffract/viz/styling/theme/applier.py +114 -0
- diffract/viz/styling/theme/components.py +66 -0
- diffract/viz/styling/theme/presets.py +58 -0
- diffract/viz/styling/theme/theme.py +36 -0
- diffract_core-0.2.1.dist-info/METADATA +530 -0
- diffract_core-0.2.1.dist-info/RECORD +193 -0
- diffract_core-0.2.1.dist-info/WHEEL +4 -0
- diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
- 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
|
+
)
|