experimaestro 1.11.1__py3-none-any.whl → 2.0.0b4__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.
Potentially problematic release.
This version of experimaestro might be problematic. Click here for more details.
- experimaestro/__init__.py +10 -11
- experimaestro/annotations.py +167 -206
- experimaestro/cli/__init__.py +140 -16
- experimaestro/cli/filter.py +42 -74
- experimaestro/cli/jobs.py +157 -106
- experimaestro/cli/progress.py +269 -0
- experimaestro/cli/refactor.py +249 -0
- experimaestro/click.py +0 -1
- experimaestro/commandline.py +19 -3
- experimaestro/connectors/__init__.py +22 -3
- experimaestro/connectors/local.py +12 -0
- experimaestro/core/arguments.py +192 -37
- experimaestro/core/identifier.py +127 -12
- experimaestro/core/objects/__init__.py +6 -0
- experimaestro/core/objects/config.py +702 -285
- experimaestro/core/objects/config_walk.py +24 -6
- experimaestro/core/serialization.py +91 -34
- experimaestro/core/serializers.py +1 -8
- experimaestro/core/subparameters.py +164 -0
- experimaestro/core/types.py +198 -83
- experimaestro/exceptions.py +26 -0
- experimaestro/experiments/cli.py +107 -25
- experimaestro/generators.py +50 -9
- experimaestro/huggingface.py +3 -1
- experimaestro/launcherfinder/parser.py +29 -0
- experimaestro/launcherfinder/registry.py +3 -3
- experimaestro/launchers/__init__.py +26 -1
- experimaestro/launchers/direct.py +12 -0
- experimaestro/launchers/slurm/base.py +154 -2
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/mkdocs/metaloader.py +0 -1
- experimaestro/mypy.py +452 -7
- experimaestro/notifications.py +75 -16
- experimaestro/progress.py +404 -0
- experimaestro/rpyc.py +0 -1
- experimaestro/run.py +19 -6
- experimaestro/scheduler/__init__.py +18 -1
- experimaestro/scheduler/base.py +504 -959
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +582 -0
- experimaestro/scheduler/interfaces.py +474 -0
- experimaestro/scheduler/jobs.py +485 -0
- experimaestro/scheduler/services.py +186 -12
- experimaestro/scheduler/signal_handler.py +32 -0
- experimaestro/scheduler/state.py +1 -1
- experimaestro/scheduler/state_db.py +388 -0
- experimaestro/scheduler/state_provider.py +2345 -0
- experimaestro/scheduler/state_sync.py +834 -0
- experimaestro/scheduler/workspace.py +52 -10
- experimaestro/scriptbuilder.py +7 -0
- experimaestro/server/__init__.py +153 -32
- experimaestro/server/data/index.css +0 -125
- experimaestro/server/data/index.css.map +1 -1
- experimaestro/server/data/index.js +194 -58
- experimaestro/server/data/index.js.map +1 -1
- experimaestro/settings.py +47 -6
- experimaestro/sphinx/__init__.py +3 -3
- experimaestro/taskglobals.py +20 -0
- experimaestro/tests/conftest.py +80 -0
- experimaestro/tests/core/test_generics.py +2 -2
- experimaestro/tests/identifier_stability.json +45 -0
- experimaestro/tests/launchers/bin/sacct +6 -2
- experimaestro/tests/launchers/bin/sbatch +4 -2
- experimaestro/tests/launchers/common.py +2 -2
- experimaestro/tests/launchers/test_slurm.py +80 -0
- experimaestro/tests/restart.py +1 -1
- experimaestro/tests/tasks/all.py +7 -0
- experimaestro/tests/tasks/test_dynamic.py +231 -0
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_cli_jobs.py +615 -0
- experimaestro/tests/test_dependencies.py +11 -17
- experimaestro/tests/test_deprecated.py +630 -0
- experimaestro/tests/test_environment.py +200 -0
- experimaestro/tests/test_experiment.py +3 -3
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_forward.py +3 -3
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +520 -169
- experimaestro/tests/test_identifier_stability.py +458 -0
- experimaestro/tests/test_instance.py +16 -21
- experimaestro/tests/test_multitoken.py +442 -0
- experimaestro/tests/test_mypy.py +433 -0
- experimaestro/tests/test_objects.py +314 -30
- experimaestro/tests/test_outputs.py +8 -8
- experimaestro/tests/test_param.py +22 -26
- experimaestro/tests/test_partial_paths.py +231 -0
- experimaestro/tests/test_progress.py +2 -50
- experimaestro/tests/test_resumable_task.py +480 -0
- experimaestro/tests/test_serializers.py +141 -60
- experimaestro/tests/test_state_db.py +434 -0
- experimaestro/tests/test_subparameters.py +160 -0
- experimaestro/tests/test_tags.py +151 -15
- experimaestro/tests/test_tasks.py +137 -160
- experimaestro/tests/test_token_locking.py +252 -0
- experimaestro/tests/test_tokens.py +25 -19
- experimaestro/tests/test_types.py +133 -11
- experimaestro/tests/test_validation.py +19 -19
- experimaestro/tests/test_workspace_triggers.py +158 -0
- experimaestro/tests/token_reschedule.py +5 -3
- experimaestro/tests/utils.py +2 -2
- experimaestro/tokens.py +154 -57
- experimaestro/tools/diff.py +8 -1
- experimaestro/tui/__init__.py +8 -0
- experimaestro/tui/app.py +2303 -0
- experimaestro/tui/app.tcss +353 -0
- experimaestro/tui/log_viewer.py +228 -0
- experimaestro/typingutils.py +11 -2
- experimaestro/utils/__init__.py +23 -0
- experimaestro/utils/environment.py +148 -0
- experimaestro/utils/git.py +129 -0
- experimaestro/utils/resources.py +1 -1
- experimaestro/version.py +34 -0
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
- experimaestro-2.0.0b4.dist-info/RECORD +181 -0
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
- experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
- experimaestro/compat.py +0 -6
- experimaestro/core/objects.pyi +0 -225
- experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
- experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
- experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
- experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
- experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
- experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
- experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
- experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- experimaestro-1.11.1.dist-info/RECORD +0 -158
- experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
experimaestro/core/arguments.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
"""Management of the arguments (params, options, etc) associated with the XPM objects"""
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from typing import Optional, TypeVar, TYPE_CHECKING, Callable, Any
|
|
4
5
|
from experimaestro.typingutils import get_optional
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
import
|
|
7
|
+
from typing import Annotated
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
|
-
from typing_extensions import Annotated
|
|
10
10
|
import experimaestro.core.types
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
from experimaestro.core.subparameters import ParameterGroup
|
|
12
|
+
|
|
13
|
+
# Track deprecation warnings per module (max 10 per module)
|
|
14
|
+
_deprecation_warning_counts: dict[str, int] = {}
|
|
15
|
+
_MAX_WARNINGS_PER_MODULE = 10
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class Argument:
|
|
@@ -29,10 +29,12 @@ class Argument:
|
|
|
29
29
|
help=None,
|
|
30
30
|
generator=None,
|
|
31
31
|
ignored=None,
|
|
32
|
-
|
|
32
|
+
field_or_default=None,
|
|
33
33
|
checker=None,
|
|
34
34
|
constant=False,
|
|
35
35
|
is_data=False,
|
|
36
|
+
overrides=False,
|
|
37
|
+
groups: set["ParameterGroup"] = None,
|
|
36
38
|
):
|
|
37
39
|
"""Creates a new argument
|
|
38
40
|
|
|
@@ -52,7 +54,8 @@ class Argument:
|
|
|
52
54
|
ignored (bool, optional): True if ignored (if None, computed
|
|
53
55
|
automatically). Defaults to None.
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
field_or_default (any | field, optional): Default value or field
|
|
58
|
+
object containing default information. Defaults to None.
|
|
56
59
|
|
|
57
60
|
checker (any, optional): Value checker. Defaults to None.
|
|
58
61
|
|
|
@@ -61,9 +64,17 @@ class Argument:
|
|
|
61
64
|
|
|
62
65
|
is_data (bool, optional): Flag for paths that are data path (to be
|
|
63
66
|
serialized). Defaults to False.
|
|
67
|
+
|
|
68
|
+
overrides (bool, optional): If True, this argument intentionally
|
|
69
|
+
overrides a parent argument. Suppresses the warning that would
|
|
70
|
+
otherwise be issued. Defaults to False.
|
|
71
|
+
|
|
72
|
+
groups (set[ParameterGroup], optional): Set of groups this parameter
|
|
73
|
+
belongs to. Used with subparameters to compute partial identifiers.
|
|
74
|
+
Defaults to None (empty set).
|
|
64
75
|
"""
|
|
65
|
-
required = (
|
|
66
|
-
if
|
|
76
|
+
required = (field_or_default is None) if required is None else required
|
|
77
|
+
if field_or_default is not None and required is not None and required:
|
|
67
78
|
raise Exception(
|
|
68
79
|
"argument '%s' is required but default value is given" % name
|
|
69
80
|
)
|
|
@@ -77,19 +88,42 @@ class Argument:
|
|
|
77
88
|
self.required = required
|
|
78
89
|
self.objecttype = None
|
|
79
90
|
self.is_data = is_data
|
|
91
|
+
self.overrides = overrides
|
|
80
92
|
|
|
81
93
|
self.generator = generator
|
|
82
94
|
self.default = None
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
self.ignore_generated = False
|
|
96
|
+
self.ignore_default_in_identifier = False
|
|
97
|
+
self.groups = groups if groups else set()
|
|
98
|
+
|
|
99
|
+
if field_or_default is not None:
|
|
100
|
+
assert (
|
|
101
|
+
self.generator is None
|
|
102
|
+
), "generator and field_or_default are exclusive options"
|
|
103
|
+
if isinstance(field_or_default, field):
|
|
104
|
+
self.ignore_generated = field_or_default.ignore_generated
|
|
105
|
+
# Allow field to override the overrides flag
|
|
106
|
+
if field_or_default.overrides:
|
|
107
|
+
self.overrides = True
|
|
108
|
+
|
|
109
|
+
# Process groups from field
|
|
110
|
+
if field_or_default.groups:
|
|
111
|
+
self.groups = field_or_default.groups
|
|
112
|
+
|
|
113
|
+
if field_or_default.ignore_default is not None:
|
|
114
|
+
# ignore_default: value is default AND ignored in identifier
|
|
115
|
+
self.default = field_or_default.ignore_default
|
|
116
|
+
self.ignore_default_in_identifier = True
|
|
117
|
+
elif field_or_default.default is not None:
|
|
118
|
+
# default: value is default but NOT ignored in identifier
|
|
119
|
+
self.default = field_or_default.default
|
|
120
|
+
self.ignore_default_in_identifier = False
|
|
121
|
+
elif field_or_default.default_factory is not None:
|
|
122
|
+
self.generator = field_or_default.default_factory
|
|
91
123
|
else:
|
|
92
|
-
|
|
124
|
+
# Bare default: backwards compatible, ignore in identifier
|
|
125
|
+
self.default = field_or_default
|
|
126
|
+
self.ignore_default_in_identifier = True
|
|
93
127
|
|
|
94
128
|
assert (
|
|
95
129
|
not self.constant or self.default is not None
|
|
@@ -134,12 +168,44 @@ class ArgumentOptions:
|
|
|
134
168
|
optionaltype = get_optional(typehint)
|
|
135
169
|
type = Type.fromType(optionaltype or typehint)
|
|
136
170
|
|
|
137
|
-
if
|
|
171
|
+
if (
|
|
172
|
+
"field_or_default" not in self.kwargs
|
|
173
|
+
or self.kwargs["field_or_default"] is None
|
|
174
|
+
):
|
|
138
175
|
defaultvalue = getattr(originaltype, name, None)
|
|
139
|
-
self.kwargs["
|
|
176
|
+
self.kwargs["field_or_default"] = defaultvalue
|
|
177
|
+
|
|
178
|
+
# Emit deprecation warning for bare default values (not wrapped in field)
|
|
179
|
+
# Skip warning for Constant parameters - they are inherently constant, not defaults
|
|
180
|
+
if (
|
|
181
|
+
defaultvalue is not None
|
|
182
|
+
and not isinstance(defaultvalue, field)
|
|
183
|
+
and not self.kwargs.get("constant")
|
|
184
|
+
):
|
|
185
|
+
module = originaltype.__module__
|
|
186
|
+
count = _deprecation_warning_counts.get(module, 0)
|
|
187
|
+
if count < _MAX_WARNINGS_PER_MODULE:
|
|
188
|
+
_deprecation_warning_counts[module] = count + 1
|
|
189
|
+
class_name = originaltype.__qualname__
|
|
190
|
+
remaining = _MAX_WARNINGS_PER_MODULE - count - 1
|
|
191
|
+
limit_msg = (
|
|
192
|
+
f" ({remaining} more warnings for this module)"
|
|
193
|
+
if remaining > 0
|
|
194
|
+
else " (no more warnings for this module)"
|
|
195
|
+
)
|
|
196
|
+
warnings.warn(
|
|
197
|
+
f"Deprecated: parameter `{name}` in {module}.{class_name} "
|
|
198
|
+
f"has an ambiguous default value. Use `field(ignore_default=...)` "
|
|
199
|
+
f"to keep current behavior (default ignored in identifier) or "
|
|
200
|
+
f"`field(default=...)` to include default in identifier. "
|
|
201
|
+
f"Run `experimaestro refactor default-values` to fix automatically."
|
|
202
|
+
f"{limit_msg}",
|
|
203
|
+
DeprecationWarning,
|
|
204
|
+
stacklevel=6,
|
|
205
|
+
)
|
|
140
206
|
|
|
141
207
|
self.kwargs["required"] = (optionaltype is None) and (
|
|
142
|
-
self.kwargs["
|
|
208
|
+
self.kwargs["field_or_default"] is None
|
|
143
209
|
)
|
|
144
210
|
|
|
145
211
|
return Argument(name, type, **self.kwargs)
|
|
@@ -171,26 +237,116 @@ T = TypeVar("T")
|
|
|
171
237
|
|
|
172
238
|
paramHint = _Param()
|
|
173
239
|
Param = Annotated[T, paramHint]
|
|
240
|
+
"""Type annotation for configuration parameters.
|
|
241
|
+
|
|
242
|
+
Parameters annotated with ``Param[T]`` are included in the configuration
|
|
243
|
+
identifier computation and must be set before the configuration is sealed.
|
|
244
|
+
|
|
245
|
+
Example::
|
|
246
|
+
|
|
247
|
+
class MyConfig(Config):
|
|
248
|
+
name: Param[str]
|
|
249
|
+
count: Param[int] = field(default=10)
|
|
250
|
+
threshold: Param[float] = field(ignore_default=0.5)
|
|
251
|
+
"""
|
|
174
252
|
|
|
175
253
|
optionHint = _Param(ignored=True)
|
|
176
254
|
Option = Annotated[T, optionHint]
|
|
255
|
+
"""Deprecated alias for Meta. Use Meta instead."""
|
|
256
|
+
|
|
177
257
|
Meta = Annotated[T, optionHint]
|
|
258
|
+
"""Type annotation for meta-parameters (ignored in identifier computation).
|
|
259
|
+
|
|
260
|
+
Use ``Meta[T]`` for parameters that should not affect the task identity,
|
|
261
|
+
such as output paths or runtime configuration.
|
|
262
|
+
|
|
263
|
+
Example::
|
|
264
|
+
|
|
265
|
+
class MyTask(Task):
|
|
266
|
+
# This affects the task identity
|
|
267
|
+
learning_rate: Param[float]
|
|
268
|
+
|
|
269
|
+
# This does not affect the identity
|
|
270
|
+
checkpoint_path: Meta[Path] = field(default_factory=PathGenerator("model.pt"))
|
|
271
|
+
"""
|
|
178
272
|
|
|
179
273
|
dataHint = _Param(ignored=True, is_data=True)
|
|
180
274
|
DataPath = Annotated[Path, dataHint]
|
|
181
|
-
"""
|
|
275
|
+
"""Type annotation for data paths that should be serialized.
|
|
276
|
+
|
|
277
|
+
Use ``DataPath`` for paths that point to data files that should be
|
|
278
|
+
preserved when serializing/deserializing a configuration. The path
|
|
279
|
+
is copied during serialization.
|
|
280
|
+
|
|
281
|
+
Example::
|
|
282
|
+
|
|
283
|
+
class MyConfig(Config):
|
|
284
|
+
model_weights: DataPath
|
|
285
|
+
"""
|
|
182
286
|
|
|
183
287
|
|
|
184
288
|
class field:
|
|
185
|
-
"""
|
|
289
|
+
"""Specify additional properties for a configuration parameter.
|
|
290
|
+
|
|
291
|
+
Use ``field()`` to control default value behavior and parameter grouping.
|
|
292
|
+
|
|
293
|
+
Example::
|
|
294
|
+
|
|
295
|
+
class MyConfig(Config):
|
|
296
|
+
# Default included in identifier
|
|
297
|
+
count: Param[int] = field(default=10)
|
|
298
|
+
|
|
299
|
+
# Default ignored in identifier (backwards compatible)
|
|
300
|
+
threshold: Param[float] = field(ignore_default=0.5)
|
|
186
301
|
|
|
187
|
-
|
|
302
|
+
# Generated path
|
|
303
|
+
output: Meta[Path] = field(default_factory=PathGenerator("out.txt"))
|
|
304
|
+
|
|
305
|
+
# Parameter in a group (for partial identifiers)
|
|
306
|
+
lr: Param[float] = field(groups=[training_group])
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
def __init__(
|
|
310
|
+
self,
|
|
311
|
+
*,
|
|
312
|
+
default: Any = None,
|
|
313
|
+
default_factory: Callable = None,
|
|
314
|
+
ignore_default: Any = None,
|
|
315
|
+
ignore_generated=False,
|
|
316
|
+
overrides=False,
|
|
317
|
+
groups: list["ParameterGroup"] = None,
|
|
318
|
+
):
|
|
319
|
+
"""Create a field specification.
|
|
320
|
+
|
|
321
|
+
:param default: Default value (included in identifier computation)
|
|
322
|
+
:param default_factory: Callable that generates the default value
|
|
323
|
+
:param ignore_default: Default value that is ignored in identifier computation
|
|
324
|
+
when the actual value equals this default. Use for backwards-compatible
|
|
325
|
+
behavior with bare default values.
|
|
326
|
+
:param ignore_generated: If True, the generated value is hidden from tasks.
|
|
327
|
+
Useful for adding a field that changes the identifier but won't be used.
|
|
328
|
+
:param overrides: If True, suppress warning when overriding parent parameter
|
|
329
|
+
:param groups: List of ParameterGroup objects for partial identifiers.
|
|
330
|
+
Used with subparameters to compute identifiers that exclude certain groups.
|
|
331
|
+
"""
|
|
188
332
|
assert not (
|
|
189
333
|
(default is not None) and (default_factory is not None)
|
|
190
334
|
), "default and default_factory are mutually exclusive options"
|
|
191
335
|
|
|
336
|
+
assert not (
|
|
337
|
+
(default is not None) and (ignore_default is not None)
|
|
338
|
+
), "default and ignore_default are mutually exclusive options"
|
|
339
|
+
|
|
340
|
+
assert not (
|
|
341
|
+
(ignore_default is not None) and (default_factory is not None)
|
|
342
|
+
), "ignore_default and default_factory are mutually exclusive options"
|
|
343
|
+
|
|
192
344
|
self.default_factory = default_factory
|
|
193
345
|
self.default = default
|
|
346
|
+
self.ignore_default = ignore_default
|
|
347
|
+
self.ignore_generated = ignore_generated
|
|
348
|
+
self.overrides = overrides
|
|
349
|
+
self.groups = set(groups) if groups else set()
|
|
194
350
|
|
|
195
351
|
|
|
196
352
|
class help(TypeAnnotation):
|
|
@@ -201,17 +357,6 @@ class help(TypeAnnotation):
|
|
|
201
357
|
options.kwargs["help"] = self.text
|
|
202
358
|
|
|
203
359
|
|
|
204
|
-
class default(TypeAnnotation):
|
|
205
|
-
"""Adds a default value (useful when we have problems with setattr and class
|
|
206
|
-
properties)"""
|
|
207
|
-
|
|
208
|
-
def __init__(self, value):
|
|
209
|
-
self.value = value
|
|
210
|
-
|
|
211
|
-
def annotate(self, options: ArgumentOptions):
|
|
212
|
-
options.kwargs["default"] = self.value
|
|
213
|
-
|
|
214
|
-
|
|
215
360
|
class ConstantHint(TypeAnnotation):
|
|
216
361
|
def annotate(self, options: ArgumentOptions):
|
|
217
362
|
options.kwargs["constant"] = True
|
|
@@ -219,3 +364,13 @@ class ConstantHint(TypeAnnotation):
|
|
|
219
364
|
|
|
220
365
|
constantHint = ConstantHint()
|
|
221
366
|
Constant = Annotated[T, constantHint]
|
|
367
|
+
"""Type annotation for constant (read-only) parameters.
|
|
368
|
+
|
|
369
|
+
Constants must have a default value and cannot be modified after creation.
|
|
370
|
+
They are included in the identifier computation.
|
|
371
|
+
|
|
372
|
+
Example::
|
|
373
|
+
|
|
374
|
+
class MyConfig(Config):
|
|
375
|
+
version: Constant[str] = "1.0"
|
|
376
|
+
"""
|
experimaestro/core/identifier.py
CHANGED
|
@@ -5,8 +5,11 @@ import hashlib
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import struct
|
|
8
|
-
from typing import Optional
|
|
9
|
-
from experimaestro.core.objects import Config
|
|
8
|
+
from typing import Optional, TYPE_CHECKING
|
|
9
|
+
from experimaestro.core.objects import Config, ConfigMixin
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from experimaestro.core.subparameters import Subparameters
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class ConfigPath:
|
|
@@ -19,6 +22,9 @@ class ConfigPath:
|
|
|
19
22
|
self.config2index = {}
|
|
20
23
|
"""Associate an index in the list with a configuration"""
|
|
21
24
|
|
|
25
|
+
self.instance_order: dict[bytes, list[int]] = {}
|
|
26
|
+
"""Maps InstanceConfig base identifier to list of object ids in encounter order"""
|
|
27
|
+
|
|
22
28
|
def detect_loop(self, config) -> Optional[int]:
|
|
23
29
|
"""If there is a loop, return the relative index and update the path"""
|
|
24
30
|
index = self.config2index.get(id(config), None)
|
|
@@ -100,7 +106,15 @@ class Identifier:
|
|
|
100
106
|
|
|
101
107
|
|
|
102
108
|
class IdentifierComputer:
|
|
103
|
-
"""This class is in charge of computing a config/task identifier
|
|
109
|
+
"""This class is in charge of computing a config/task identifier
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
config: The configuration to compute the identifier for
|
|
113
|
+
config_path: Used to track cycles when computing identifiers
|
|
114
|
+
version: Hash computation version (defaults to XPM_HASH_COMPUTER env var or 2)
|
|
115
|
+
subparameters: If provided, only include parameters that are not excluded
|
|
116
|
+
by this Subparameters instance (for partial identifier computation)
|
|
117
|
+
"""
|
|
104
118
|
|
|
105
119
|
OBJECT_ID = b"\x00"
|
|
106
120
|
INT_ID = b"\x01"
|
|
@@ -115,13 +129,22 @@ class IdentifierComputer:
|
|
|
115
129
|
ENUM_ID = b"\x0a"
|
|
116
130
|
CYCLE_REFERENCE = b"\x0b"
|
|
117
131
|
INIT_TASKS = b"\x0c"
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
INSTANCE_ID = b"\x0d"
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
config: "ConfigMixin",
|
|
137
|
+
config_path: ConfigPath,
|
|
138
|
+
*,
|
|
139
|
+
version=None,
|
|
140
|
+
subparameters: "Subparameters" = None,
|
|
141
|
+
):
|
|
120
142
|
# Hasher for parameters
|
|
121
143
|
self._hasher = hashlib.sha256()
|
|
122
144
|
self.config = config
|
|
123
145
|
self.config_path = config_path
|
|
124
146
|
self.version = version or int(os.environ.get("XPM_HASH_COMPUTER", 2))
|
|
147
|
+
self.subparameters = subparameters
|
|
125
148
|
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
126
149
|
hash_logger.debug(
|
|
127
150
|
"starting hash (%s): %s", hash(str(self.config)), self.config
|
|
@@ -170,7 +193,7 @@ class IdentifierComputer:
|
|
|
170
193
|
self._hashupdate(IdentifierComputer.ENUM_ID)
|
|
171
194
|
k = value.__class__
|
|
172
195
|
self._hashupdate(
|
|
173
|
-
f"{k.__module__}.{k.__qualname__
|
|
196
|
+
f"{k.__module__}.{k.__qualname__}:{value.name}".encode("utf-8"),
|
|
174
197
|
)
|
|
175
198
|
elif isinstance(value, dict):
|
|
176
199
|
self._hashupdate(IdentifierComputer.DICT_ID)
|
|
@@ -183,7 +206,20 @@ class IdentifierComputer:
|
|
|
183
206
|
self.update(value)
|
|
184
207
|
|
|
185
208
|
# Handles configurations
|
|
186
|
-
elif isinstance(value,
|
|
209
|
+
elif isinstance(value, ConfigMixin):
|
|
210
|
+
# For deprecated configs with __convert__, compute identifier from converted config
|
|
211
|
+
# This must happen BEFORE hashing OBJECT_ID to ensure identical identifiers
|
|
212
|
+
if myself:
|
|
213
|
+
xpmtype = value.__xpmtype__
|
|
214
|
+
if xpmtype.deprecated and hasattr(value, "__convert__"):
|
|
215
|
+
hash_logger.debug(
|
|
216
|
+
"Computing hash via __convert__ for deprecated %s", xpmtype
|
|
217
|
+
)
|
|
218
|
+
converted = value.__convert__()
|
|
219
|
+
# Delegate entirely to the converted config
|
|
220
|
+
self.update(converted, myself=True)
|
|
221
|
+
return
|
|
222
|
+
|
|
187
223
|
# Encodes the identifier
|
|
188
224
|
self._hashupdate(IdentifierComputer.OBJECT_ID)
|
|
189
225
|
|
|
@@ -201,6 +237,33 @@ class IdentifierComputer:
|
|
|
201
237
|
)
|
|
202
238
|
self._hashupdate(value_id.all)
|
|
203
239
|
|
|
240
|
+
# Handle InstanceConfig: add instance order only if duplicate detected
|
|
241
|
+
from experimaestro.core.objects import InstanceConfig
|
|
242
|
+
|
|
243
|
+
if isinstance(value, InstanceConfig):
|
|
244
|
+
# Use the base identifier to track instances
|
|
245
|
+
base_id = value_id.all
|
|
246
|
+
obj_id = id(value)
|
|
247
|
+
|
|
248
|
+
# Track instances with this base identifier
|
|
249
|
+
if base_id not in self.config_path.instance_order:
|
|
250
|
+
# First occurrence - just record it (NO-OP for backwards compatibility)
|
|
251
|
+
self.config_path.instance_order[base_id] = [obj_id]
|
|
252
|
+
else:
|
|
253
|
+
# Check if this specific object was already seen
|
|
254
|
+
instances = self.config_path.instance_order[base_id]
|
|
255
|
+
if obj_id not in instances:
|
|
256
|
+
# New instance with same params - add to list
|
|
257
|
+
instances.append(obj_id)
|
|
258
|
+
|
|
259
|
+
# Get position in the list
|
|
260
|
+
position = instances.index(obj_id)
|
|
261
|
+
|
|
262
|
+
# Only add instance order if not the first (position > 0)
|
|
263
|
+
if position > 0:
|
|
264
|
+
self._hashupdate(IdentifierComputer.INSTANCE_ID)
|
|
265
|
+
self._hashupdate(struct.pack("!q", position))
|
|
266
|
+
|
|
204
267
|
# And that's it!
|
|
205
268
|
return
|
|
206
269
|
|
|
@@ -216,6 +279,11 @@ class IdentifierComputer:
|
|
|
216
279
|
# Process arguments (sort by name to ensure uniqueness)
|
|
217
280
|
arguments = sorted(xpmtype.arguments.values(), key=lambda a: a.name)
|
|
218
281
|
for argument in arguments:
|
|
282
|
+
# Skip arguments excluded by subparameters (for partial identifiers)
|
|
283
|
+
if self.subparameters is not None:
|
|
284
|
+
if self.subparameters.is_excluded(argument.groups):
|
|
285
|
+
continue
|
|
286
|
+
|
|
219
287
|
# Ignored argument
|
|
220
288
|
if argument.ignored:
|
|
221
289
|
argvalue = value.__xpm__.values.get(argument.name, None)
|
|
@@ -234,8 +302,18 @@ class IdentifierComputer:
|
|
|
234
302
|
# Argument value
|
|
235
303
|
# Skip if the argument is not a constant, and
|
|
236
304
|
# - optional argument: both value and default are None
|
|
237
|
-
# - the argument value is equal to the default value
|
|
238
|
-
|
|
305
|
+
# - the argument value is equal to the default value AND
|
|
306
|
+
# ignore_default_in_identifier is True
|
|
307
|
+
try:
|
|
308
|
+
argvalue = getattr(value, argument.name, None)
|
|
309
|
+
except KeyError:
|
|
310
|
+
logging.warning(
|
|
311
|
+
"Parameter %s has not been set in %s created at %s",
|
|
312
|
+
argument.name,
|
|
313
|
+
value,
|
|
314
|
+
value.__xpm__._initinfo,
|
|
315
|
+
)
|
|
316
|
+
raise
|
|
239
317
|
if not argument.constant and (
|
|
240
318
|
(
|
|
241
319
|
not argument.required
|
|
@@ -243,7 +321,8 @@ class IdentifierComputer:
|
|
|
243
321
|
and argvalue is None
|
|
244
322
|
)
|
|
245
323
|
or (
|
|
246
|
-
argument.
|
|
324
|
+
argument.ignore_default_in_identifier
|
|
325
|
+
and argument.default is not None
|
|
247
326
|
and argument.default == remove_meta(argvalue)
|
|
248
327
|
)
|
|
249
328
|
):
|
|
@@ -264,12 +343,17 @@ class IdentifierComputer:
|
|
|
264
343
|
self._hashupdate(IdentifierComputer.NAME_ID)
|
|
265
344
|
self.update(argvalue)
|
|
266
345
|
|
|
346
|
+
# Add init tasks
|
|
347
|
+
if value.__xpm__.init_tasks:
|
|
348
|
+
self._hashupdate(IdentifierComputer.INIT_TASKS)
|
|
349
|
+
for init_task in value.__xpm__.init_tasks:
|
|
350
|
+
self.update(init_task)
|
|
267
351
|
else:
|
|
268
352
|
raise NotImplementedError("Cannot compute hash of type %s" % type(value))
|
|
269
353
|
|
|
270
354
|
@staticmethod
|
|
271
355
|
def compute(
|
|
272
|
-
config: "
|
|
356
|
+
config: "ConfigMixin", config_path: ConfigPath | None = None, version=None
|
|
273
357
|
) -> Identifier:
|
|
274
358
|
"""Compute the identifier for a configuration
|
|
275
359
|
|
|
@@ -281,7 +365,7 @@ class IdentifierComputer:
|
|
|
281
365
|
# Try to use the cached value first
|
|
282
366
|
# (if there are no loops)
|
|
283
367
|
if config.__xpm__._sealed:
|
|
284
|
-
identifier = config.__xpm__.
|
|
368
|
+
identifier = config.__xpm__._identifier
|
|
285
369
|
if identifier is not None and not identifier.has_loops:
|
|
286
370
|
return identifier
|
|
287
371
|
|
|
@@ -294,3 +378,34 @@ class IdentifierComputer:
|
|
|
294
378
|
identifier.has_loops = config_path.has_loop()
|
|
295
379
|
|
|
296
380
|
return identifier
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def compute_partial(
|
|
384
|
+
config: "ConfigMixin",
|
|
385
|
+
subparameters: "Subparameters",
|
|
386
|
+
config_path: ConfigPath | None = None,
|
|
387
|
+
version=None,
|
|
388
|
+
) -> Identifier:
|
|
389
|
+
"""Compute a partial identifier for a configuration
|
|
390
|
+
|
|
391
|
+
A partial identifier excludes certain parameter groups, allowing
|
|
392
|
+
configurations that differ only in those groups to share the same
|
|
393
|
+
partial identifier (and thus the same partial directory).
|
|
394
|
+
|
|
395
|
+
:param config: the configuration for which we compute the identifier
|
|
396
|
+
:param subparameters: the Subparameters instance defining which groups
|
|
397
|
+
to include/exclude
|
|
398
|
+
:param config_path: used to track down cycles between configurations
|
|
399
|
+
:param version: version for the hash computation (None for the last one)
|
|
400
|
+
"""
|
|
401
|
+
config_path = config_path or ConfigPath()
|
|
402
|
+
|
|
403
|
+
with config_path.push(config):
|
|
404
|
+
computer = IdentifierComputer(
|
|
405
|
+
config, config_path, version=version, subparameters=subparameters
|
|
406
|
+
)
|
|
407
|
+
computer.update(config, myself=True)
|
|
408
|
+
identifier = computer.identifier()
|
|
409
|
+
identifier.has_loops = config_path.has_loop()
|
|
410
|
+
|
|
411
|
+
return identifier
|
|
@@ -2,8 +2,11 @@ from .config_walk import ConfigWalkContext, ConfigWalk
|
|
|
2
2
|
from .config import (
|
|
3
3
|
ConfigMixin,
|
|
4
4
|
Config,
|
|
5
|
+
InstanceConfig,
|
|
5
6
|
ConfigInformation,
|
|
6
7
|
Task,
|
|
8
|
+
TaskStub,
|
|
9
|
+
ResumableTask,
|
|
7
10
|
LightweightTask,
|
|
8
11
|
WatchedOutput,
|
|
9
12
|
DependentMarker,
|
|
@@ -25,10 +28,13 @@ from .config_utils import (
|
|
|
25
28
|
__all__ = [
|
|
26
29
|
"ConfigMixin",
|
|
27
30
|
"Config",
|
|
31
|
+
"InstanceConfig",
|
|
28
32
|
"ConfigInformation",
|
|
29
33
|
"ConfigWalkContext",
|
|
30
34
|
"ConfigWalk",
|
|
31
35
|
"Task",
|
|
36
|
+
"TaskStub",
|
|
37
|
+
"ResumableTask",
|
|
32
38
|
"LightweightTask",
|
|
33
39
|
"ObjectStore",
|
|
34
40
|
"WatchedOutput",
|