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/build.py ADDED
@@ -0,0 +1,727 @@
1
+ import functools
2
+ import inspect
3
+ from dataclasses import dataclass, field
4
+
5
+ from . import aliases, wrap
6
+ from .coerce import coerced
7
+ from .context import train_of
8
+ from .errors import BuildError, KalfaError, ProblemsError, error
9
+ from .make import build_model, build_optimizer
10
+ from .rules import parse_rules
11
+
12
+
13
+ SLOT_KEYS = {
14
+ "data": (),
15
+ "model": ("inputs", "outputs", "ema", "freeze", "weights", "init"),
16
+ "loss": ("pred", "target", "on", "every", "rescale", "sample_weight"),
17
+ "metric": ("pred", "target", "on", "every", "rescale"),
18
+ "optimizer": ("over", "minimizes", "schedule"),
19
+ "step": ("order", "amp", "grad_clip", "accumulate"),
20
+ "trigger": (),
21
+ "action": (),
22
+ "logger": (),
23
+ "schedule": (),
24
+ "plot": (),
25
+ "sampler": (),
26
+ "strategy": (),
27
+ "value": (),
28
+ }
29
+
30
+ SECTION_SLOTS = {
31
+ "data": "data",
32
+ "models": "model",
33
+ "losses": "loss",
34
+ "metrics": "metric",
35
+ "optimizers": "optimizer",
36
+ "plots": "plot",
37
+ "generate": "sampler",
38
+ }
39
+
40
+ ORDER = ("data", "models", "losses", "metrics", "optimizers", "train.step", "plots", "generate")
41
+
42
+ MODEL_FIXED = ("inputs", "outputs", "ema", "weights", "init")
43
+ OPTIMIZER_FIXED = ("over", "schedule")
44
+ CALL_KEYS = {"model": ("init",), "optimizer": ("schedule",)}
45
+
46
+ TOP_LEVEL = ("include", "params", "plugins", "seed", "device", "record", "data", "models", "losses", "metrics",
47
+ "optimizers", "train", "rules", "plots", "generate", "sweep")
48
+ LOADER_KEYS = ("include", "params", "plugins")
49
+ SINGLE_NODES = ("data", "generate")
50
+ DEFAULT_STEP = "supervised"
51
+ STEP_ATTRIBUTES = SLOT_KEYS["step"]
52
+
53
+
54
+ @dataclass
55
+ class Ref:
56
+ text: str
57
+ path: str
58
+ attr: str | None
59
+ at: str
60
+ target: object = None
61
+
62
+ @property
63
+ def name(self):
64
+ return self.path.rsplit(".", 1)[-1]
65
+
66
+
67
+ @dataclass
68
+ class Node:
69
+ path: str
70
+ slot: str
71
+ use: str | None
72
+ target: object
73
+ params: dict
74
+ keys: dict = field(default_factory=dict)
75
+ refs: list = field(default_factory=list)
76
+ children: list = field(default_factory=list)
77
+ obj: object = None
78
+
79
+ @property
80
+ def name(self):
81
+ return self.path.rsplit(".", 1)[-1]
82
+
83
+ @property
84
+ def section(self):
85
+ return self.path.split(".", 1)[0]
86
+
87
+
88
+ @dataclass
89
+ class Signature:
90
+ params: dict
91
+ accepts_extra: bool
92
+ required: tuple
93
+
94
+
95
+ @dataclass
96
+ class Tree:
97
+ config: object
98
+ nodes: dict
99
+ order: list
100
+ settings: dict
101
+ rules: list
102
+ problems: list = field(default_factory=list)
103
+ imports: list = field(default_factory=list)
104
+
105
+ @property
106
+ def errors(self):
107
+ return [problem for problem in self.problems if problem.severity == "error"]
108
+
109
+ def node(self, path):
110
+ return self.nodes[path]
111
+
112
+ def section(self, name):
113
+ return {node.name: node for node in self.nodes.values()
114
+ if node.path.startswith(f"{name}.") and "." not in node.path[len(name) + 1:]
115
+ and "[" not in node.path[len(name) + 1:]}
116
+
117
+
118
+ def signature_of(target):
119
+ if inspect.isclass(target):
120
+ function = target.__init__
121
+ skip = 1
122
+ else:
123
+ function = target
124
+ skip = 0
125
+ try:
126
+ raw = inspect.signature(function)
127
+ except (TypeError, ValueError):
128
+ return Signature({}, True, ())
129
+ params = {}
130
+ extra = False
131
+ required = []
132
+ for name, parameter in list(raw.parameters.items())[skip:]:
133
+ if parameter.kind is inspect.Parameter.VAR_KEYWORD:
134
+ extra = True
135
+ continue
136
+ if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
137
+ continue
138
+ params[name] = parameter
139
+ if parameter.default is inspect.Parameter.empty:
140
+ required.append(name)
141
+ return Signature(params, extra, tuple(required))
142
+
143
+
144
+ def leading_required(signature):
145
+ count = 0
146
+ positional = (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
147
+ for parameter in signature.params.values():
148
+ if parameter.kind not in positional or parameter.default is not inspect.Parameter.empty:
149
+ break
150
+ count += 1
151
+ return count
152
+
153
+
154
+ def criterion_function(target, signature=None):
155
+ if target is None or inspect.isclass(target) or not callable(target):
156
+ return False
157
+ signature = signature_of(target) if signature is None else signature
158
+ return leading_required(signature) >= 2
159
+
160
+
161
+ def node_signature(target, slot):
162
+ signature = signature_of(target)
163
+ drop = ()
164
+ names = list(signature.params)
165
+ if slot == "optimizer" and names:
166
+ drop = ("params",) if "params" in names else (names[0],)
167
+ elif slot == "plot" and not inspect.isclass(target):
168
+ drop = tuple(names[:2])
169
+ elif slot == "loss" and criterion_function(target, signature):
170
+ drop = tuple(names[:2])
171
+ if not drop:
172
+ return signature
173
+ params = {name: parameter for name, parameter in signature.params.items() if name not in drop}
174
+ required = tuple(name for name in signature.required if name not in drop)
175
+ return Signature(params, signature.accepts_extra, required)
176
+
177
+
178
+ def fixed_of(node):
179
+ target = node.target
180
+ if node.slot == "model":
181
+ names = list(MODEL_FIXED)
182
+ if target is not None:
183
+ names.extend(node_signature(target, node.slot).params)
184
+ names.extend(name for name in node.params if name not in names)
185
+ return tuple(dict.fromkeys(names))
186
+ if node.slot in ("loss", "metric"):
187
+ return tuple(dict.fromkeys(("pred", "target", *declared_fixed(target))))
188
+ if node.slot == "optimizer":
189
+ return OPTIMIZER_FIXED
190
+ if node.slot in ("step", "trigger"):
191
+ return tuple(declared_fixed(target))
192
+ return ()
193
+
194
+
195
+ def declared_fixed(target):
196
+ found = getattr(target, "fixed", ()) if target is not None else ()
197
+ if isinstance(found, str):
198
+ return (found,)
199
+ try:
200
+ return tuple(str(name) for name in found)
201
+ except TypeError:
202
+ return ()
203
+
204
+
205
+ def join(path, key):
206
+ return f"{path}[{key}]" if isinstance(key, int) else f"{path}.{key}"
207
+
208
+
209
+ class Builder:
210
+ def __init__(self, config, data):
211
+ self.config = config
212
+ self.data = data
213
+ self.problems = []
214
+ self.declared = []
215
+ self.known = set()
216
+ self.nodes = {}
217
+ self.settings = {}
218
+ self.rules = {}
219
+ self.imports = []
220
+
221
+ def fail(self, code, message, path=None, hint=None):
222
+ self.problems.append(error(code, message, path, hint))
223
+
224
+ def run(self):
225
+ self.scan_top_level()
226
+ self.declare_all()
227
+ self.known = {path for path, _, _ in self.declared}
228
+ for path, slot, raw in self.declared:
229
+ self.add(self.make_node(path, slot, raw))
230
+ specs = parse_rules(self.rules or None, self.problems)
231
+ return Tree(self.config, self.nodes, list(self.nodes), self.settings, specs, self.problems, self.imports)
232
+
233
+ def scan_top_level(self):
234
+ for key, value in self.data.items():
235
+ name = str(key)
236
+ if name not in TOP_LEVEL:
237
+ self.fail("unknown_section", f"{name!r} is not a top level key; the keys are {list(TOP_LEVEL)}", name)
238
+ self.settings[name] = value
239
+ continue
240
+ if name in LOADER_KEYS or name in SECTION_SLOTS:
241
+ continue
242
+ if name == "rules":
243
+ if value is None:
244
+ continue
245
+ if not isinstance(value, dict):
246
+ self.fail("bad_section", "rules must be a mapping of rule names to rules", "rules")
247
+ continue
248
+ self.rules = value
249
+ elif name == "train":
250
+ train = self.train_mapping()
251
+ self.settings["train"] = {k: v for k, v in train.items() if k != "step"}
252
+ else:
253
+ self.settings[name] = value
254
+
255
+ def train_mapping(self):
256
+ train = self.data.get("train")
257
+ if train is None:
258
+ return {}
259
+ if not isinstance(train, dict):
260
+ self.fail("bad_section", "train must be a mapping with epochs or updates", "train")
261
+ return {}
262
+ return train
263
+
264
+ def declare(self, path, slot, raw):
265
+ self.declared.append((path, slot, raw))
266
+
267
+ def declare_all(self):
268
+ for section in ORDER:
269
+ if section == "train.step":
270
+ train = self.data.get("train") if isinstance(self.data.get("train"), dict) else {}
271
+ step = train.get("step")
272
+ if step is None:
273
+ step = {"use": DEFAULT_STEP}
274
+ self.declare("train.step", "step", step)
275
+ log = train.get("log")
276
+ for index, entry in enumerate([log] if isinstance(log, (str, dict)) else list(log or [])):
277
+ self.declare(f"train.log[{index}]", "logger", entry)
278
+ elif section in SINGLE_NODES:
279
+ if section in self.data and self.data[section] is not None:
280
+ self.declare(section, SECTION_SLOTS[section], self.data[section])
281
+ else:
282
+ self.declare_section(section)
283
+ rules = self.rules if isinstance(self.rules, dict) else {}
284
+ for name, rule in rules.items():
285
+ if not isinstance(rule, dict):
286
+ continue
287
+ if rule.get("when") is not None:
288
+ self.declare(f"rules.{name}.when", "trigger", rule["when"])
289
+ if rule.get("run") is not None:
290
+ self.declare(f"rules.{name}.run", "action", rule["run"])
291
+
292
+ def declare_section(self, section):
293
+ raw = self.data.get(section)
294
+ if raw is None:
295
+ return
296
+ if not isinstance(raw, dict):
297
+ self.fail("bad_section", f"{section} must be a mapping of names to calls", section)
298
+ return
299
+ for name, value in raw.items():
300
+ self.declare(f"{section}.{name}", SECTION_SLOTS[section], value)
301
+
302
+ def add(self, node):
303
+ self.nodes[node.path] = node
304
+ for child in node.children:
305
+ self.add(child)
306
+
307
+ def make_node(self, path, slot, raw):
308
+ if isinstance(raw, str) and not raw.startswith("@"):
309
+ raw = {"use": raw}
310
+ node = Node(path, slot, None, None, {})
311
+ if not isinstance(raw, dict) or "use" not in raw:
312
+ self.fail("not_a_call", f"expected a call such as {{use: name, ...}} or a name, got {raw!r}", path,
313
+ "a mapping is a call only when it carries use")
314
+ return node
315
+ text = raw["use"]
316
+ if not isinstance(text, str) or not text:
317
+ self.fail("not_a_call", f"use must be a short name or an import path, got {text!r}", path)
318
+ return node
319
+ node.use = text
320
+ resolved = None
321
+ try:
322
+ resolved = aliases.resolve(text)
323
+ except BuildError as exc:
324
+ self.fail("unknown_use", str(exc), path, aliases.suggestion(text))
325
+ if resolved is not None:
326
+ node.use = aliases.target_path(resolved)
327
+ try:
328
+ node.target = aliases.import_target(resolved)
329
+ except BuildError as exc:
330
+ self.fail("import_failed", str(exc), path)
331
+ if node.target is not None and aliases.is_import_path(node.use) and node.use not in self.imports:
332
+ self.imports.append(node.use)
333
+ slot_keys = SLOT_KEYS.get(slot, ())
334
+ for key, value in raw.items():
335
+ if key == "use":
336
+ continue
337
+ if not isinstance(key, str):
338
+ self.fail("bad_key", f"the key {key!r} is not a name; quote keys such as yes, no, on and off", path)
339
+ continue
340
+ if key in CALL_KEYS.get(slot, ()) and isinstance(value, str) and not value.startswith("@"):
341
+ value = {"use": value}
342
+ holder = node.keys if key in slot_keys else node.params
343
+ holder[key] = self.value(value, join(path, key), node)
344
+ if slot == "optimizer":
345
+ self.check_optimizer(node)
346
+ if node.target is not None:
347
+ self.check_signature(node)
348
+ return node
349
+
350
+ def value(self, raw, at, node):
351
+ if isinstance(raw, dict):
352
+ if "use" in raw:
353
+ child = self.make_node(at, "value", raw)
354
+ node.children.append(child)
355
+ return child
356
+ return {key: self.value(item, join(at, key), node) for key, item in raw.items()}
357
+ if isinstance(raw, (list, tuple)):
358
+ return [self.value(item, join(at, index), node) for index, item in enumerate(raw)]
359
+ if isinstance(raw, str) and raw.startswith("@"):
360
+ ref = self.reference(raw, at, node)
361
+ node.refs.append(ref)
362
+ return ref
363
+ return raw
364
+
365
+ def reference(self, text, at, node):
366
+ body = text[1:]
367
+ parts = [part for part in body.split(".") if part]
368
+ if parts and len(parts) == body.count(".") + 1:
369
+ for cut in range(len(parts), 0, -1):
370
+ path = ".".join(parts[:cut])
371
+ if path in self.known:
372
+ attr = ".".join(parts[cut:]) or None
373
+ return Ref(text, path, attr, at)
374
+ root = parts[0] if parts else ""
375
+ if root in SECTION_SLOTS or root == "train":
376
+ known = sorted(path for path in self.known if path == root or path.startswith(f"{root}."))
377
+ message = f"{at}: {text} names nothing; {root} has {known}"
378
+ else:
379
+ roots = ", ".join([*SECTION_SLOTS, "train.step"])
380
+ message = f"{at}: {text} names nothing; a reference starts with one of {roots}"
381
+ self.fail("reference_unknown", message, node.path)
382
+ return Ref(text, body, None, at)
383
+
384
+ def check_optimizer(self, node):
385
+ over = node.keys.get("over")
386
+ if "over" not in node.keys:
387
+ self.fail("missing_key", "an optimizer needs over, the modules it optimizes, such as over: '@models.net'",
388
+ node.path)
389
+ else:
390
+ items = over if isinstance(over, list) else [over]
391
+ for index, item in enumerate(items):
392
+ where = join(join(node.path, "over"), index) if isinstance(over, list) else join(node.path, "over")
393
+ if isinstance(item, dict):
394
+ if not isinstance(item.get("of"), Ref):
395
+ self.fail("reference_expected", f"{where}: a group needs of: '@models.<name>'", node.path)
396
+ elif not isinstance(item, Ref):
397
+ self.fail("reference_expected", f"{where}: expected '@models.<name>', got {item!r}", node.path)
398
+ minimizes = node.keys.get("minimizes")
399
+ if minimizes is not None and not isinstance(minimizes, Ref):
400
+ self.fail("reference_expected", f"{join(node.path, 'minimizes')}: expected '@losses.<name>', got "
401
+ f"{minimizes!r}", node.path)
402
+
403
+ def check_signature(self, node):
404
+ signature = node_signature(node.target, node.slot)
405
+ known = signature.params
406
+ label = node.use
407
+ if not signature.accepts_extra:
408
+ for key in node.params:
409
+ if key not in known:
410
+ self.fail("unknown_key", f"{label} has no param {key!r}; its params are {sorted(known)}",
411
+ node.path)
412
+ provided = set(node.params)
413
+ if node.slot == "step":
414
+ provided.update(node.keys)
415
+ missing = [name for name in signature.required if name not in provided]
416
+ if missing:
417
+ self.fail("missing_key", f"{label} needs {missing}", node.path)
418
+ if node.slot != "step":
419
+ for key in node.keys:
420
+ if key in known:
421
+ self.fail("slot_conflict", f"{key!r} is a {node.slot} slot key and also a param of {label}; "
422
+ f"the class cannot take it from the config", node.path,
423
+ "rename the constructor param")
424
+
425
+
426
+ def build(config):
427
+ data = config if isinstance(config, dict) else getattr(config, "data", None)
428
+ if not isinstance(data, dict):
429
+ data = {}
430
+ return Builder(config, data).run()
431
+
432
+
433
+ def materialize(value):
434
+ if isinstance(value, Node):
435
+ return value.obj
436
+ if isinstance(value, Ref):
437
+ return value.target
438
+ if isinstance(value, dict):
439
+ return {key: materialize(item) for key, item in value.items()}
440
+ if isinstance(value, list):
441
+ return [materialize(item) for item in value]
442
+ return value
443
+
444
+
445
+ def instantiate(tree, ctx):
446
+ if tree.errors:
447
+ raise ProblemsError(tree.errors)
448
+ built = set()
449
+ for path in tree.order:
450
+ if path not in built:
451
+ build_node(tree, ctx, tree.nodes[path], built)
452
+ for section in ("models", "losses", "metrics", "optimizers"):
453
+ table = getattr(ctx, section, None)
454
+ if isinstance(table, dict):
455
+ for name in tree.section(section):
456
+ if name in table:
457
+ table[name] = table.pop(name)
458
+ return ctx
459
+
460
+
461
+ def build_node(tree, ctx, node, built=None):
462
+ built = set() if built is None else built
463
+ built.add(node.path)
464
+ for child in node.children:
465
+ if child.path not in built:
466
+ build_node(tree, ctx, child, built)
467
+ resolve_refs(tree, ctx, [node.path], built)
468
+ if node.target is None:
469
+ raise BuildError(f"{node.path}: nothing to build, {node.use!r} did not import")
470
+ kwargs = materialize(node.params)
471
+ keys = materialize(node.keys)
472
+ signature = node_signature(node.target, node.slot)
473
+ slot = node.slot
474
+ if slot == "optimizer":
475
+ obj = build_optimizer(node, kwargs, keys, signature, ctx)
476
+ ctx.optimizers[node.name] = obj
477
+ elif slot == "plot":
478
+ if inspect.isclass(node.target):
479
+ obj = construct(node, kwargs)
480
+ label(obj, node, signature)
481
+ bind(obj, ctx)
482
+ ctx.plots[node.name] = (obj, {})
483
+ else:
484
+ obj = node.target
485
+ ctx.plots[node.name] = (obj, kwargs)
486
+ else:
487
+ if slot == "step":
488
+ kwargs = {**kwargs, **{key: value for key, value in keys.items() if key in signature.params}}
489
+ if slot == "loss" and criterion_function(node.target):
490
+ obj = functools.partial(node.target, **kwargs) if kwargs else node.target
491
+ else:
492
+ obj = construct(node, kwargs)
493
+ label(obj, node, signature)
494
+ if slot == "data":
495
+ ctx.data = obj
496
+ bind(obj, ctx)
497
+ prepare_data(obj, ctx, node)
498
+ elif slot == "model":
499
+ bind(obj, ctx)
500
+ ctx.models[node.name] = build_model(node, obj, keys, ctx)
501
+ elif slot == "loss":
502
+ bind(obj, ctx)
503
+ wrapped = wrap.wrap_loss(obj, keys, node.name, node.path)
504
+ if wrapped is not obj:
505
+ bind(wrapped, ctx)
506
+ ctx.losses[node.name] = wrapped
507
+ obj = wrapped
508
+ elif slot == "metric":
509
+ bind(obj, ctx)
510
+ wrapped = wrap.wrap_metric(obj, keys, node.name, node.path)
511
+ if wrapped is not obj:
512
+ bind(wrapped, ctx)
513
+ ctx.metrics[node.name] = wrapped
514
+ obj = wrapped
515
+ elif slot == "step":
516
+ for key in STEP_ATTRIBUTES:
517
+ if key in keys:
518
+ setattr(obj, key, keys[key])
519
+ bind(obj, ctx)
520
+ ctx.step = obj
521
+ elif slot == "sampler":
522
+ bind(obj, ctx)
523
+ ctx.generate = obj
524
+ elif slot == "logger":
525
+ if ctx.fitted_from is None:
526
+ bind(obj, ctx)
527
+ ctx.loggers.append(obj)
528
+ else:
529
+ bind(obj, ctx)
530
+ node.obj = obj
531
+ return obj
532
+
533
+
534
+ def construct(node, kwargs):
535
+ try:
536
+ obj = node.target(**coerced(node.target, kwargs))
537
+ except ProblemsError:
538
+ raise
539
+ except KalfaError as exc:
540
+ message = str(exc)
541
+ if message.startswith((f"{node.path}:", f"{node.path}.", f"{node.path}[")):
542
+ raise
543
+ raise type(exc)(f"{node.path}: {message}") from exc
544
+ except Exception as exc:
545
+ raise BuildError(f"{node.path}: {node.use} could not be built: {type(exc).__name__}: {exc}") from exc
546
+ if obj is None:
547
+ raise BuildError(f"{node.path}: {node.use} returned None; a factory returns the object to use")
548
+ return obj
549
+
550
+
551
+ def label(obj, node, signature):
552
+ for attribute, value in (("name", node.name), ("path", node.path)):
553
+ if attribute in signature.params:
554
+ continue
555
+ try:
556
+ setattr(obj, attribute, value)
557
+ except (AttributeError, TypeError):
558
+ pass
559
+
560
+
561
+ def bind(obj, ctx):
562
+ method = getattr(obj, "bind", None)
563
+ if callable(method):
564
+ method(ctx)
565
+
566
+
567
+ def prepare_data(obj, ctx, node):
568
+ source = getattr(ctx, "fitted_from", None)
569
+ if source is None:
570
+ fit(obj, ctx, node)
571
+ return
572
+ if callable(getattr(obj, "load", None)):
573
+ obj.load(str(source))
574
+ if getattr(ctx, "want_sets", True) and callable(getattr(obj, "rebuild", None)):
575
+ try:
576
+ obj.rebuild(ctx)
577
+ except KalfaError as exc:
578
+ ctx.rebuild_failure = str(exc)
579
+ ctx.logger.warning(f"the sets of {source} could not be rebuilt ({exc}); only the fitted preprocessors "
580
+ f"are loaded")
581
+
582
+
583
+ def fit(obj, ctx, node):
584
+ method = getattr(obj, "fit", None)
585
+ if not callable(method):
586
+ return
587
+ try:
588
+ method(ctx)
589
+ except KalfaError:
590
+ raise
591
+ except Exception as exc:
592
+ raise BuildError(f"{node.path}: {node.use} could not fit: {type(exc).__name__}: {exc}") from exc
593
+
594
+
595
+ def resolve_refs(tree, ctx, paths=None, built=None):
596
+ nodes = [tree.nodes[path] for path in paths] if paths is not None else list(tree.nodes.values())
597
+ resolved = {}
598
+ for node in nodes:
599
+ for ref in node.refs:
600
+ ref.target = locate(tree, ctx, ref, built)
601
+ resolved[ref.at] = ref.target
602
+ return resolved
603
+
604
+
605
+ def demand(tree, ctx, ref, built):
606
+ node = tree.nodes.get(ref.path)
607
+ if node is None or node.obj is not None or built is None:
608
+ return
609
+ if node.section != ref.at.split(".", 1)[0].split("[", 1)[0]:
610
+ return
611
+ if node.path in built:
612
+ raise BuildError(f"{ref.at}: {ref.text} is being built and refers back to itself; the references form a "
613
+ f"cycle")
614
+ build_node(tree, ctx, node, built)
615
+
616
+
617
+ def exposed(ref, base):
618
+ first = ref.attr.split(".")[0]
619
+ exposes = getattr(base, "exposes", None)
620
+ if exposes is None:
621
+ if first.startswith("_") or not hasattr(base, first):
622
+ raise BuildError(f"{ref.at}: {ref.text}: the data class has no attribute {first!r}")
623
+ return
624
+ if first not in tuple(exposes):
625
+ raise BuildError(f"{ref.at}: {ref.text} is not exposed; the data class exposes {list(exposes)}")
626
+
627
+
628
+ def locate(tree, ctx, ref, built=None):
629
+ root, _, rest = ref.path.partition(".")
630
+ if ref.path == "data":
631
+ base = ctx.data
632
+ if base is None:
633
+ raise BuildError(f"{ref.at}: {ref.text} is asked before data is built")
634
+ if ref.attr is None:
635
+ return base
636
+ exposed(ref, base)
637
+ return walk(tree, ref, base, ref.attr)
638
+ if root == "models" and rest:
639
+ entry = ctx.models.get(rest)
640
+ if entry is None:
641
+ demand(tree, ctx, ref, built)
642
+ entry = ctx.models.get(rest)
643
+ if entry is None:
644
+ raise BuildError(f"{ref.at}: {ref.text} is not built yet")
645
+ if ref.attr is None:
646
+ return entry.module
647
+ parts = ref.attr.split(".")
648
+ if parts[0] == "ema":
649
+ if entry.ema is None:
650
+ raise BuildError(f"{ref.at}: {ref.text} asks for an EMA copy but models.{rest} has no ema key")
651
+ return walk(tree, ref, entry.ema, ".".join(parts[1:])) if len(parts) > 1 else entry.ema
652
+ return walk(tree, ref, entry.module, ref.attr)
653
+ tables = {"losses": ctx.losses, "metrics": ctx.metrics, "optimizers": ctx.optimizers}
654
+ base = None
655
+ if root in tables and rest:
656
+ base = tables[root].get(rest)
657
+ elif ref.path == "generate":
658
+ base = ctx.generate
659
+ elif ref.path == "train.step":
660
+ base = ctx.step
661
+ if base is None:
662
+ demand(tree, ctx, ref, built)
663
+ node = tree.nodes.get(ref.path)
664
+ base = node.obj if node is not None else None
665
+ if base is None:
666
+ raise BuildError(f"{ref.at}: {ref.text} is not built yet")
667
+ return walk(tree, ref, base, ref.attr) if ref.attr else base
668
+
669
+
670
+ def walk(tree, ref, base, attr):
671
+ current = base
672
+ done = []
673
+ for part in attr.split("."):
674
+ done.append(part)
675
+ try:
676
+ current = getattr(current, part)
677
+ except AttributeError:
678
+ child = tree.nodes.get(f"{ref.path}.{'.'.join(done)}")
679
+ if child is not None and child.obj is not None:
680
+ current = child.obj
681
+ else:
682
+ raise BuildError(f"{ref.at}: {ref.text}: {type(current).__name__} has no attribute {part!r}") from None
683
+ return current
684
+
685
+
686
+ def param_names(node):
687
+ names = list(node.params)
688
+ if node.target is not None:
689
+ signature = node_signature(node.target, node.slot)
690
+ names.extend(name for name in signature.params if name not in names)
691
+ return names
692
+
693
+
694
+ def leaves(path, value):
695
+ yield path
696
+ if isinstance(value, dict):
697
+ for key, item in value.items():
698
+ yield from leaves(join(path, key), item)
699
+ elif isinstance(value, list):
700
+ for index, item in enumerate(value):
701
+ yield from leaves(join(path, index), item)
702
+
703
+
704
+ def mutable_paths(tree):
705
+ found = set()
706
+ if train_of(tree).get("updates"):
707
+ found.add("train.updates.total")
708
+ else:
709
+ found.add("train.epochs")
710
+ for node in tree.nodes.values():
711
+ fixed = fixed_of(node)
712
+ if node.slot == "optimizer":
713
+ found.update(f"{node.path}.{name}" for name in param_names(node))
714
+ found.add(f"{node.path}.minimizes")
715
+ elif node.slot == "model":
716
+ found.add(f"{node.path}.freeze")
717
+ elif node.slot in ("loss", "metric", "step", "trigger"):
718
+ names = param_names(node)
719
+ if node.slot == "step":
720
+ names.extend(name for name in STEP_ATTRIBUTES if name not in names)
721
+ for name in names:
722
+ if name in fixed:
723
+ continue
724
+ found.update(leaves(join(node.path, name), node.params.get(name)))
725
+ if node.slot in ("loss", "metric"):
726
+ found.update((f"{node.path}.on", f"{node.path}.every"))
727
+ return sorted(found)