experimaestro 2.0.0a8__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.

Files changed (116) hide show
  1. experimaestro/__init__.py +10 -11
  2. experimaestro/annotations.py +167 -206
  3. experimaestro/cli/__init__.py +130 -5
  4. experimaestro/cli/filter.py +42 -74
  5. experimaestro/cli/jobs.py +157 -106
  6. experimaestro/cli/refactor.py +249 -0
  7. experimaestro/click.py +0 -1
  8. experimaestro/commandline.py +19 -3
  9. experimaestro/connectors/__init__.py +20 -1
  10. experimaestro/connectors/local.py +12 -0
  11. experimaestro/core/arguments.py +182 -46
  12. experimaestro/core/identifier.py +107 -6
  13. experimaestro/core/objects/__init__.py +6 -0
  14. experimaestro/core/objects/config.py +542 -25
  15. experimaestro/core/objects/config_walk.py +20 -0
  16. experimaestro/core/serialization.py +91 -34
  17. experimaestro/core/subparameters.py +164 -0
  18. experimaestro/core/types.py +175 -38
  19. experimaestro/exceptions.py +26 -0
  20. experimaestro/experiments/cli.py +107 -25
  21. experimaestro/generators.py +50 -9
  22. experimaestro/huggingface.py +3 -1
  23. experimaestro/launcherfinder/parser.py +29 -0
  24. experimaestro/launchers/__init__.py +26 -1
  25. experimaestro/launchers/direct.py +12 -0
  26. experimaestro/launchers/slurm/base.py +154 -2
  27. experimaestro/mkdocs/metaloader.py +0 -1
  28. experimaestro/mypy.py +452 -7
  29. experimaestro/notifications.py +63 -13
  30. experimaestro/progress.py +0 -2
  31. experimaestro/rpyc.py +0 -1
  32. experimaestro/run.py +19 -6
  33. experimaestro/scheduler/base.py +489 -125
  34. experimaestro/scheduler/dependencies.py +43 -28
  35. experimaestro/scheduler/dynamic_outputs.py +259 -130
  36. experimaestro/scheduler/experiment.py +225 -30
  37. experimaestro/scheduler/interfaces.py +474 -0
  38. experimaestro/scheduler/jobs.py +216 -206
  39. experimaestro/scheduler/services.py +186 -12
  40. experimaestro/scheduler/state_db.py +388 -0
  41. experimaestro/scheduler/state_provider.py +2345 -0
  42. experimaestro/scheduler/state_sync.py +834 -0
  43. experimaestro/scheduler/workspace.py +52 -10
  44. experimaestro/scriptbuilder.py +7 -0
  45. experimaestro/server/__init__.py +147 -57
  46. experimaestro/server/data/index.css +0 -125
  47. experimaestro/server/data/index.css.map +1 -1
  48. experimaestro/server/data/index.js +194 -58
  49. experimaestro/server/data/index.js.map +1 -1
  50. experimaestro/settings.py +44 -5
  51. experimaestro/sphinx/__init__.py +3 -3
  52. experimaestro/taskglobals.py +20 -0
  53. experimaestro/tests/conftest.py +80 -0
  54. experimaestro/tests/core/test_generics.py +2 -2
  55. experimaestro/tests/identifier_stability.json +45 -0
  56. experimaestro/tests/launchers/bin/sacct +6 -2
  57. experimaestro/tests/launchers/bin/sbatch +4 -2
  58. experimaestro/tests/launchers/test_slurm.py +80 -0
  59. experimaestro/tests/tasks/test_dynamic.py +231 -0
  60. experimaestro/tests/test_cli_jobs.py +615 -0
  61. experimaestro/tests/test_deprecated.py +630 -0
  62. experimaestro/tests/test_environment.py +200 -0
  63. experimaestro/tests/test_file_progress_integration.py +1 -1
  64. experimaestro/tests/test_forward.py +3 -3
  65. experimaestro/tests/test_identifier.py +372 -41
  66. experimaestro/tests/test_identifier_stability.py +458 -0
  67. experimaestro/tests/test_instance.py +3 -3
  68. experimaestro/tests/test_multitoken.py +442 -0
  69. experimaestro/tests/test_mypy.py +433 -0
  70. experimaestro/tests/test_objects.py +312 -5
  71. experimaestro/tests/test_outputs.py +2 -2
  72. experimaestro/tests/test_param.py +8 -12
  73. experimaestro/tests/test_partial_paths.py +231 -0
  74. experimaestro/tests/test_progress.py +0 -48
  75. experimaestro/tests/test_resumable_task.py +480 -0
  76. experimaestro/tests/test_serializers.py +141 -1
  77. experimaestro/tests/test_state_db.py +434 -0
  78. experimaestro/tests/test_subparameters.py +160 -0
  79. experimaestro/tests/test_tags.py +136 -0
  80. experimaestro/tests/test_tasks.py +107 -121
  81. experimaestro/tests/test_token_locking.py +252 -0
  82. experimaestro/tests/test_tokens.py +17 -13
  83. experimaestro/tests/test_types.py +123 -1
  84. experimaestro/tests/test_workspace_triggers.py +158 -0
  85. experimaestro/tests/token_reschedule.py +4 -2
  86. experimaestro/tests/utils.py +2 -2
  87. experimaestro/tokens.py +154 -57
  88. experimaestro/tools/diff.py +1 -1
  89. experimaestro/tui/__init__.py +8 -0
  90. experimaestro/tui/app.py +2303 -0
  91. experimaestro/tui/app.tcss +353 -0
  92. experimaestro/tui/log_viewer.py +228 -0
  93. experimaestro/utils/__init__.py +23 -0
  94. experimaestro/utils/environment.py +148 -0
  95. experimaestro/utils/git.py +129 -0
  96. experimaestro/utils/resources.py +1 -1
  97. experimaestro/version.py +34 -0
  98. {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +68 -38
  99. experimaestro-2.0.0b4.dist-info/RECORD +181 -0
  100. {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
  101. experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
  102. experimaestro/compat.py +0 -6
  103. experimaestro/core/objects.pyi +0 -221
  104. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  105. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  106. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  107. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  108. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  109. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  110. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  111. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  112. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  113. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  114. experimaestro-2.0.0a8.dist-info/RECORD +0 -166
  115. experimaestro-2.0.0a8.dist-info/entry_points.txt +0 -17
  116. {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/licenses/LICENSE +0 -0
@@ -29,6 +29,26 @@ class ConfigWalkContext:
29
29
  return self.path / self._configpath
30
30
  return self.path
31
31
 
32
+ def partial_path(self, subparameters, config) -> Path:
33
+ """Returns the partial directory path for a given subparameters instance.
34
+
35
+ This method should be overridden in subclasses that have access to
36
+ workspace information (like JobContext).
37
+
38
+ Args:
39
+ subparameters: The Subparameters instance defining which groups to exclude
40
+ config: The configuration to compute the partial identifier for
41
+
42
+ Returns:
43
+ The partial directory path.
44
+
45
+ Raises:
46
+ NotImplementedError: If the context doesn't support partial paths.
47
+ """
48
+ raise NotImplementedError(
49
+ "Partial paths require a context with workspace information (like JobContext)"
50
+ )
51
+
32
52
  @contextmanager
33
53
  def push(self, key: str):
34
54
  """Push a new key to contextualize paths"""
@@ -31,10 +31,14 @@ def json_object(context: SerializationContext, value: Any, objects=[]):
31
31
 
32
32
 
33
33
  def state_dict(context: SerializationContext, obj: Any):
34
- """Returns a state dictionary of the object
34
+ """Convert an object to a state dictionary for serialization.
35
+
36
+ Returns a dictionary representation that can be serialized to JSON
37
+ and later restored with :func:`from_state_dict`.
35
38
 
36
39
  :param context: The serialization context
37
- :param obj: the object to serialize
40
+ :param obj: The object to serialize
41
+ :return: A dictionary with 'objects' and 'data' keys
38
42
  """
39
43
  objects: list[Any] = []
40
44
  data = json_object(context, obj, objects)
@@ -48,14 +52,19 @@ def save_definition(obj: Any, context: SerializationContext, path: Path):
48
52
 
49
53
 
50
54
  def save(obj: Any, save_directory: Optional[Path]):
51
- """Saves an object into a disk file
55
+ """Save a configuration to a directory.
56
+
57
+ The serialization process stores the configuration in "definition.json"
58
+ and copies any files or folders registered as DataPath parameters.
59
+
60
+ Example::
52
61
 
53
- The serialization process also stores in the given folder the different
54
- files or folders that are registered as Path parameters (or
55
- meta-parameters).
62
+ config = MyConfig.C(data_path=Path("/data/file.txt"))
63
+ save(config, Path("/output/saved_config"))
56
64
 
65
+ :param obj: The configuration to save
57
66
  :param save_directory: The directory in which the object and its data will
58
- be saved (by default, the object is saved in "definition.json")
67
+ be saved (object is saved in "definition.json")
59
68
  """
60
69
  context = SerializationContext(save_directory=save_directory)
61
70
  save_definition(
@@ -89,19 +98,32 @@ def from_state_dict(
89
98
  state: Dict[str, Any],
90
99
  path: Union[None, str, Path, SerializedPathLoader] = None,
91
100
  *,
92
- as_instance: bool = False
101
+ as_instance: bool = False,
102
+ partial_loading: Optional[bool] = None,
93
103
  ):
94
- """Load an object from a state dictionary
95
-
96
- :param state: The state
97
- :param path: A directory or a function that transforms relative file path
98
- into absolute ones
99
- :param as_instance: returns instances instead of configuration objects
104
+ """Load an object from a state dictionary.
105
+
106
+ Restores a configuration from a dictionary previously created by
107
+ :func:`state_dict`.
108
+
109
+ :param state: The state dictionary to load from
110
+ :param path: Directory containing data files, or a function that resolves
111
+ relative paths to absolute ones
112
+ :param as_instance: If True, return an instance instead of a config
113
+ :param partial_loading: If True, skip loading task references. If None
114
+ (default), partial_loading is enabled when as_instance is True.
115
+ :return: The loaded configuration or instance
100
116
  """
117
+ # Determine effective partial_loading: as_instance implies partial_loading
118
+ effective_partial_loading = (
119
+ partial_loading if partial_loading is not None else as_instance
120
+ )
121
+
101
122
  objects = ConfigInformation.load_objects(
102
123
  state["objects"],
103
124
  as_instance=as_instance,
104
125
  data_loader=get_data_loader(path),
126
+ partial_loading=effective_partial_loading,
105
127
  )
106
128
 
107
129
  return ConfigInformation._objectFromParameters(state["data"], objects)
@@ -110,46 +132,72 @@ def from_state_dict(
110
132
  def load(
111
133
  path: Union[str, Path, SerializedPathLoader],
112
134
  as_instance: bool = False,
135
+ partial_loading: Optional[bool] = None,
113
136
  ) -> Tuple[Any, List["LightweightTask"]]:
114
- """Load data from disk
137
+ """Load a configuration from a directory.
138
+
139
+ Restores a configuration previously saved with :func:`save`.
140
+
141
+ Example::
115
142
 
116
- :param path: A directory or a function that transforms relative file path
117
- into absolute ones
118
- :param as_instance: returns instances instead of configuration objects
143
+ config = load(Path("/output/saved_config"))
144
+
145
+ :param path: Directory containing the saved configuration, or a function
146
+ that resolves relative paths to absolute ones
147
+ :param as_instance: If True, return an instance instead of a config
148
+ :param partial_loading: If True, skip loading task references. If None
149
+ (default), partial_loading is enabled when as_instance is True.
150
+ :return: The loaded configuration or instance
119
151
  """
120
152
  data_loader = get_data_loader(path)
121
153
 
122
154
  with data_loader("definition.json").open("rt") as fh:
123
155
  content = json.load(fh)
124
- return from_state_dict(content, as_instance=as_instance)
156
+ return from_state_dict(
157
+ content, as_instance=as_instance, partial_loading=partial_loading
158
+ )
125
159
 
126
160
 
127
161
  def from_task_dir(
128
162
  path: Union[str, Path, SerializedPathLoader],
129
163
  as_instance: bool = False,
164
+ partial_loading: Optional[bool] = None,
130
165
  ):
131
- """Loads a task object"""
166
+ """Load a task configuration from a task directory.
167
+
168
+ Loads the task parameters from a job directory (containing params.json).
169
+ This is useful for reloading task configurations after execution.
170
+
171
+ :param path: Task directory containing params.json, or a function that
172
+ resolves relative paths to absolute ones
173
+ :param as_instance: If True, return an instance instead of a config
174
+ :param partial_loading: If True, skip loading task references. If None
175
+ (default), partial_loading is enabled when as_instance is True.
176
+ :return: The loaded task configuration or instance
177
+ """
132
178
  data_loader = get_data_loader(path)
133
179
  with data_loader("params.json").open("rt") as fh:
134
180
  content = json.load(fh)
135
181
 
136
182
  content["data"] = {"type": "python", "value": content["objects"][-1]["id"]}
137
183
 
138
- return from_state_dict(content, as_instance=as_instance)
184
+ return from_state_dict(
185
+ content, as_instance=as_instance, partial_loading=partial_loading
186
+ )
139
187
 
140
188
 
141
189
  def serialize(
142
190
  obj: Any, save_directory: Path, *, init_tasks: list["LightweightTask"] = []
143
191
  ):
144
- """Saves an object into a disk file, including initialization tasks
192
+ """Serialize a configuration to a directory with initialization tasks.
145
193
 
146
- The serialization process also stores in the given folder the different
147
- files or folders that are registered as Path parameters (or
148
- meta-parameters).
194
+ Similar to :func:`save`, but also stores lightweight initialization tasks
195
+ that should be run when the configuration is deserialized.
149
196
 
197
+ :param obj: The configuration to serialize
150
198
  :param save_directory: The directory in which the object and its data will
151
- be saved (by default, the object is saved in "definition.json")
152
- :param init_tasks: The optional
199
+ be saved (object is saved in "definition.json")
200
+ :param init_tasks: List of lightweight tasks to run on deserialization
153
201
  """
154
202
  context = SerializationContext(save_directory=save_directory)
155
203
  save_definition((obj, init_tasks), context, save_directory / "definition.json")
@@ -158,20 +206,29 @@ def serialize(
158
206
  def deserialize(
159
207
  path: Union[str, Path, SerializedPathLoader],
160
208
  as_instance: bool = False,
209
+ partial_loading: Optional[bool] = None,
161
210
  ) -> tuple[Any, List["LightweightTask"]] | Any:
162
- """Load data from disk, and initialize the object
163
-
164
- :param path: A directory or a function that transforms relative file path
165
- into absolute ones
166
- :param as_instance: returns instances instead of configuration objects
167
- :returns: either the object (as_instance is true), or a tuple
211
+ """Deserialize a configuration from a directory.
212
+
213
+ Restores a configuration previously saved with :func:`serialize`.
214
+ When ``as_instance=True``, runs any stored initialization tasks.
215
+
216
+ :param path: Directory containing the serialized configuration, or a function
217
+ that resolves relative paths to absolute ones
218
+ :param as_instance: If True, return an instance and run init tasks
219
+ :param partial_loading: If True, skip loading task references. If None
220
+ (default), partial_loading is enabled when as_instance is True.
221
+ :return: The configuration/instance (if as_instance), or tuple of
222
+ (configuration, init_tasks)
168
223
  """
169
224
  data_loader = get_data_loader(path)
170
225
 
171
226
  with data_loader("definition.json").open("rt") as fh:
172
227
  content = json.load(fh)
173
228
 
174
- object, init_tasks = from_state_dict(content, data_loader, as_instance=as_instance)
229
+ object, init_tasks = from_state_dict(
230
+ content, data_loader, as_instance=as_instance, partial_loading=partial_loading
231
+ )
175
232
 
176
233
  if as_instance:
177
234
  for init_task in init_tasks:
@@ -0,0 +1,164 @@
1
+ """Subparameters for partial identifier computation.
2
+
3
+ This module provides the `subparameters` function and `Subparameters` class
4
+ for defining parameter subsets that compute partial identifiers. This enables
5
+ sharing directories (like checkpoints) across tasks that differ only in
6
+ excluded parameter groups.
7
+
8
+ Example:
9
+ iter_group = param_group("iter")
10
+
11
+ class Learn(Task):
12
+ checkpoints = subparameters(exclude_groups=[iter_group])
13
+
14
+ max_iter: Param[int] = field(groups=[iter_group])
15
+ learning_rate: Param[float]
16
+
17
+ # Path will be in WORKSPACE/partials/TASK_ID/checkpoints/PARTIAL_ID/
18
+ checkpoints_path: Param[Path] = field(
19
+ default_factory=PathGenerator(partial=checkpoints)
20
+ )
21
+ """
22
+
23
+ from dataclasses import dataclass, field as dataclass_field
24
+ from typing import Set, Optional
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ParameterGroup:
29
+ """A parameter group with a name
30
+
31
+ The name is just for reference, what is important is the identity of the
32
+ object. The dataclass is frozen to make it hashable.
33
+ """
34
+
35
+ name: str
36
+
37
+
38
+ def param_group(name: str) -> ParameterGroup:
39
+ """Create a parameter group for use with subparameters.
40
+
41
+ Parameter groups allow computing partial identifiers that exclude
42
+ certain parameters, enabling shared directories across related tasks.
43
+
44
+ Example::
45
+
46
+ training_group = param_group("training")
47
+
48
+ class MyTask(Task):
49
+ model_size: Param[int]
50
+ learning_rate: Param[float] = field(groups=[training_group])
51
+
52
+ :param name: Unique name for this parameter group
53
+ :return: A ParameterGroup object
54
+ """
55
+ return ParameterGroup(name)
56
+
57
+
58
+ @dataclass
59
+ class Subparameters:
60
+ """Defines a subset of parameters for partial identifier computation.
61
+
62
+ A Subparameters instance defines which parameter groups to include or exclude
63
+ when computing a partial identifier. This enables sharing directories
64
+ (like checkpoints) across experiments that only differ in excluded groups.
65
+
66
+ The inclusion/exclusion logic follows these rules:
67
+ 1. If `exclude_all` is True, all parameters are excluded by default
68
+ 2. Parameters in `exclude_groups` are excluded
69
+ 3. Parameters with no group are excluded if `exclude_no_group` is True
70
+ 4. Parameters in `include_groups` are always included (overrides exclusion)
71
+
72
+ Attributes:
73
+ exclude_groups: Set of group names to exclude from identifier computation
74
+ include_groups: Set of group names to always include (overrides exclusion)
75
+ exclude_no_group: If True, exclude parameters with no group assigned
76
+ exclude_all: If True, exclude all parameters by default
77
+ name: The name of this parameter set (auto-set from class attribute name)
78
+ """
79
+
80
+ #: Set of group names to exclude from identifier computation
81
+ exclude_groups: Set[ParameterGroup] = dataclass_field(default_factory=set)
82
+
83
+ #: Set of group names to always include (overrides exclusion)
84
+ include_groups: Set[ParameterGroup] = dataclass_field(default_factory=set)
85
+
86
+ #: If True, exclude parameters with no group assigned
87
+ exclude_no_group: bool = False
88
+
89
+ #: If True, exclude all parameters by default (use include_groups to select)
90
+ exclude_all: bool = False
91
+
92
+ #: Name of this parameter set (auto-set from class attribute)
93
+ name: Optional[ParameterGroup] = None
94
+
95
+ def __post_init__(self):
96
+ # Ensure groups are sets
97
+ if not isinstance(self.exclude_groups, set):
98
+ self.exclude_groups = set(self.exclude_groups)
99
+ if not isinstance(self.include_groups, set):
100
+ self.include_groups = set(self.include_groups)
101
+
102
+ def is_excluded(self, groups: Set[ParameterGroup]) -> bool:
103
+ """Check if a parameter with the given groups should be excluded.
104
+
105
+ Args:
106
+ groups: The set of groups the parameter belongs to (empty if no groups).
107
+
108
+ Returns:
109
+ True if the parameter should be excluded from partial identifier.
110
+ """
111
+ # Include always overrides exclude - if any group is in include_groups
112
+ if groups and (groups & self.include_groups):
113
+ return False
114
+
115
+ # Check exclusion rules
116
+ if self.exclude_all:
117
+ return True
118
+ if not groups and self.exclude_no_group:
119
+ return True
120
+ if groups and (groups & self.exclude_groups):
121
+ return True
122
+
123
+ return False
124
+
125
+
126
+ def subparameters(
127
+ *,
128
+ exclude_groups: list[ParameterGroup] | None = None,
129
+ include_groups: list[ParameterGroup] | None = None,
130
+ exclude_no_group: bool = False,
131
+ exclude_all: bool = False,
132
+ ) -> Subparameters:
133
+ """Create a subparameters specification for partial identifier computation.
134
+
135
+ Subparameters allow tasks to share directories when they differ only
136
+ in certain parameter groups (e.g., training hyperparameters).
137
+
138
+ Example::
139
+
140
+ training_group = param_group("training")
141
+
142
+ class Train(Task):
143
+ model: Param[Model]
144
+ epochs: Param[int] = field(groups=[training_group])
145
+
146
+ checkpoint: Meta[Path] = field(
147
+ default_factory=PathGenerator(
148
+ "model.pt",
149
+ subparameters=subparameters(exclude=[training_group])
150
+ )
151
+ )
152
+
153
+ :param exclude_groups: Parameter groups to exclude from identifier
154
+ :param include_groups: Parameter groups to always include (overrides exclusion)
155
+ :param exclude_no_group: If True, exclude parameters with no group assigned
156
+ :param exclude_all: If True, exclude all parameters by default
157
+ :return: A Subparameters object
158
+ """
159
+ return Subparameters(
160
+ exclude_groups=set(exclude_groups or []),
161
+ include_groups=set(include_groups or []),
162
+ exclude_no_group=exclude_no_group,
163
+ exclude_all=exclude_all,
164
+ )