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.

Files changed (133) hide show
  1. experimaestro/__init__.py +10 -11
  2. experimaestro/annotations.py +167 -206
  3. experimaestro/cli/__init__.py +140 -16
  4. experimaestro/cli/filter.py +42 -74
  5. experimaestro/cli/jobs.py +157 -106
  6. experimaestro/cli/progress.py +269 -0
  7. experimaestro/cli/refactor.py +249 -0
  8. experimaestro/click.py +0 -1
  9. experimaestro/commandline.py +19 -3
  10. experimaestro/connectors/__init__.py +22 -3
  11. experimaestro/connectors/local.py +12 -0
  12. experimaestro/core/arguments.py +192 -37
  13. experimaestro/core/identifier.py +127 -12
  14. experimaestro/core/objects/__init__.py +6 -0
  15. experimaestro/core/objects/config.py +702 -285
  16. experimaestro/core/objects/config_walk.py +24 -6
  17. experimaestro/core/serialization.py +91 -34
  18. experimaestro/core/serializers.py +1 -8
  19. experimaestro/core/subparameters.py +164 -0
  20. experimaestro/core/types.py +198 -83
  21. experimaestro/exceptions.py +26 -0
  22. experimaestro/experiments/cli.py +107 -25
  23. experimaestro/generators.py +50 -9
  24. experimaestro/huggingface.py +3 -1
  25. experimaestro/launcherfinder/parser.py +29 -0
  26. experimaestro/launcherfinder/registry.py +3 -3
  27. experimaestro/launchers/__init__.py +26 -1
  28. experimaestro/launchers/direct.py +12 -0
  29. experimaestro/launchers/slurm/base.py +154 -2
  30. experimaestro/mkdocs/base.py +6 -8
  31. experimaestro/mkdocs/metaloader.py +0 -1
  32. experimaestro/mypy.py +452 -7
  33. experimaestro/notifications.py +75 -16
  34. experimaestro/progress.py +404 -0
  35. experimaestro/rpyc.py +0 -1
  36. experimaestro/run.py +19 -6
  37. experimaestro/scheduler/__init__.py +18 -1
  38. experimaestro/scheduler/base.py +504 -959
  39. experimaestro/scheduler/dependencies.py +43 -28
  40. experimaestro/scheduler/dynamic_outputs.py +259 -130
  41. experimaestro/scheduler/experiment.py +582 -0
  42. experimaestro/scheduler/interfaces.py +474 -0
  43. experimaestro/scheduler/jobs.py +485 -0
  44. experimaestro/scheduler/services.py +186 -12
  45. experimaestro/scheduler/signal_handler.py +32 -0
  46. experimaestro/scheduler/state.py +1 -1
  47. experimaestro/scheduler/state_db.py +388 -0
  48. experimaestro/scheduler/state_provider.py +2345 -0
  49. experimaestro/scheduler/state_sync.py +834 -0
  50. experimaestro/scheduler/workspace.py +52 -10
  51. experimaestro/scriptbuilder.py +7 -0
  52. experimaestro/server/__init__.py +153 -32
  53. experimaestro/server/data/index.css +0 -125
  54. experimaestro/server/data/index.css.map +1 -1
  55. experimaestro/server/data/index.js +194 -58
  56. experimaestro/server/data/index.js.map +1 -1
  57. experimaestro/settings.py +47 -6
  58. experimaestro/sphinx/__init__.py +3 -3
  59. experimaestro/taskglobals.py +20 -0
  60. experimaestro/tests/conftest.py +80 -0
  61. experimaestro/tests/core/test_generics.py +2 -2
  62. experimaestro/tests/identifier_stability.json +45 -0
  63. experimaestro/tests/launchers/bin/sacct +6 -2
  64. experimaestro/tests/launchers/bin/sbatch +4 -2
  65. experimaestro/tests/launchers/common.py +2 -2
  66. experimaestro/tests/launchers/test_slurm.py +80 -0
  67. experimaestro/tests/restart.py +1 -1
  68. experimaestro/tests/tasks/all.py +7 -0
  69. experimaestro/tests/tasks/test_dynamic.py +231 -0
  70. experimaestro/tests/test_checkers.py +2 -2
  71. experimaestro/tests/test_cli_jobs.py +615 -0
  72. experimaestro/tests/test_dependencies.py +11 -17
  73. experimaestro/tests/test_deprecated.py +630 -0
  74. experimaestro/tests/test_environment.py +200 -0
  75. experimaestro/tests/test_experiment.py +3 -3
  76. experimaestro/tests/test_file_progress.py +425 -0
  77. experimaestro/tests/test_file_progress_integration.py +477 -0
  78. experimaestro/tests/test_forward.py +3 -3
  79. experimaestro/tests/test_generators.py +93 -0
  80. experimaestro/tests/test_identifier.py +520 -169
  81. experimaestro/tests/test_identifier_stability.py +458 -0
  82. experimaestro/tests/test_instance.py +16 -21
  83. experimaestro/tests/test_multitoken.py +442 -0
  84. experimaestro/tests/test_mypy.py +433 -0
  85. experimaestro/tests/test_objects.py +314 -30
  86. experimaestro/tests/test_outputs.py +8 -8
  87. experimaestro/tests/test_param.py +22 -26
  88. experimaestro/tests/test_partial_paths.py +231 -0
  89. experimaestro/tests/test_progress.py +2 -50
  90. experimaestro/tests/test_resumable_task.py +480 -0
  91. experimaestro/tests/test_serializers.py +141 -60
  92. experimaestro/tests/test_state_db.py +434 -0
  93. experimaestro/tests/test_subparameters.py +160 -0
  94. experimaestro/tests/test_tags.py +151 -15
  95. experimaestro/tests/test_tasks.py +137 -160
  96. experimaestro/tests/test_token_locking.py +252 -0
  97. experimaestro/tests/test_tokens.py +25 -19
  98. experimaestro/tests/test_types.py +133 -11
  99. experimaestro/tests/test_validation.py +19 -19
  100. experimaestro/tests/test_workspace_triggers.py +158 -0
  101. experimaestro/tests/token_reschedule.py +5 -3
  102. experimaestro/tests/utils.py +2 -2
  103. experimaestro/tokens.py +154 -57
  104. experimaestro/tools/diff.py +8 -1
  105. experimaestro/tui/__init__.py +8 -0
  106. experimaestro/tui/app.py +2303 -0
  107. experimaestro/tui/app.tcss +353 -0
  108. experimaestro/tui/log_viewer.py +228 -0
  109. experimaestro/typingutils.py +11 -2
  110. experimaestro/utils/__init__.py +23 -0
  111. experimaestro/utils/environment.py +148 -0
  112. experimaestro/utils/git.py +129 -0
  113. experimaestro/utils/resources.py +1 -1
  114. experimaestro/version.py +34 -0
  115. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
  116. experimaestro-2.0.0b4.dist-info/RECORD +181 -0
  117. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
  118. experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
  119. experimaestro/compat.py +0 -6
  120. experimaestro/core/objects.pyi +0 -225
  121. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  122. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  123. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  124. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  125. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  126. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  127. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  128. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  129. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  130. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  131. experimaestro-1.11.1.dist-info/RECORD +0 -158
  132. experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
  133. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
@@ -5,8 +5,8 @@ from pathlib import Path
5
5
  from typing import Dict, List, Optional
6
6
  from experimaestro import (
7
7
  Param,
8
- deprecate,
9
8
  Config,
9
+ InstanceConfig,
10
10
  Constant,
11
11
  Meta,
12
12
  Option,
@@ -14,6 +14,8 @@ from experimaestro import (
14
14
  field,
15
15
  Task,
16
16
  LightweightTask,
17
+ subparameters,
18
+ param_group,
17
19
  )
18
20
  from experimaestro.core.objects import (
19
21
  ConfigInformation,
@@ -34,7 +36,7 @@ class B(Config):
34
36
 
35
37
 
36
38
  class C(Config):
37
- a: Param[int] = 1
39
+ a: Param[int] = field(ignore_default=1)
38
40
  b: Param[int]
39
41
 
40
42
 
@@ -68,42 +70,42 @@ def assert_notequal(a, b, message=""):
68
70
  assert getidentifier(a) != getidentifier(b), message
69
71
 
70
72
 
71
- def test_param_int():
72
- assert_equal(A(a=1), A(a=1))
73
+ def test_identifier_int():
74
+ assert_equal(A.C(a=1), A.C(a=1))
73
75
 
74
76
 
75
- def test_param_different_type():
76
- assert_notequal(A(a=1), B(a=1))
77
+ def test_identifier_different_type():
78
+ assert_notequal(A.C(a=1), B.C(a=1))
77
79
 
78
80
 
79
- def test_param_order():
80
- assert_equal(Values(value1=1, value2=2), Values(value2=2, value1=1))
81
+ def test_identifier_order():
82
+ assert_equal(Values.C(value1=1, value2=2), Values.C(value2=2, value1=1))
81
83
 
82
84
 
83
- def test_param_default():
84
- assert_equal(C(a=1, b=2), C(b=2))
85
+ def test_identifier_default():
86
+ assert_equal(C.C(a=1, b=2), C.C(b=2))
85
87
 
86
88
 
87
89
  def test_identifier_default_field():
88
- assert_equal(CField(a=1, b=2), CField(b=2))
90
+ assert_equal(CField.C(a=1, b=2), CField.C(b=2))
89
91
 
90
92
 
91
- def test_param_inner_eq():
92
- assert_equal(D(a=A(a=1)), D(a=A(a=1)))
93
+ def test_identifier_inner_eq():
94
+ assert_equal(D.C(a=A.C(a=1)), D.C(a=A.C(a=1)))
93
95
 
94
96
 
95
- def test_param_float():
96
- assert_equal(Float(value=1), Float(value=1))
97
+ def test_identifier_float():
98
+ assert_equal(Float.C(value=1), Float.C(value=1))
97
99
 
98
100
 
99
- def test_param_float2():
100
- assert_equal(Float(value=1.0), Float(value=1))
101
+ def test_identifier_float2():
102
+ assert_equal(Float.C(value=1.0), Float.C(value=1))
101
103
 
102
104
 
103
105
  # --- Argument name
104
106
 
105
107
 
106
- def test_param_name():
108
+ def test_identifier_name():
107
109
  """The identifier fully determines the hash code"""
108
110
 
109
111
  class Config0(Config):
@@ -118,28 +120,28 @@ def test_param_name():
118
120
  __xpmid__ = "test.identifier.argumentname"
119
121
  a: Param[int]
120
122
 
121
- assert_notequal(Config0(a=2), Config1(b=2))
122
- assert_equal(Config0(a=2), Config3(a=2))
123
+ assert_notequal(Config0.C(a=2), Config1.C(b=2))
124
+ assert_equal(Config0.C(a=2), Config3.C(a=2))
123
125
 
124
126
 
125
127
  # --- Test option
126
128
 
127
129
 
128
- def test_param_option():
130
+ def test_identifier_option():
129
131
  class OptionConfig(Config):
130
132
  __xpmid__ = "test.identifier.option"
131
133
  a: Param[int]
132
- b: Option[int] = 1
134
+ b: Option[int] = field(ignore_default=1)
133
135
 
134
- assert_notequal(OptionConfig(a=2), OptionConfig(a=1))
135
- assert_equal(OptionConfig(a=1, b=2), OptionConfig(a=1))
136
- assert_equal(OptionConfig(a=1, b=2), OptionConfig(a=1, b=2))
136
+ assert_notequal(OptionConfig.C(a=2), OptionConfig.C(a=1))
137
+ assert_equal(OptionConfig.C(a=1, b=2), OptionConfig.C(a=1))
138
+ assert_equal(OptionConfig.C(a=1, b=2), OptionConfig.C(a=1, b=2))
137
139
 
138
140
 
139
141
  # --- Dictionnary
140
142
 
141
143
 
142
- def test_param_identifier_dict():
144
+ def test_identifier_dict():
143
145
  """Test identifiers of dictionary structures"""
144
146
 
145
147
  class B(Config):
@@ -148,11 +150,14 @@ def test_param_identifier_dict():
148
150
  class A(Config):
149
151
  bs: Param[Dict[str, B]]
150
152
 
151
- assert_equal(A(bs={"b1": B(x=1)}), A(bs={"b1": B(x=1)}))
152
- assert_equal(A(bs={"b1": B(x=1), "b2": B(x=2)}), A(bs={"b2": B(x=2), "b1": B(x=1)}))
153
+ assert_equal(A.C(bs={"b1": B.C(x=1)}), A.C(bs={"b1": B.C(x=1)}))
154
+ assert_equal(
155
+ A.C(bs={"b1": B.C(x=1), "b2": B.C(x=2)}),
156
+ A.C(bs={"b2": B.C(x=2), "b1": B.C(x=1)}),
157
+ )
153
158
 
154
- assert_notequal(A(bs={"b1": B(x=1)}), A(bs={"b1": B(x=2)}))
155
- assert_notequal(A(bs={"b1": B(x=1)}), A(bs={"b2": B(x=1)}))
159
+ assert_notequal(A.C(bs={"b1": B.C(x=1)}), A.C(bs={"b1": B.C(x=2)}))
160
+ assert_notequal(A.C(bs={"b1": B.C(x=1)}), A.C(bs={"b2": B.C(x=1)}))
156
161
 
157
162
 
158
163
  # --- Ignore paths
@@ -163,16 +168,16 @@ class TypeWithPath(Config):
163
168
  path: Param[Path]
164
169
 
165
170
 
166
- def test_param_identifier_path():
171
+ def test_identifier_path():
167
172
  """Path should be ignored"""
168
- assert_equal(TypeWithPath(a=1, path="/a/b"), TypeWithPath(a=1, path="/c/d"))
169
- assert_notequal(TypeWithPath(a=2, path="/a/b"), TypeWithPath(a=1, path="/c/d"))
173
+ assert_equal(TypeWithPath.C(a=1, path="/a/b"), TypeWithPath.C(a=1, path="/c/d"))
174
+ assert_notequal(TypeWithPath.C(a=2, path="/a/b"), TypeWithPath.C(a=1, path="/c/d"))
170
175
 
171
176
 
172
177
  # --- Test with added arguments
173
178
 
174
179
 
175
- def test_param_identifier_pathoption():
180
+ def test_identifier_pathoption():
176
181
  """Path arguments should be ignored"""
177
182
 
178
183
  class A_with_path(Config):
@@ -184,10 +189,10 @@ def test_param_identifier_pathoption():
184
189
  __xpmid__ = "pathoption_test"
185
190
  a: Param[int]
186
191
 
187
- assert_equal(A_with_path(a=1), A_without_path(a=1))
192
+ assert_equal(A_with_path.C(a=1), A_without_path.C(a=1))
188
193
 
189
194
 
190
- def test_param_identifier_enum():
195
+ def test_identifier_enum():
191
196
  """test enum parameters"""
192
197
  from enum import Enum
193
198
 
@@ -198,11 +203,11 @@ def test_param_identifier_enum():
198
203
  class EnumConfig(Config):
199
204
  a: Param[EnumParam]
200
205
 
201
- assert_notequal(EnumConfig(a=EnumParam.FIRST), EnumConfig(a=EnumParam.SECOND))
202
- assert_equal(EnumConfig(a=EnumParam.FIRST), EnumConfig(a=EnumParam.FIRST))
206
+ assert_notequal(EnumConfig.C(a=EnumParam.FIRST), EnumConfig.C(a=EnumParam.SECOND))
207
+ assert_equal(EnumConfig.C(a=EnumParam.FIRST), EnumConfig.C(a=EnumParam.FIRST))
203
208
 
204
209
 
205
- def test_param_identifier_addnone():
210
+ def test_identifier_addnone():
206
211
  """Test the case of new parameter (with None default)"""
207
212
 
208
213
  class B(Config):
@@ -215,28 +220,28 @@ def test_param_identifier_addnone():
215
220
  class A(Config):
216
221
  __xpmid__ = "defaultnone"
217
222
 
218
- assert_equal(A_with_b(), A())
219
- assert_notequal(A_with_b(b=B(x=1)), A())
223
+ assert_equal(A_with_b.C(), A.C())
224
+ assert_notequal(A_with_b.C(b=B.C(x=1)), A.C())
220
225
 
221
226
 
222
- def test_param_defaultnew():
227
+ def test_identifier_defaultnew():
223
228
  """Path arguments should be ignored"""
224
229
 
225
230
  class A_with_b(Config):
226
231
  __xpmid__ = "defaultnew"
227
232
 
228
233
  a: Param[int]
229
- b: Param[int] = 1
234
+ b: Param[int] = field(ignore_default=1)
230
235
 
231
236
  class A(Config):
232
237
  __xpmid__ = "defaultnew"
233
238
  a: Param[int]
234
239
 
235
- assert_equal(A_with_b(a=1, b=1), A(a=1))
236
- assert_equal(A_with_b(a=1), A(a=1))
240
+ assert_equal(A_with_b.C(a=1, b=1), A.C(a=1))
241
+ assert_equal(A_with_b.C(a=1), A.C(a=1))
237
242
 
238
243
 
239
- def test_param_taskconfigidentifier():
244
+ def test_identifier_taskconfigidentifier():
240
245
  """Test whether the embedded task arguments make the configuration different"""
241
246
 
242
247
  class MyConfig(Config):
@@ -246,19 +251,19 @@ def test_param_taskconfigidentifier():
246
251
  x: Param[int]
247
252
 
248
253
  def task_outputs(self, dep):
249
- return dep(MyConfig(a=1))
254
+ return dep(MyConfig.C(a=1))
250
255
 
251
256
  assert_equal(
252
- MyTask(x=1).submit(run_mode=RunMode.DRY_RUN),
253
- MyTask(x=1).submit(run_mode=RunMode.DRY_RUN),
257
+ MyTask.C(x=1).submit(run_mode=RunMode.DRY_RUN),
258
+ MyTask.C(x=1).submit(run_mode=RunMode.DRY_RUN),
254
259
  )
255
260
  assert_notequal(
256
- MyTask(x=2).submit(run_mode=RunMode.DRY_RUN),
257
- MyTask(x=1).submit(run_mode=RunMode.DRY_RUN),
261
+ MyTask.C(x=2).submit(run_mode=RunMode.DRY_RUN),
262
+ MyTask.C(x=1).submit(run_mode=RunMode.DRY_RUN),
258
263
  )
259
264
 
260
265
 
261
- def test_param_constant():
266
+ def test_identifier_constant():
262
267
  """Test if constants are taken into account for signature computation"""
263
268
 
264
269
  class A1(Config):
@@ -269,53 +274,20 @@ def test_param_constant():
269
274
  __xpmid__ = "test.constant"
270
275
  version: Constant[int] = 1
271
276
 
272
- assert_equal(A1(), A1bis())
277
+ assert_equal(A1.C(), A1bis.C())
273
278
 
274
279
  class A2(Config):
275
280
  __xpmid__ = "test.constant"
276
281
  version: Constant[int] = 2
277
282
 
278
- assert_notequal(A1(), A2())
279
-
280
-
281
- def test_param_identifier_deprecated_class():
282
- """Test that when submitting the task, the computed identifier is the one of
283
- the new class"""
284
-
285
- class NewConfig(Config):
286
- __xpmid__ = "new"
287
-
288
- @deprecate
289
- class OldConfig(NewConfig):
290
- __xpmid__ = "old"
291
-
292
- class DerivedConfig(NewConfig):
293
- __xpmid__ = "derived"
294
-
295
- assert_notequal(
296
- NewConfig(), DerivedConfig(), "A derived configuration has another ID"
297
- )
298
- assert_equal(
299
- NewConfig(), OldConfig(), "Deprecated and new configuration have the same ID"
300
- )
301
-
302
-
303
- def test_param_identifier_deprecated_attribute():
304
- class Values(Config):
305
- values: Param[List[int]] = []
306
-
307
- @deprecate
308
- def value(self, x):
309
- self.values = [x]
310
-
311
- assert_equal(Values(values=[1]), Values(value=1))
283
+ assert_notequal(A1.C(), A2.C())
312
284
 
313
285
 
314
286
  class MetaA(Config):
315
287
  x: Param[int]
316
288
 
317
289
 
318
- def test_param_identifier_meta():
290
+ def test_identifier_meta():
319
291
  """Test forced meta-parameter"""
320
292
 
321
293
  class B(Config):
@@ -331,121 +303,136 @@ def test_param_identifier_meta():
331
303
  params: Param[Dict[str, MetaA]]
332
304
 
333
305
  # As meta
334
- assert_notequal(B(a=MetaA(x=1)), B(a=MetaA(x=2)))
335
- assert_equal(B(a=setmeta(MetaA(x=1), True)), B(a=setmeta(MetaA(x=2), True)))
306
+ assert_notequal(B.C(a=MetaA.C(x=1)), B.C(a=MetaA.C(x=2)))
307
+ assert_equal(B.C(a=setmeta(MetaA.C(x=1), True)), B.C(a=setmeta(MetaA.C(x=2), True)))
336
308
 
337
309
  # As parameter
338
- assert_equal(C(a=MetaA(x=1)), C(a=MetaA(x=2)))
339
- assert_notequal(C(a=setmeta(MetaA(x=1), False)), C(a=setmeta(MetaA(x=2), False)))
310
+ assert_equal(C.C(a=MetaA.C(x=1)), C.C(a=MetaA.C(x=2)))
311
+ assert_notequal(
312
+ C.C(a=setmeta(MetaA.C(x=1), False)), C.C(a=setmeta(MetaA.C(x=2), False))
313
+ )
340
314
 
341
315
  # Array with mixed
342
316
  assert_equal(
343
- ArrayConfig(array=[MetaA(x=1)]),
344
- ArrayConfig(array=[MetaA(x=1), setmeta(MetaA(x=2), True)]),
317
+ ArrayConfig.C(array=[MetaA.C(x=1)]),
318
+ ArrayConfig.C(array=[MetaA.C(x=1), setmeta(MetaA.C(x=2), True)]),
345
319
  )
346
320
 
347
321
  # Array with empty list
348
- assert_equal(ArrayConfig(array=[]), ArrayConfig(array=[setmeta(MetaA(x=2), True)]))
322
+ assert_equal(
323
+ ArrayConfig.C(array=[]), ArrayConfig.C(array=[setmeta(MetaA.C(x=2), True)])
324
+ )
349
325
 
350
326
  # Dict with mixed
351
327
  assert_equal(
352
- DictConfig(params={"a": MetaA(x=1)}),
353
- DictConfig(params={"a": MetaA(x=1), "b": setmeta(MetaA(x=2), True)}),
328
+ DictConfig.C(params={"a": MetaA.C(x=1)}),
329
+ DictConfig.C(params={"a": MetaA.C(x=1), "b": setmeta(MetaA.C(x=2), True)}),
354
330
  )
355
331
 
356
332
 
357
- def test_param_identifier_meta_default_dict():
333
+ def test_identifier_meta_default_dict():
358
334
  class DictConfig(Config):
359
- params: Param[Dict[str, MetaA]] = {}
335
+ params: Param[Dict[str, MetaA]] = field(ignore_default={})
360
336
 
361
337
  assert_equal(
362
- DictConfig(params={}),
363
- DictConfig(params={"b": setmeta(MetaA(x=2), True)}),
338
+ DictConfig.C(params={}),
339
+ DictConfig.C(params={"b": setmeta(MetaA.C(x=2), True)}),
364
340
  )
365
341
 
366
342
  # Dict with mixed
367
343
  assert_equal(
368
- DictConfig(params={"a": MetaA(x=1)}),
369
- DictConfig(params={"a": MetaA(x=1), "b": setmeta(MetaA(x=2), True)}),
344
+ DictConfig.C(params={"a": MetaA.C(x=1)}),
345
+ DictConfig.C(params={"a": MetaA.C(x=1), "b": setmeta(MetaA.C(x=2), True)}),
370
346
  )
371
347
 
372
348
 
373
- def test_param_identifier_meta_default_array():
349
+ def test_identifier_meta_default_array():
374
350
  class ArrayConfigWithDefault(Config):
375
- array: Param[List[MetaA]] = []
351
+ array: Param[List[MetaA]] = field(ignore_default=[])
376
352
 
377
353
  # Array (with default) with mixed
378
354
  assert_equal(
379
- ArrayConfigWithDefault(array=[MetaA(x=1)]),
380
- ArrayConfigWithDefault(array=[MetaA(x=1), setmeta(MetaA(x=2), True)]),
355
+ ArrayConfigWithDefault.C(array=[MetaA.C(x=1)]),
356
+ ArrayConfigWithDefault.C(array=[MetaA.C(x=1), setmeta(MetaA.C(x=2), True)]),
381
357
  )
382
358
  # Array (with default) with empty list
383
359
  assert_equal(
384
- ArrayConfigWithDefault(array=[]),
385
- ArrayConfigWithDefault(array=[setmeta(MetaA(x=2), True)]),
360
+ ArrayConfigWithDefault.C(array=[]),
361
+ ArrayConfigWithDefault.C(array=[setmeta(MetaA.C(x=2), True)]),
386
362
  )
387
363
 
388
364
 
389
- def test_param_identifier_pre_task():
365
+ def test_identifier_init_task():
390
366
  class MyConfig(Config):
391
367
  pass
392
368
 
393
- class IdentifierPreLightTask(LightweightTask):
369
+ class IdentifierInitTask(LightweightTask):
370
+ pass
371
+
372
+ class IdentifierInitTask2(Task):
394
373
  pass
395
374
 
396
- class IdentifierPreTask(Task):
375
+ class IdentifierTask(Task):
397
376
  x: Param[MyConfig]
398
377
 
399
- task = IdentifierPreTask(x=MyConfig()).submit(run_mode=RunMode.DRY_RUN)
400
- task_with_pre = (
401
- IdentifierPreTask(x=MyConfig())
402
- .add_pretasks(IdentifierPreLightTask())
403
- .submit(run_mode=RunMode.DRY_RUN)
378
+ task = IdentifierTask.C(x=MyConfig.C()).submit(run_mode=RunMode.DRY_RUN)
379
+ task_with_pre = IdentifierTask.C(x=MyConfig.C()).submit(
380
+ run_mode=RunMode.DRY_RUN,
381
+ init_tasks=[IdentifierInitTask.C(), IdentifierInitTask2.C()],
404
382
  )
405
- task_with_pre_2 = (
406
- IdentifierPreTask(x=MyConfig())
407
- .add_pretasks(IdentifierPreLightTask())
408
- .submit(run_mode=RunMode.DRY_RUN)
383
+ task_with_pre_2 = IdentifierTask.C(x=MyConfig.C()).submit(
384
+ run_mode=RunMode.DRY_RUN,
385
+ init_tasks=[IdentifierInitTask.C(), IdentifierInitTask2.C()],
386
+ )
387
+ task_with_pre_3 = IdentifierTask.C(x=MyConfig.C()).submit(
388
+ run_mode=RunMode.DRY_RUN,
389
+ init_tasks=[IdentifierInitTask2.C(), IdentifierInitTask.C()],
409
390
  )
410
- task_with_pre_3 = IdentifierPreTask(
411
- x=MyConfig().add_pretasks(IdentifierPreLightTask())
412
- ).submit(run_mode=RunMode.DRY_RUN)
413
391
 
414
- assert_notequal(task, task_with_pre, "No pre-task")
392
+ assert_notequal(task, task_with_pre, "Should be different with init-task")
415
393
  assert_equal(task_with_pre, task_with_pre_2, "Same parameters")
416
- assert_equal(task_with_pre, task_with_pre_3, "Pre-tasks are order-less")
394
+ assert_notequal(task_with_pre, task_with_pre_3, "Other parameters")
417
395
 
418
396
 
419
- def test_param_identifier_init_task():
420
- class MyConfig(Config):
421
- pass
397
+ def test_identifier_init_task_dep():
398
+ class Loader(LightweightTask):
399
+ param1: Param[float]
422
400
 
423
- class IdentifierInitTask(LightweightTask):
424
- pass
401
+ def execute(self):
402
+ pass
425
403
 
426
- class IdentifierInitTask2(Task):
427
- pass
404
+ class FirstTask(Task):
405
+ def task_outputs(self, dep):
406
+ return dep(Loader.C(param1=1))
428
407
 
429
- class IdentierTask(Task):
430
- x: Param[MyConfig]
408
+ def execute(self):
409
+ pass
431
410
 
432
- task = IdentierTask(x=MyConfig()).submit(run_mode=RunMode.DRY_RUN)
433
- task_with_pre = IdentierTask(x=MyConfig()).submit(
434
- run_mode=RunMode.DRY_RUN,
435
- init_tasks=[IdentifierInitTask(), IdentifierInitTask2()],
436
- )
437
- task_with_pre_2 = IdentierTask(x=MyConfig()).submit(
438
- run_mode=RunMode.DRY_RUN,
439
- init_tasks=[IdentifierInitTask(), IdentifierInitTask2()],
411
+ class SecondTask(Task):
412
+ param3: Param[int]
413
+
414
+ def execute(self):
415
+ pass
416
+
417
+ # Two identical tasks
418
+ task_a_1 = FirstTask.C()
419
+ task_a_2 = FirstTask.C()
420
+ assert_equal(task_a_1, task_a_2)
421
+
422
+ # We process them with two different init tasks
423
+ loader_1 = task_a_1.submit(
424
+ init_tasks=[Loader.C(param1=0.5)], run_mode=RunMode.DRY_RUN
440
425
  )
441
- task_with_pre_3 = IdentierTask(x=MyConfig()).submit(
442
- run_mode=RunMode.DRY_RUN,
443
- init_tasks=[IdentifierInitTask2(), IdentifierInitTask()],
426
+ loader_2 = task_a_2.submit(
427
+ init_tasks=[Loader.C(param1=5)], run_mode=RunMode.DRY_RUN
444
428
  )
429
+ assert_notequal(loader_1, loader_2)
445
430
 
446
- assert_notequal(task, task_with_pre, "No pre-task")
447
- assert_equal(task_with_pre, task_with_pre_2, "Same parameters")
448
- assert_notequal(task_with_pre, task_with_pre_3, "Same parameters")
431
+ # Now, we process
432
+ c_1 = SecondTask.C(param3=2).submit(init_tasks=[loader_1], run_mode=RunMode.DRY_RUN)
433
+
434
+ c_2 = SecondTask.C(param3=2).submit(init_tasks=[loader_2], run_mode=RunMode.DRY_RUN)
435
+ assert_notequal(c_1, c_2)
449
436
 
450
437
 
451
438
  # --- Check configuration reloads
@@ -463,7 +450,7 @@ def check_reload(config):
463
450
  new_config = ConfigInformation.fromParameters(
464
451
  data, as_instance=False, discard_id=True
465
452
  )
466
- assert new_config.__xpm__._full_identifier is None
453
+ assert new_config.__xpm__._identifier is None
467
454
  new_identifier = new_config.__xpm__.identifier.all
468
455
 
469
456
  assert new_identifier == old_identifier
@@ -473,28 +460,28 @@ class IdentifierReloadConfig(Config):
473
460
  id: Param[str]
474
461
 
475
462
 
476
- def test_param_identifier_reload_config():
463
+ def test_identifier_reload_config():
477
464
  # Creates the configuration
478
- check_reload(IdentifierReloadConfig(id="123"))
465
+ check_reload(IdentifierReloadConfig.C(id="123"))
479
466
 
480
467
 
481
468
  class IdentifierReload(Task):
482
469
  id: Param[str]
483
470
 
484
- def task_outputs(self, dep):
485
- return IdentifierReloadConfig(id=self.id)
471
+ def task_outputs(self, dep) -> IdentifierReloadConfig.C:
472
+ return IdentifierReloadConfig.C(id=self.id)
486
473
 
487
474
 
488
475
  class IdentifierReloadDerived(Config):
489
476
  task: Param[IdentifierReloadConfig]
490
477
 
491
478
 
492
- def test_param_identifier_reload_taskoutput():
479
+ def test_identifier_reload_taskoutput():
493
480
  """When using a task output, the identifier should not be different"""
494
481
 
495
482
  # Creates the configuration
496
- task = IdentifierReload(id="123").submit(run_mode=RunMode.DRY_RUN)
497
- config = IdentifierReloadDerived(task=task)
483
+ task = IdentifierReload.C(id="123").submit(run_mode=RunMode.DRY_RUN)
484
+ config = IdentifierReloadDerived.C(task=task)
498
485
  check_reload(config)
499
486
 
500
487
 
@@ -511,23 +498,23 @@ class IdentifierReloadTaskDerived(Config):
511
498
  other: Param[IdentifierReloadTaskConfig]
512
499
 
513
500
 
514
- def test_param_identifier_reload_task_direct():
501
+ def test_identifier_reload_task_direct():
515
502
  """When using a direct task output, the identifier should not be different"""
516
503
 
517
504
  # Creates the configuration
518
- task = IdentifierReloadTask(id="123").submit(run_mode=RunMode.DRY_RUN)
519
- config = IdentifierReloadTaskDerived(
520
- task=task, other=IdentifierReloadTaskConfig(x=2)
505
+ task = IdentifierReloadTask.C(id="123").submit(run_mode=RunMode.DRY_RUN)
506
+ config = IdentifierReloadTaskDerived.C(
507
+ task=task, other=IdentifierReloadTaskConfig.C(x=2)
521
508
  )
522
509
  check_reload(config)
523
510
 
524
511
 
525
- def test_param_identifier_reload_meta():
512
+ def test_identifier_reload_meta():
526
513
  """Test identifier don't change when using meta"""
527
514
  # Creates the configuration
528
- task = IdentifierReloadTask(id="123").submit(run_mode=RunMode.DRY_RUN)
529
- config = IdentifierReloadTaskDerived(
530
- task=task, other=setmeta(IdentifierReloadTaskConfig(x=2), True)
515
+ task = IdentifierReloadTask.C(id="123").submit(run_mode=RunMode.DRY_RUN)
516
+ config = IdentifierReloadTaskDerived.C(
517
+ task=task, other=setmeta(IdentifierReloadTaskConfig.C(x=2), True)
531
518
  )
532
519
  check_reload(config)
533
520
 
@@ -545,10 +532,10 @@ class LoopC(Config):
545
532
  param_b: Param["LoopB"]
546
533
 
547
534
 
548
- def test_param_identifier_loop():
549
- c = LoopC()
550
- b = LoopB(param_c=c)
551
- a = LoopA(param_b=b)
535
+ def test_identifier_loop():
536
+ c = LoopC.C()
537
+ b = LoopB.C(param_c=c)
538
+ a = LoopA.C(param_b=b)
552
539
  c.param_a = a
553
540
  c.param_b = b
554
541
 
@@ -570,3 +557,367 @@ def test_param_identifier_loop():
570
557
  for i in range(len(configs)):
571
558
  for j in range(1, len(configs)):
572
559
  assert identifiers[i][0] == identifiers[i][j]
560
+
561
+
562
+ # --- Test InstanceConfig
563
+
564
+
565
+ class SubModel(InstanceConfig):
566
+ """Test InstanceConfig - instances are distinguished even with same params"""
567
+
568
+ pass
569
+
570
+
571
+ class SubModelAsConfig(Config):
572
+ """Same as SubModel but as regular Config for backwards compat testing"""
573
+
574
+ __xpmid__ = "test.SubModel"
575
+ pass
576
+
577
+
578
+ class Model(Config):
579
+ """Model that can contain SubModel instances"""
580
+
581
+ m1: Param[SubModel]
582
+ m2: Param[SubModel]
583
+
584
+
585
+ class ModelWithRegularConfig(Config):
586
+ """Model using regular Config instead of InstanceConfig"""
587
+
588
+ __xpmid__ = "test.Model"
589
+ m1: Param[SubModelAsConfig]
590
+ m2: Param[SubModelAsConfig]
591
+
592
+
593
+ def test_instanceconfig_backwards_compat():
594
+ """Model using single InstanceConfig should have same ID as with regular Config"""
595
+ # Using InstanceConfig (first occurrence only, no instance marker added)
596
+ sm1 = SubModel.C()
597
+ sm1.__xpmtype__.identifier.name = "test.SubModel" # Match the __xpmid__
598
+ m_instance = Model.C(m1=sm1, m2=sm1)
599
+ m_instance.__xpmtype__.identifier.name = "test.Model"
600
+
601
+ # Using regular Config
602
+ sc1 = SubModelAsConfig.C()
603
+ m_regular = ModelWithRegularConfig.C(m1=sc1, m2=sc1)
604
+
605
+ # Should have same identifier (backwards compatible)
606
+ assert_equal(
607
+ m_instance, m_regular, "Single InstanceConfig should be backwards compatible"
608
+ )
609
+
610
+
611
+ def test_instanceconfig_same_params_different_instances():
612
+ """Model with separate InstanceConfig instances should differ from shared"""
613
+ sm1 = SubModel.C()
614
+ sm2 = SubModel.C()
615
+
616
+ # Using the same instance twice (shared)
617
+ m1 = Model.C(m1=sm1, m2=sm1)
618
+
619
+ # Using different instances (separate)
620
+ m2 = Model.C(m1=sm1, m2=sm2)
621
+
622
+ # These should be different because sm2 is a second instance with same params
623
+ assert_notequal(m1, m2, "Models with shared vs separate instances should differ")
624
+
625
+
626
+ def test_instanceconfig_reused_instance():
627
+ """Reusing the same InstanceConfig instance should give same ID"""
628
+ sm1 = SubModel.C()
629
+
630
+ # Using the same instance object multiple times should be OK
631
+ m1 = Model.C(m1=sm1, m2=sm1)
632
+ m2 = Model.C(m1=sm1, m2=sm1)
633
+
634
+ # These should be the same because we're reusing the exact same objects
635
+ assert_equal(m1, m2, "Models with same instance objects should be equal")
636
+
637
+
638
+ def test_instanceconfig_serialization():
639
+ """InstanceConfig identifiers should be stable after serialization"""
640
+ sm1 = SubModel.C()
641
+ sm2 = SubModel.C()
642
+
643
+ # Create a model with two different instances
644
+ m1 = Model.C(m1=sm1, m2=sm2)
645
+ original_id = getidentifier(m1)
646
+
647
+ # Serialize and reload
648
+ check_reload(m1)
649
+
650
+ # The identifier should remain the same
651
+ assert getidentifier(m1) == original_id
652
+
653
+
654
+ # --- Test ignore_default vs default in field() ---
655
+
656
+
657
+ def test_identifier_field_ignore_default():
658
+ """Test that field(ignore_default=X) ignores value in identifier when value == X"""
659
+
660
+ class ConfigWithIgnoreDefault(Config):
661
+ __xpmid__ = "test.identifier.field_ignore_default"
662
+ a: Param[int] = field(ignore_default=1)
663
+ b: Param[int]
664
+
665
+ # When a=1 (matches ignore_default), should be same as not specifying a
666
+ class ConfigWithoutA(Config):
667
+ __xpmid__ = "test.identifier.field_ignore_default"
668
+ b: Param[int]
669
+
670
+ assert_equal(
671
+ ConfigWithIgnoreDefault.C(a=1, b=2),
672
+ ConfigWithIgnoreDefault.C(b=2),
673
+ "field(ignore_default=1) should ignore a=1 in identifier",
674
+ )
675
+ assert_equal(
676
+ ConfigWithIgnoreDefault.C(a=1, b=2),
677
+ ConfigWithoutA.C(b=2),
678
+ "Config with ignore_default should match config without that param",
679
+ )
680
+
681
+ # When a=2 (doesn't match ignore_default), should be included
682
+ assert_notequal(
683
+ ConfigWithIgnoreDefault.C(a=2, b=2),
684
+ ConfigWithIgnoreDefault.C(b=2),
685
+ "field(ignore_default=1) should include a=2 in identifier",
686
+ )
687
+
688
+
689
+ def test_identifier_field_default():
690
+ """Test that field(default=X) includes value in identifier even when value == X"""
691
+
692
+ class ConfigWithDefault(Config):
693
+ __xpmid__ = "test.identifier.field_default"
694
+ a: Param[int] = field(default=1)
695
+ b: Param[int]
696
+
697
+ class ConfigWithoutA(Config):
698
+ __xpmid__ = "test.identifier.field_default"
699
+ b: Param[int]
700
+
701
+ # When a=1 (matches default), should still be included in identifier
702
+ # so Config with a=1 should differ from Config without a
703
+ assert_notequal(
704
+ ConfigWithDefault.C(a=1, b=2),
705
+ ConfigWithoutA.C(b=2),
706
+ "field(default=1) should include a=1 in identifier",
707
+ )
708
+
709
+ # But two configs with same a=1 should be equal
710
+ assert_equal(
711
+ ConfigWithDefault.C(a=1, b=2),
712
+ ConfigWithDefault.C(a=1, b=2),
713
+ "Same values should have same identifier",
714
+ )
715
+
716
+
717
+ def test_identifier_field_default_vs_ignore_default():
718
+ """Test difference between field(default=X) and field(ignore_default=X)"""
719
+
720
+ class ConfigWithDefault(Config):
721
+ __xpmid__ = "test.identifier.field_default_vs_ignore"
722
+ a: Param[int] = field(default=1)
723
+ b: Param[int]
724
+
725
+ class ConfigWithIgnoreDefault(Config):
726
+ __xpmid__ = "test.identifier.field_default_vs_ignore"
727
+ a: Param[int] = field(ignore_default=1)
728
+ b: Param[int]
729
+
730
+ # Both with a=1, b=2 - should differ because one includes a, other doesn't
731
+ assert_notequal(
732
+ ConfigWithDefault.C(a=1, b=2),
733
+ ConfigWithIgnoreDefault.C(a=1, b=2),
734
+ "field(default=1) vs field(ignore_default=1) should differ when a=1",
735
+ )
736
+
737
+ # Both with a=2 (not matching default), should be the same
738
+ assert_equal(
739
+ ConfigWithDefault.C(a=2, b=2),
740
+ ConfigWithIgnoreDefault.C(a=2, b=2),
741
+ "field(default=1) vs field(ignore_default=1) should be same when a!=1",
742
+ )
743
+
744
+
745
+ # --- Test partial identifiers (subparameters) ---
746
+
747
+
748
+ # Define parameter groups at module level
749
+ iter_group = param_group("iter")
750
+ model_group = param_group("model")
751
+
752
+
753
+ def get_partial_identifier(config, sp):
754
+ """Helper to get partial identifier for a config and subparameters"""
755
+ return config.__xpm__.get_partial_identifier(sp).all
756
+
757
+
758
+ def test_partial_identifier_excludes_grouped_params():
759
+ """Test that partial identifier excludes parameters in excluded groups"""
760
+
761
+ class ConfigWithGroups(Config):
762
+ checkpoints = subparameters(exclude_groups=[iter_group])
763
+ max_iter: Param[int] = field(groups=[iter_group])
764
+ learning_rate: Param[float]
765
+
766
+ c1 = ConfigWithGroups.C(max_iter=100, learning_rate=0.1)
767
+ c2 = ConfigWithGroups.C(max_iter=200, learning_rate=0.1)
768
+
769
+ # Full identifiers should differ (max_iter is different)
770
+ assert_notequal(c1, c2, "Full identifiers should differ when max_iter differs")
771
+
772
+ # Partial identifiers should be the same (max_iter is excluded)
773
+ pid1 = get_partial_identifier(c1, ConfigWithGroups.checkpoints)
774
+ pid2 = get_partial_identifier(c2, ConfigWithGroups.checkpoints)
775
+ assert (
776
+ pid1 == pid2
777
+ ), "Partial identifiers should match when only excluded params differ"
778
+
779
+
780
+ def test_partial_identifier_includes_ungrouped_params():
781
+ """Test that partial identifier includes parameters not in excluded groups"""
782
+
783
+ class ConfigWithGroups(Config):
784
+ checkpoints = subparameters(exclude_groups=[iter_group])
785
+ max_iter: Param[int] = field(groups=[iter_group])
786
+ learning_rate: Param[float]
787
+
788
+ c1 = ConfigWithGroups.C(max_iter=100, learning_rate=0.1)
789
+ c2 = ConfigWithGroups.C(max_iter=100, learning_rate=0.2)
790
+
791
+ # Partial identifiers should differ (learning_rate is not excluded)
792
+ pid1 = get_partial_identifier(c1, ConfigWithGroups.checkpoints)
793
+ pid2 = get_partial_identifier(c2, ConfigWithGroups.checkpoints)
794
+ assert (
795
+ pid1 != pid2
796
+ ), "Partial identifiers should differ when non-excluded params differ"
797
+
798
+
799
+ def test_partial_identifier_matches_config_without_excluded():
800
+ """Test that partial identifier matches config without the excluded fields"""
801
+
802
+ class ConfigWithIter(Config):
803
+ __xpmid__ = "test.partial_identifier.config"
804
+ checkpoints = subparameters(exclude_groups=[iter_group])
805
+ max_iter: Param[int] = field(groups=[iter_group])
806
+ learning_rate: Param[float]
807
+
808
+ class ConfigWithoutIter(Config):
809
+ __xpmid__ = "test.partial_identifier.config"
810
+ learning_rate: Param[float]
811
+
812
+ c_with = ConfigWithIter.C(max_iter=100, learning_rate=0.1)
813
+ c_without = ConfigWithoutIter.C(learning_rate=0.1)
814
+
815
+ # The partial identifier of c_with should match full identifier of c_without
816
+ pid = get_partial_identifier(c_with, ConfigWithIter.checkpoints)
817
+ full_id = getidentifier(c_without)
818
+ assert (
819
+ pid == full_id
820
+ ), "Partial identifier should match config without excluded fields"
821
+
822
+
823
+ def test_partial_identifier_multiple_groups():
824
+ """Test partial identifier with parameter in multiple groups"""
825
+
826
+ class ConfigMultiGroup(Config):
827
+ checkpoints = subparameters(exclude_groups=[iter_group])
828
+ # This parameter is in both groups - should be excluded if any group is excluded
829
+ x: Param[int] = field(groups=[iter_group, model_group])
830
+ y: Param[float]
831
+
832
+ c1 = ConfigMultiGroup.C(x=1, y=0.1)
833
+ c2 = ConfigMultiGroup.C(x=2, y=0.1)
834
+
835
+ # Partial identifiers should be the same (x is in iter_group which is excluded)
836
+ pid1 = get_partial_identifier(c1, ConfigMultiGroup.checkpoints)
837
+ pid2 = get_partial_identifier(c2, ConfigMultiGroup.checkpoints)
838
+ assert (
839
+ pid1 == pid2
840
+ ), "Partial identifiers should match when param is in any excluded group"
841
+
842
+
843
+ def test_partial_identifier_include_overrides_exclude():
844
+ """Test that include_groups overrides exclude_groups"""
845
+
846
+ class ConfigIncludeOverride(Config):
847
+ # iter_group is excluded but also included, so it should NOT be excluded
848
+ partial = subparameters(
849
+ exclude_groups=[iter_group, model_group], include_groups=[iter_group]
850
+ )
851
+ x: Param[int] = field(groups=[iter_group])
852
+ y: Param[int] = field(groups=[model_group])
853
+ z: Param[float]
854
+
855
+ c1 = ConfigIncludeOverride.C(x=1, y=1, z=0.1)
856
+ c2 = ConfigIncludeOverride.C(x=2, y=1, z=0.1)
857
+ c3 = ConfigIncludeOverride.C(x=1, y=2, z=0.1)
858
+
859
+ # x is in iter_group which is included (overrides exclusion)
860
+ # so different x should give different partial identifiers
861
+ pid1 = get_partial_identifier(c1, ConfigIncludeOverride.partial)
862
+ pid2 = get_partial_identifier(c2, ConfigIncludeOverride.partial)
863
+ assert pid1 != pid2, "Include should override exclude - x should be included"
864
+
865
+ # y is in model_group which is excluded (not included)
866
+ # so different y should give SAME partial identifiers
867
+ pid3 = get_partial_identifier(c3, ConfigIncludeOverride.partial)
868
+ assert pid1 == pid3, "y is excluded - different y should give same partial ID"
869
+
870
+
871
+ def test_partial_identifier_exclude_all():
872
+ """Test exclude_all option"""
873
+
874
+ class ConfigExcludeAll(Config):
875
+ # Exclude all, but include model_group
876
+ partial = subparameters(exclude_all=True, include_groups=[model_group])
877
+ x: Param[int] = field(groups=[iter_group])
878
+ y: Param[int] = field(groups=[model_group])
879
+ z: Param[float] # No group
880
+
881
+ c1 = ConfigExcludeAll.C(x=1, y=1, z=0.1)
882
+ c2 = ConfigExcludeAll.C(x=2, y=1, z=0.1) # Different x (excluded)
883
+ c3 = ConfigExcludeAll.C(x=1, y=2, z=0.1) # Different y (included)
884
+ c4 = ConfigExcludeAll.C(x=1, y=1, z=0.2) # Different z (excluded - no group)
885
+
886
+ pid1 = get_partial_identifier(c1, ConfigExcludeAll.partial)
887
+ pid2 = get_partial_identifier(c2, ConfigExcludeAll.partial)
888
+ pid3 = get_partial_identifier(c3, ConfigExcludeAll.partial)
889
+ pid4 = get_partial_identifier(c4, ConfigExcludeAll.partial)
890
+
891
+ # x is excluded (in iter_group, not included) - same partial ID
892
+ assert pid1 == pid2, "x is excluded - should have same partial ID"
893
+
894
+ # y is included (in model_group) - different partial ID
895
+ assert pid1 != pid3, "y is included - should have different partial ID"
896
+
897
+ # z is excluded (no group, exclude_all=True) - same partial ID
898
+ assert (
899
+ pid1 == pid4
900
+ ), "z (no group) is excluded by exclude_all - should have same partial ID"
901
+
902
+
903
+ def test_partial_identifier_exclude_no_group():
904
+ """Test exclude_no_group option"""
905
+
906
+ class ConfigExcludeNoGroup(Config):
907
+ partial = subparameters(exclude_no_group=True)
908
+ x: Param[int] = field(groups=[iter_group])
909
+ y: Param[float] # No group
910
+
911
+ c1 = ConfigExcludeNoGroup.C(x=1, y=0.1)
912
+ c2 = ConfigExcludeNoGroup.C(x=2, y=0.1) # Different x (has group - not excluded)
913
+ c3 = ConfigExcludeNoGroup.C(x=1, y=0.2) # Different y (no group - excluded)
914
+
915
+ pid1 = get_partial_identifier(c1, ConfigExcludeNoGroup.partial)
916
+ pid2 = get_partial_identifier(c2, ConfigExcludeNoGroup.partial)
917
+ pid3 = get_partial_identifier(c3, ConfigExcludeNoGroup.partial)
918
+
919
+ # x has a group, so it's NOT excluded by exclude_no_group
920
+ assert pid1 != pid2, "x has group - should have different partial ID"
921
+
922
+ # y has no group, so it IS excluded by exclude_no_group
923
+ assert pid1 == pid3, "y has no group - should have same partial ID"