kalfa 3.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
kalfa/core/check.py ADDED
@@ -0,0 +1,550 @@
1
+ import inspect
2
+ import os
3
+ from dataclasses import dataclass, field
4
+
5
+ import torch
6
+
7
+ from . import aliases, build, plan, spec
8
+ from .context import train_of
9
+ from .errors import ProblemsError, RunError, error, warning
10
+ from .protocols import ContextMetric, Objective
11
+ from .refs import SINGULAR, join_tokens, ref_parts, tokenize
12
+
13
+
14
+ TRAIN_KEYS = ("epochs", "updates", "step", "predicts", "evaluate", "checkpoint", "report", "track", "log")
15
+ CHECKPOINT_KEYS = ("watch", "mode", "last", "every")
16
+ SECTIONS = ("models", "losses", "metrics", "optimizers")
17
+ SET_NAMES = ("train", "valid", "test")
18
+ SERIES_PREFIXES = ("train", "val", "test", "track", "lr", "minimizes")
19
+ STD_STEPS = ("supervised", "alternating")
20
+
21
+
22
+ @dataclass
23
+ class Checked:
24
+ config: object
25
+ tree: object
26
+ plan: dict | None
27
+ problems: list = field(default_factory=list)
28
+
29
+ @property
30
+ def errors(self):
31
+ return [problem for problem in self.problems if problem.severity == "error"]
32
+
33
+ @property
34
+ def warnings(self):
35
+ return [problem for problem in self.problems if problem.severity == "warning"]
36
+
37
+ @property
38
+ def ok(self):
39
+ return not self.errors
40
+
41
+ def text(self):
42
+ return "\n".join(str(problem) for problem in self.problems) or "ok"
43
+
44
+
45
+ def merge(into, more):
46
+ for problem in more or ():
47
+ if problem not in into:
48
+ into.append(problem)
49
+ return into
50
+
51
+
52
+ def has_errors(problems):
53
+ return any(problem.severity == "error" for problem in problems)
54
+
55
+
56
+ def positive_int(value):
57
+ return isinstance(value, int) and not isinstance(value, bool) and value > 0
58
+
59
+
60
+ def ref_target(value):
61
+ if isinstance(value, build.Ref):
62
+ return value.path, value.attr
63
+ text = ref_parts(value)
64
+ if text is None:
65
+ return None, None
66
+ try:
67
+ tokens = tokenize(text)
68
+ except RunError:
69
+ return None, None
70
+ split = 2 if tokens[0] in SECTIONS else 1
71
+ return join_tokens(tokens[:split]), join_tokens(tokens[split:]) or None
72
+
73
+
74
+ def ref_text(value):
75
+ return value.text if isinstance(value, build.Ref) else value
76
+
77
+
78
+ def mutable_set(tree):
79
+ found = build.mutable_paths(tree)
80
+ if isinstance(found, dict):
81
+ found = found.keys()
82
+ return {str(path) for path in found or ()}
83
+
84
+
85
+ def nested_paths(prefix, value):
86
+ paths = set()
87
+ if isinstance(value, dict) and "use" not in value:
88
+ for key, inner in value.items():
89
+ leaf = f"{prefix}.{key}"
90
+ paths.add(leaf)
91
+ paths |= nested_paths(leaf, inner)
92
+ elif isinstance(value, (list, tuple)):
93
+ for index, inner in enumerate(value):
94
+ leaf = f"{prefix}[{index}]"
95
+ paths.add(leaf)
96
+ paths |= nested_paths(leaf, inner)
97
+ return paths
98
+
99
+
100
+ def known_paths(tree):
101
+ paths = set()
102
+ for node in tree.nodes.values():
103
+ paths.add(node.path)
104
+ paths.update(f"{node.path}.{key}" for key in build.SLOT_KEYS.get(node.slot, ()))
105
+ for bucket in (node.params, node.keys):
106
+ for key, value in (bucket or {}).items():
107
+ leaf = f"{node.path}.{key}"
108
+ paths.add(leaf)
109
+ paths |= nested_paths(leaf, value)
110
+ for key, value in train_of(tree).items():
111
+ paths.add(f"train.{key}")
112
+ paths |= nested_paths(f"train.{key}", value)
113
+ return paths
114
+
115
+
116
+ def node_for(tree, path):
117
+ try:
118
+ tokens = tokenize(path)
119
+ except RunError:
120
+ return None, None
121
+ for count in range(len(tokens), 0, -1):
122
+ text = join_tokens(tokens[:count])
123
+ if text in tree.nodes:
124
+ return tree.nodes[text], tokens[count:]
125
+ return None, tokens
126
+
127
+
128
+ def nearest(tree, node, mutable, path):
129
+ if node is not None:
130
+ under = sorted(item for item in mutable if item.startswith((f"{node.path}.", f"{node.path}[")))
131
+ if under:
132
+ return f"mutable under {node.path}: " + ", ".join(under)
133
+ return f"nothing under {node.path} can be set by a rule"
134
+ section = path.split(".", 1)[0].split("[", 1)[0]
135
+ if section == "train":
136
+ under = sorted(item for item in mutable if item.startswith("train."))
137
+ return "mutable under train: " + ", ".join(under) if under else "nothing under train can be set by a rule"
138
+ names = sorted(tree.section(section)) if section in SECTIONS else []
139
+ if names:
140
+ return f"the {section} are " + ", ".join(names)
141
+ return "a path starts with " + ", ".join(SECTIONS + ("train",))
142
+
143
+
144
+ def check_set_path(tree, rule, path, mutable, known, problems):
145
+ if path in mutable:
146
+ return
147
+ where = f"rules.{rule.name}.set"
148
+ node, _ = node_for(tree, path)
149
+ hint = nearest(tree, node, mutable, path)
150
+ if path in known:
151
+ problems.append(error("not_mutable", f"{path} is fixed and cannot be set by a rule", where, hint=hint))
152
+ else:
153
+ problems.append(error("unknown_path", f"{path} is not a path of the plan", where, hint=hint))
154
+
155
+
156
+ def check_set_value(tree, rule, path, value, problems):
157
+ if ref_parts(value) is None:
158
+ return
159
+ where = f"rules.{rule.name}.set"
160
+ target, _ = ref_target(value)
161
+ if target is None:
162
+ problems.append(error("rule_ref", f"{path} is set to {value!r}, which is not a reference", where))
163
+ return
164
+ if target not in tree.nodes:
165
+ section = target.split(".", 1)[0]
166
+ names = sorted(tree.section(section)) if section in SECTIONS else []
167
+ hint = f"the {section} are " + ", ".join(names) if names else None
168
+ problems.append(error("rule_ref", f"{path} is set to {value!r}, which names no node", where, hint=hint))
169
+
170
+
171
+ def check_cycles(specs, problems):
172
+ after = {rule.name: rule.after for rule in specs}
173
+ reported = set()
174
+ for name in after:
175
+ seen = []
176
+ current = name
177
+ while current is not None and current in after and current not in seen:
178
+ seen.append(current)
179
+ current = after[current]
180
+ if current == name and tuple(sorted(seen)) not in reported:
181
+ reported.add(tuple(sorted(seen)))
182
+ problems.append(error("rule_after", "rules " + ", ".join(seen) + " wait for each other and can never fire",
183
+ f"rules.{name}.after"))
184
+
185
+
186
+ def data_weights(tree):
187
+ node = tree.nodes.get("data")
188
+ weights = (node.params or {}).get("weights") if node is not None else None
189
+ return [str(name) for name in weights] if isinstance(weights, dict) else []
190
+
191
+
192
+ def data_targets(tree):
193
+ node = tree.nodes.get("data")
194
+ if node is None:
195
+ return False, []
196
+ params = node.params or {}
197
+ table = node.use == "table" or "targets" in params or getattr(node.target, "__name__", "") == "Table"
198
+ targets = params.get("targets")
199
+ names = [str(name) for name in targets] if isinstance(targets, dict) else []
200
+ return table, names
201
+
202
+
203
+ def check_train(tree, problems):
204
+ settings = train_of(tree)
205
+ keys_hint = "the train keys are " + ", ".join(TRAIN_KEYS)
206
+ for key in settings:
207
+ if key not in TRAIN_KEYS:
208
+ problems.append(error("train_key", f"unknown train key {key!r}", f"train.{key}", hint=keys_hint))
209
+ epochs = settings.get("epochs")
210
+ updates = settings.get("updates")
211
+ if epochs is not None and updates is not None:
212
+ problems.append(error("train_unit", "train takes epochs or updates, not both", "train"))
213
+ elif epochs is None and updates is None:
214
+ problems.append(error("train_unit", "train needs epochs or updates: {total, turn}", "train"))
215
+ if epochs is not None and not positive_int(epochs):
216
+ problems.append(error("train_unit", f"epochs is a positive integer, got {epochs!r}", "train.epochs"))
217
+ if updates is not None:
218
+ if not isinstance(updates, dict) or not positive_int(updates.get("total")):
219
+ problems.append(error("train_unit", "updates is a mapping {total, turn} with a positive integer total",
220
+ "train.updates"))
221
+ else:
222
+ for key in updates:
223
+ if key not in ("total", "turn"):
224
+ problems.append(error("train_key", f"unknown updates key {key!r}", f"train.updates.{key}",
225
+ hint="the updates keys are total and turn"))
226
+ if "turn" in updates and not positive_int(updates["turn"]):
227
+ problems.append(error("train_unit", f"updates.turn is a positive integer, got {updates['turn']!r}",
228
+ "train.updates.turn"))
229
+ report = settings.get("report", "last")
230
+ checkpoint = settings.get("checkpoint")
231
+ if report not in ("best", "last"):
232
+ problems.append(error("train_report", f"report is best or last, got {report!r}", "train.report"))
233
+ elif report == "best" and not (isinstance(checkpoint, dict) and checkpoint.get("watch")):
234
+ problems.append(error("report_best", "report: best needs a checkpoint with a watch series", "train.report",
235
+ hint="add checkpoint: {watch: val/<metric>}"))
236
+ if checkpoint is not None:
237
+ check_checkpoint(checkpoint, problems)
238
+ evaluate = settings.get("evaluate")
239
+ if evaluate is not None:
240
+ check_evaluate(tree, evaluate, problems)
241
+ predicts = settings.get("predicts")
242
+ if predicts is not None and predicts is not False:
243
+ target, attr = ref_target(predicts)
244
+ if target is None or attr or not target.startswith("models.") or target not in tree.nodes:
245
+ names = sorted(tree.section("models"))
246
+ problems.append(error("predicts_ref", f"predicts names a model like @models.<name>, got {predicts!r}",
247
+ "train.predicts", hint="the models are " + ", ".join(names) if names else None))
248
+ track = settings.get("track")
249
+ if track is not None and (not isinstance(track, list) or not all(isinstance(item, str) for item in track)):
250
+ problems.append(error("train_track", f"track is a list of strings, got {track!r}", "train.track"))
251
+ elif track:
252
+ for path in track:
253
+ check_track_path(tree, path, problems)
254
+ log = settings.get("log")
255
+ entries = [log] if isinstance(log, (str, dict)) else log
256
+ if log is not None and (not isinstance(entries, list)
257
+ or not all(isinstance(item, str) or (isinstance(item, dict) and "use" in item)
258
+ for item in entries)):
259
+ problems.append(error("train_log", f"log is a list of logger names or calls, got {log!r}", "train.log"))
260
+
261
+
262
+ def check_track_path(tree, path, problems):
263
+ where = "train.track"
264
+ try:
265
+ tokens = tokenize(path)
266
+ except RunError as failure:
267
+ problems.append(error("track_path", str(failure), where))
268
+ return
269
+ root = tokens[0]
270
+ if root == "train":
271
+ return
272
+ if root not in SECTIONS:
273
+ problems.append(error("unknown_path", f"{path} is not a path of the plan", where,
274
+ hint="a track path starts with " + ", ".join(SECTIONS + ("train",))))
275
+ return
276
+ if len(tokens) < 3 or not isinstance(tokens[1], str) or f"{root}.{tokens[1]}" not in tree.nodes:
277
+ names = sorted(tree.section(root))
278
+ hint = f"the {root} are " + ", ".join(names) if names else f"there are no {root}"
279
+ problems.append(error("unknown_path", f"{path} names no {SINGULAR[root]} of the plan and one of its "
280
+ f"values, like {root}.<name>.<param>", where, hint=hint))
281
+
282
+
283
+ def check_checkpoint(checkpoint, problems):
284
+ if not isinstance(checkpoint, dict):
285
+ problems.append(error("checkpoint_value", "checkpoint is a mapping {watch, mode, last, every}",
286
+ "train.checkpoint"))
287
+ return
288
+ for key in checkpoint:
289
+ if key not in CHECKPOINT_KEYS:
290
+ problems.append(error("checkpoint_key", f"unknown checkpoint key {key!r}", f"train.checkpoint.{key}",
291
+ hint="the checkpoint keys are " + ", ".join(CHECKPOINT_KEYS)))
292
+ watch = checkpoint.get("watch")
293
+ if watch is not None and (not isinstance(watch, str) or not watch):
294
+ problems.append(error("checkpoint_watch", f"watch is a series name like val/rmse, got {watch!r}",
295
+ "train.checkpoint.watch"))
296
+ mode = checkpoint.get("mode", "min")
297
+ if mode not in ("min", "max"):
298
+ problems.append(error("checkpoint_value", f"mode is min or max, got {mode!r}", "train.checkpoint.mode"))
299
+ every = checkpoint.get("every")
300
+ if every is not None and not positive_int(every):
301
+ problems.append(error("checkpoint_value", f"every is a positive integer, got {every!r}",
302
+ "train.checkpoint.every"))
303
+ last = checkpoint.get("last", True)
304
+ if not isinstance(last, bool):
305
+ problems.append(error("checkpoint_value", f"last is true or false, got {last!r}", "train.checkpoint.last"))
306
+
307
+
308
+ def check_evaluate(tree, evaluate, problems):
309
+ if not isinstance(evaluate, list) or not all(isinstance(name, str) and name for name in evaluate):
310
+ problems.append(error("evaluate_set", f"evaluate is a list of set names, got {evaluate!r}", "train.evaluate"))
311
+ return
312
+ if len(set(evaluate)) != len(evaluate):
313
+ problems.append(error("evaluate_set", "evaluate names a set twice", "train.evaluate"))
314
+ table, _ = data_targets(tree)
315
+ unknown = [name for name in evaluate if name not in SET_NAMES]
316
+ if table and unknown:
317
+ problems.append(error("evaluate_set", "evaluate names " + ", ".join(unknown) + ", which a table does not make",
318
+ "train.evaluate", hint="the sets of a table are " + ", ".join(SET_NAMES)))
319
+
320
+
321
+ def step_is_std(tree):
322
+ node = tree.nodes.get("train.step")
323
+ if node is None or node.use in STD_STEPS:
324
+ return True
325
+ return getattr(node.target, "__module__", "").startswith("kalfa.std")
326
+
327
+
328
+ def check_optimizers(tree, problems):
329
+ make = error if step_is_std(tree) else warning
330
+ losses = sorted(tree.section("losses"))
331
+ hint = "the losses are " + ", ".join(losses) if losses else "there are no losses"
332
+ for node in tree.section("optimizers").values():
333
+ minimizes = (node.keys or {}).get("minimizes", (node.params or {}).get("minimizes"))
334
+ if minimizes is None:
335
+ problems.append(make("minimizes_required", f"optimizer {node.name!r} names no loss to minimize", node.path,
336
+ hint='add minimizes: "@losses.<name>"'))
337
+ continue
338
+ target, attr = ref_target(minimizes)
339
+ if target is None or attr or not target.startswith("losses.") or target not in tree.nodes:
340
+ problems.append(error("minimizes_ref", f"minimizes references a loss like @losses.<name>, got "
341
+ f"{ref_text(minimizes)!r}", f"{node.path}.minimizes", hint=hint))
342
+
343
+
344
+ def is_criterion(node):
345
+ target = node.target
346
+ if target is None:
347
+ return False
348
+ if inspect.isclass(target) and issubclass(target, (Objective, ContextMetric)):
349
+ return False
350
+ return True
351
+
352
+
353
+ def output_names(tree):
354
+ outputs = []
355
+ for node in tree.section("models").values():
356
+ names = (node.keys or {}).get("outputs")
357
+ if isinstance(names, str):
358
+ names = [names]
359
+ if isinstance(names, (list, tuple)) and names:
360
+ outputs.extend(str(name) for name in names)
361
+ else:
362
+ outputs.append(node.name)
363
+ return outputs
364
+
365
+
366
+ def check_criteria(tree, problems):
367
+ models = tree.section("models")
368
+ predicts = train_of(tree).get("predicts")
369
+ target, attr = ref_target(predicts) if predicts is not None else (None, None)
370
+ predicts_known = (len(models) == 1 and predicts is not False) or (target in tree.nodes and not attr)
371
+ outputs = output_names(tree)
372
+ table, targets = data_targets(tree)
373
+ weights = data_weights(tree)
374
+ outputs_hint = "the outputs are " + ", ".join(outputs) if outputs else None
375
+ for section in ("losses", "metrics"):
376
+ kind = SINGULAR[section]
377
+ for node in tree.section(section).values():
378
+ if not is_criterion(node):
379
+ if (node.keys or {}).get("sample_weight") is not None:
380
+ problems.append(error("weight_objective", f"{kind} {node.name!r} is an objective; sample_weight "
381
+ "belongs to a criterion, a loss of (pred, target)",
382
+ f"{node.path}.sample_weight",
383
+ hint="an objective reads the weight field from the batch itself"))
384
+ continue
385
+ keys = node.keys or {}
386
+ pred = keys.get("pred")
387
+ target = keys.get("target")
388
+ if pred is None and not predicts_known:
389
+ problems.append(error("pred_required", f"{kind} {node.name!r} needs pred: <output> because no single "
390
+ "model predicts", node.path,
391
+ hint=outputs_hint or "set train.predicts to the model whose output it compares"))
392
+ elif pred is not None and outputs and pred not in outputs:
393
+ problems.append(warning("unknown_output", f"pred {pred!r} is no output of a model", f"{node.path}.pred",
394
+ hint=outputs_hint))
395
+ if table and target is None and len(targets) != 1:
396
+ hint = "the targets are " + ", ".join(targets) if targets else "the data declares no targets"
397
+ problems.append(error("target_required", f"{kind} {node.name!r} needs target: <name> because the data "
398
+ f"has {len(targets)} targets", node.path, hint=hint))
399
+ weight = keys.get("sample_weight")
400
+ if table and weight is not None and weight not in weights:
401
+ hint = ("the weights are " + ", ".join(weights) if weights
402
+ else 'declare it under data.weights, such as weights: {w: {"price < 5": 5}}')
403
+ problems.append(error("unknown_weight", f"sample_weight {weight!r} is no weight of the data",
404
+ f"{node.path}.sample_weight", hint=hint))
405
+
406
+
407
+ def check_device(tree, problems):
408
+ data = getattr(getattr(tree, "config", None), "data", None)
409
+ device = data.get("device") if isinstance(data, dict) else None
410
+ if device is None or device == "auto":
411
+ return
412
+ if not isinstance(device, str):
413
+ problems.append(error("device_value", f"device is a string like cpu, cuda:0 or auto, got {device!r}", "device"))
414
+ return
415
+ try:
416
+ torch.device(device)
417
+ except (RuntimeError, TypeError, ValueError):
418
+ problems.append(error("device_value", f"{device!r} is not a device torch knows", "device",
419
+ hint="use auto, cpu, mps, cuda or cuda:<index>"))
420
+
421
+
422
+ def check_series(text, names, prefixes, where, problems, strict=False):
423
+ make = error if strict else warning
424
+ code = "watch_unknown" if strict else "unknown_series"
425
+ prefix, _, rest = text.partition("/")
426
+ if not rest or prefix not in prefixes:
427
+ problems.append(make(code, f"{text!r} does not look like a series such as val/<metric>", where))
428
+ return
429
+ if prefix in ("train", "val", "test") and rest.split("/")[0] not in names:
430
+ problems.append(make(code, f"{text!r} names no loss or metric", where,
431
+ hint="the names are " + ", ".join(sorted(names)) if names else None))
432
+
433
+
434
+ def check_watches(tree, problems):
435
+ names = set(tree.section("losses")) | set(tree.section("metrics"))
436
+ settings = train_of(tree)
437
+ prefixes = set(SERIES_PREFIXES)
438
+ evaluate = settings.get("evaluate")
439
+ if isinstance(evaluate, list):
440
+ prefixes.update(str(name) for name in evaluate)
441
+ checkpoint = settings.get("checkpoint")
442
+ if isinstance(checkpoint, dict) and isinstance(checkpoint.get("watch"), str) and checkpoint["watch"]:
443
+ check_series(checkpoint["watch"], names, prefixes, "train.checkpoint.watch", problems,
444
+ strict=settings.get("report") == "best")
445
+ for node in tree.nodes.values():
446
+ watch = (node.params or {}).get("watch")
447
+ if node.path.endswith(".when") and isinstance(watch, str) and watch:
448
+ check_series(watch, names, prefixes, f"{node.path}.watch", problems)
449
+
450
+
451
+ def check_names(tree, problems):
452
+ shared = set(tree.section("losses")) & set(tree.section("metrics"))
453
+ for name in sorted(shared):
454
+ problems.append(warning("series_clash", f"loss {name!r} and metric {name!r} write the same series "
455
+ f"train/{name}, val/{name} and test/{name}; the metric value is kept",
456
+ f"metrics.{name}", hint="rename one of them to record both"))
457
+
458
+
459
+ def check_data(tree, problems):
460
+ node = tree.nodes.get("data")
461
+ if node is None or node.target is None:
462
+ return
463
+ checker = inspect.getattr_static(node.target, "check", None)
464
+ if not isinstance(checker, (classmethod, staticmethod)):
465
+ return
466
+ directories = getattr(getattr(tree, "config", None), "directories", None) or ()
467
+ node.target.check(dict(node.params or {}), problems, directories)
468
+
469
+
470
+ def check_data_refs(tree, problems):
471
+ node = tree.nodes.get("data")
472
+ exposes = getattr(node.target, "exposes", None) if node is not None and node.target is not None else None
473
+ if exposes is None:
474
+ return
475
+ exposes = tuple(str(name) for name in exposes)
476
+ for owner in tree.nodes.values():
477
+ for ref in owner.refs:
478
+ if ref.path != "data" or not ref.attr:
479
+ continue
480
+ first = ref.attr.split(".")[0]
481
+ if first not in exposes:
482
+ problems.append(error("reference_unknown", f"{ref.at}: {ref.text} is not exposed", owner.path,
483
+ hint="the data class exposes " + ", ".join(exposes)))
484
+
485
+
486
+ def check_actions(tree, problems):
487
+ has_generate = "generate" in tree.nodes
488
+ for node in tree.nodes.values():
489
+ if node.slot != "action" or not getattr(node.target, "requires_generate", False):
490
+ continue
491
+ if node.params.get("sampler") is None and not has_generate:
492
+ problems.append(error("generate_required", f"{node.use} needs a sampler or a generate section", node.path,
493
+ hint="write sampler: {use: ...} on the action or a generate section"))
494
+
495
+
496
+ def check_sweep(tree, problems):
497
+ raw = tree.settings.get("sweep") if isinstance(tree.settings, dict) else None
498
+ if raw is None:
499
+ return
500
+ from kalfa.commands.sweep import check_sweep as checker
501
+
502
+ checker(tree, raw, problems)
503
+
504
+
505
+ def validate(tree):
506
+ problems = []
507
+ specs = tree.rules
508
+ mutable = mutable_set(tree)
509
+ known = known_paths(tree)
510
+ for rule in specs:
511
+ for path, value in rule.set.items():
512
+ check_set_path(tree, rule, path, mutable, known, problems)
513
+ check_set_value(tree, rule, path, value, problems)
514
+ check_cycles(specs, problems)
515
+ check_data(tree, problems)
516
+ check_data_refs(tree, problems)
517
+ check_train(tree, problems)
518
+ check_optimizers(tree, problems)
519
+ check_criteria(tree, problems)
520
+ check_device(tree, problems)
521
+ check_watches(tree, problems)
522
+ check_names(tree, problems)
523
+ check_actions(tree, problems)
524
+ check_sweep(tree, problems)
525
+ return problems
526
+
527
+
528
+ def check(paths=(), sets=(), params=(), mappings=()):
529
+ config = spec.load(paths=paths, sets=sets, params=params, mappings=mappings)
530
+ problems = merge([], config.problems)
531
+ for directory in config.directories:
532
+ aliases.add_search_dir(directory)
533
+ aliases.add_search_dir(os.getcwd())
534
+ if has_errors(problems):
535
+ return Checked(config, None, None, problems)
536
+ aliases.import_plugins(config.plugins, problems)
537
+ if has_errors(problems):
538
+ return Checked(config, None, None, problems)
539
+ try:
540
+ tree = build.build(config)
541
+ except ProblemsError as failure:
542
+ return Checked(config, None, None, merge(problems, failure.problems))
543
+ merge(problems, tree.problems)
544
+ merge(problems, validate(tree))
545
+ made = None
546
+ if not has_errors(problems):
547
+ from kalfa import __version__
548
+
549
+ made = plan.plan_of(tree, __version__)
550
+ return Checked(config, tree, made, problems)
kalfa/core/coerce.py ADDED
@@ -0,0 +1,36 @@
1
+ import inspect
2
+ import numbers
3
+
4
+ import torch
5
+
6
+
7
+ TENSORS = ("weight", "pos_weight")
8
+
9
+
10
+ def is_loss(target):
11
+ return inspect.isclass(target) and issubclass(target, torch.nn.modules.loss._Loss)
12
+
13
+
14
+ def numeric(value):
15
+ if isinstance(value, bool):
16
+ return False
17
+ if isinstance(value, numbers.Number):
18
+ return True
19
+ return isinstance(value, list) and bool(value) and all(numeric(item) for item in value)
20
+
21
+
22
+ def coerced(target, kwargs):
23
+ try:
24
+ parameters = inspect.signature(target).parameters
25
+ except (TypeError, ValueError):
26
+ return kwargs
27
+ found = dict(kwargs)
28
+ for name, value in kwargs.items():
29
+ parameter = parameters.get(name)
30
+ if parameter is None:
31
+ continue
32
+ if isinstance(value, list) and isinstance(parameter.default, tuple):
33
+ found[name] = tuple(value)
34
+ elif name in TENSORS and is_loss(target) and numeric(value):
35
+ found[name] = torch.as_tensor(value, dtype=torch.float32)
36
+ return found