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/run.py ADDED
@@ -0,0 +1,568 @@
1
+ import contextlib
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import signal
6
+ import socket
7
+ import sys
8
+ import threading
9
+ import traceback
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+
14
+ import pandas
15
+ import torch
16
+
17
+ from ..records.reader import read_lines
18
+ from . import build, loop, predict
19
+ from .aliases import add_search_dir
20
+ from .check import check
21
+ from .context import Context
22
+ from .device import Device
23
+ from .errors import KalfaError, ProblemsError, RunError
24
+ from .history import progress_text
25
+ from .record import Pulse, RecordWriter, atomic, manifest_files, now, start_record
26
+ from .rng import Rng
27
+ from .rules import Rule, replay
28
+ from .spec import parse_param, parse_set
29
+ from .state import checkpoint_path, load_checkpoint
30
+
31
+
32
+ CHECKPOINTS = ("best", "last", "final")
33
+ TABLES = (".parquet", ".pq", ".csv", ".tsv")
34
+
35
+
36
+ @dataclass
37
+ class Result:
38
+ record: str
39
+ history: list
40
+ context: object
41
+
42
+
43
+ class Monitor:
44
+ def __init__(self, stream=None, quiet=False):
45
+ self.stream = stream
46
+ self.quiet = quiet
47
+ self.total = None
48
+
49
+ def expect(self, total):
50
+ self.total = total
51
+
52
+ def say(self, text):
53
+ if not self.quiet:
54
+ print(text, file=self.stream or sys.stderr, flush=True)
55
+
56
+ def turn(self, line):
57
+ self.say(progress_text(line, self.total))
58
+
59
+
60
+ def stamp():
61
+ return datetime.now().strftime("%Y%m%d_%H%M%S")
62
+
63
+
64
+ def default_record(paths):
65
+ files = [str(path) for path in paths if not str(path).startswith("<")]
66
+ stem = Path(files[0]).stem if files else "run"
67
+ return f"runs/{stem}_{stamp()}"
68
+
69
+
70
+ def pairs(items, kind="set"):
71
+ parser = parse_param if kind == "param" else parse_set
72
+ flag = "-p" if kind == "param" else "--set"
73
+ found = []
74
+ if isinstance(items, dict):
75
+ items = [items]
76
+ for item in items or ():
77
+ if isinstance(item, str):
78
+ found.append(tuple(parser(item)))
79
+ elif isinstance(item, (tuple, list)) and len(item) == 2:
80
+ found.append((str(item[0]), item[1]))
81
+ elif isinstance(item, dict):
82
+ found.extend((str(key), value) for key, value in item.items())
83
+ else:
84
+ raise KalfaError(f"{flag} takes 'NAME=VALUE' or a (name, value) pair, got {item!r}")
85
+ return found
86
+
87
+
88
+ def config_files(checked, paths=()):
89
+ given = [str(path) for path in paths]
90
+ layers = getattr(checked.config, "layers", None) or []
91
+ given.extend(str(file) for file in (getattr(layer, "file", None) for layer in layers) if file)
92
+ for file in list(given):
93
+ if Path(file).name == "resolved.yaml":
94
+ given.extend(manifest_files(Path(file).parent))
95
+ files, seen = [], set()
96
+ for file in given:
97
+ key = str(Path(file).resolve())
98
+ if key not in seen:
99
+ seen.add(key)
100
+ files.append(file)
101
+ return files
102
+
103
+
104
+ def note_default(ctx, path, key, value):
105
+ nodes = getattr(ctx.tree, "nodes", None) or {}
106
+ node = nodes.get(path)
107
+ if node is not None and isinstance(node.keys, dict) and node.keys.get(key) is None:
108
+ node.keys[key] = value
109
+ planned = ctx.plan.get("nodes") if isinstance(ctx.plan, dict) else None
110
+ entry = planned.get(path) if isinstance(planned, dict) else None
111
+ if isinstance(entry, dict) and isinstance(entry.get("keys"), dict) and entry["keys"].get(key) is None:
112
+ entry["keys"][key] = value
113
+
114
+
115
+ def fill_defaults(ctx):
116
+ predicts = ctx.predicts()
117
+ targets = predict.target_names(ctx.data)
118
+ for section, entries in (("losses", ctx.losses), ("metrics", ctx.metrics)):
119
+ for name, entry in entries.items():
120
+ if not hasattr(entry, "pred") or not hasattr(entry, "target"):
121
+ continue
122
+ path = f"{section}.{name}"
123
+ if entry.pred is None:
124
+ if predicts is None or not getattr(predicts, "outputs", None):
125
+ raise RunError(f"{path}: pred_required: there is no predicts model whose first output could be "
126
+ f"pred; name pred")
127
+ entry.pred = predicts.outputs[0]
128
+ note_default(ctx, path, "pred", entry.pred)
129
+ if entry.target is None:
130
+ if len(targets) != 1:
131
+ raise RunError(f"{path}: target_required: the data has {len(targets)} targets, not one; name "
132
+ f"target")
133
+ entry.target = targets[0]
134
+ note_default(ctx, path, "target", entry.target)
135
+ if section == "metrics" and getattr(entry, "rescale", None) is None:
136
+ entry.rescale = predict.rescales(ctx.data, entry.target)
137
+ note_default(ctx, path, "rescale", entry.rescale)
138
+
139
+
140
+ def node_object(tree, path):
141
+ node = tree.nodes.get(path)
142
+ return None if node is None else node.obj
143
+
144
+
145
+ def build_rules(ctx, tree, config):
146
+ ctx.rules = []
147
+ ctx.rules_by_name = {}
148
+ for spec in tree.rules:
149
+ rule = Rule(spec, when=node_object(tree, f"rules.{spec.name}.when"),
150
+ run=node_object(tree, f"rules.{spec.name}.run"))
151
+ ctx.rules.append(rule)
152
+ ctx.rules_by_name[spec.name] = rule
153
+
154
+
155
+ def place(ctx):
156
+ device = ctx.device.torch
157
+ for entry in ctx.models.values():
158
+ module = getattr(entry, "module", entry)
159
+ module.to(device)
160
+ ema = getattr(entry, "ema", None)
161
+ if isinstance(ema, torch.nn.Module):
162
+ ema.to(device)
163
+ for objective in ctx.losses.values():
164
+ if isinstance(objective, torch.nn.Module):
165
+ objective.to(device)
166
+ for metric in ctx.metrics.values():
167
+ mover = getattr(metric, "to", None)
168
+ if callable(mover):
169
+ mover(device)
170
+ amp = bool(getattr(ctx.step, "amp", False))
171
+ for entry in ctx.optimizers.values():
172
+ if amp:
173
+ scaler = ctx.device.scaler(True)
174
+ if scaler is not None:
175
+ entry.scaler = scaler
176
+ if getattr(entry, "real", None) is None:
177
+ entry.build()
178
+
179
+
180
+ def save_fitted(ctx, directory):
181
+ saver = getattr(ctx.data, "save", None)
182
+ if callable(saver) and not (Path(directory) / "fitted").exists():
183
+ saver(str(directory))
184
+
185
+
186
+ def data_document(ctx):
187
+ from kalfa.std.data import flow
188
+
189
+ data = ctx.data
190
+ report = None
191
+ reporter = getattr(data, "report", None)
192
+ if callable(reporter):
193
+ try:
194
+ report = reporter()
195
+ except Exception as exc:
196
+ ctx.logger.warning(f"data.json: the data report failed ({type(exc).__name__}: {exc})")
197
+ try:
198
+ pipeline = flow.pipeline_of(data)
199
+ except Exception as exc:
200
+ pipeline = None
201
+ ctx.logger.warning(f"data.json: the data flowchart could not be drawn ({type(exc).__name__}: {exc}); the "
202
+ f"board shows the data tables without it")
203
+ return flow.jsonable({"format": 1, "kind": flow.kind_of(data), "report": report, "pipeline": pipeline})
204
+
205
+
206
+ def save_data(ctx):
207
+ if ctx.record is not None and getattr(ctx, "data", None) is not None:
208
+ ctx.record.write_json("data.json", data_document(ctx))
209
+
210
+
211
+ def unused_directory(directory):
212
+ directory = Path(directory)
213
+ if not directory.exists() or not any(directory.iterdir()):
214
+ return directory
215
+ base = f"{str(directory).rstrip('/')}_{stamp()}"
216
+ chosen = Path(base)
217
+ count = 1
218
+ while chosen.exists():
219
+ count += 1
220
+ chosen = Path(f"{base}_{count}")
221
+ logging.getLogger("kalfa.run").warning(f"record {directory} already holds a run; writing {chosen}")
222
+ return chosen
223
+
224
+
225
+ def prepare(checked, record=None, monitor=None, parent=None, paths=(), fitted=None, sets=True, sweep=None):
226
+ if checked.errors:
227
+ raise ProblemsError(checked.problems)
228
+ tree = checked.tree
229
+ config = checked.config.data
230
+ files = config_files(checked, paths)
231
+ ctx = Context(tree, checked.plan)
232
+ ctx.monitor = monitor if monitor is not None else Monitor(quiet=True)
233
+ ctx.seed = int(config.get("seed") or 0)
234
+ ctx.rng = Rng(ctx.seed)
235
+ ctx.rng.seed_everything()
236
+ ctx.device = Device(config.get("device") or "auto")
237
+ ctx.fitted_from = fitted
238
+ ctx.want_sets = sets
239
+ directory = None if record is False else Path(record or config.get("record") or default_record(files))
240
+ if directory is not None and parent is None and sweep is None:
241
+ directory = unused_directory(directory)
242
+ build.instantiate(tree, ctx)
243
+ fill_defaults(ctx)
244
+ build_rules(ctx, tree, config)
245
+ place(ctx)
246
+ expect = getattr(ctx.monitor, "expect", None)
247
+ if callable(expect):
248
+ expect(ctx.train.total_turns())
249
+ if directory is not None:
250
+ writer = start_record(ctx, directory, files, checked.plan, parent=parent, sweep=sweep)
251
+ if ctx.record is None:
252
+ ctx.record = writer if isinstance(writer, RecordWriter) else RecordWriter(directory)
253
+ save_fitted(ctx, directory)
254
+ save_data(ctx)
255
+ say = getattr(ctx.monitor, "say", None)
256
+ if callable(say):
257
+ say(f"record {directory}")
258
+ return ctx
259
+
260
+
261
+ @contextlib.contextmanager
262
+ def interruptible(ctx):
263
+ if threading.current_thread() is not threading.main_thread():
264
+ yield
265
+ return
266
+ asked = []
267
+
268
+ def handle(number, frame):
269
+ if asked:
270
+ raise KeyboardInterrupt
271
+ asked.append(number)
272
+ ctx.stop("keyboard")
273
+ ctx.stopped_by = "keyboard"
274
+ say = getattr(ctx.monitor, "say", None)
275
+ if callable(say):
276
+ say("stop requested: the run ends after this turn with its final state and predictions; ctrl-c again "
277
+ "aborts it now")
278
+
279
+ try:
280
+ previous = signal.signal(signal.SIGINT, handle)
281
+ except ValueError:
282
+ yield
283
+ return
284
+ try:
285
+ yield
286
+ finally:
287
+ signal.signal(signal.SIGINT, previous)
288
+
289
+
290
+ def finish(ctx, status):
291
+ if ctx.record is not None:
292
+ extra = {}
293
+ if status == "stopped" and getattr(ctx, "stopping", None):
294
+ extra = {"stop_reason": ctx.stopping, "stopped_by": getattr(ctx, "stopped_by", None)}
295
+ ctx.record.manifest(status=status, finished=now(), **extra)
296
+
297
+
298
+ def note_record(ctx, exc):
299
+ if ctx.record is not None:
300
+ exc.add_note(f"record {ctx.record.directory}")
301
+
302
+
303
+ class RecordLog(logging.Handler):
304
+ def __init__(self, record):
305
+ super().__init__(level=logging.INFO)
306
+ self.record = record
307
+
308
+ def emit(self, entry):
309
+ try:
310
+ self.record.log(f"{entry.levelname.lower()}: {entry.getMessage()}")
311
+ except Exception:
312
+ self.handleError(entry)
313
+
314
+
315
+ @contextlib.contextmanager
316
+ def followed(ctx):
317
+ record = ctx.record
318
+ if record is None:
319
+ yield
320
+ return
321
+ logger = logging.getLogger("kalfa")
322
+ handler = RecordLog(record)
323
+ logger.addHandler(handler)
324
+ record.log(f"run on {socket.gethostname()}, pid {os.getpid()}, device {getattr(ctx, 'device', None)}")
325
+ try:
326
+ with Pulse(record):
327
+ yield
328
+ finally:
329
+ logger.removeHandler(handler)
330
+
331
+
332
+ def train_into(ctx):
333
+ with followed(ctx):
334
+ return trained(ctx)
335
+
336
+
337
+ def trained(ctx):
338
+ history = None
339
+ try:
340
+ with interruptible(ctx):
341
+ history = loop.train(ctx)
342
+ loop.after(ctx)
343
+ finish(ctx, "done" if ctx.stopping is None else "stopped")
344
+ except KeyboardInterrupt as exc:
345
+ if ctx.record is not None:
346
+ ctx.record.event("stop", turn=ctx.turn, by="keyboard", aborted=True)
347
+ ctx.record.log(f"aborted from the keyboard in turn {ctx.turn}")
348
+ finish(ctx, "stopped")
349
+ note_record(ctx, exc)
350
+ raise
351
+ except Exception as exc:
352
+ if ctx.record is not None:
353
+ failure = {"type": type(exc).__name__, "message": str(exc), "traceback": traceback.format_exc()}
354
+ ctx.record.event("error", turn=ctx.turn, type=failure["type"], message=failure["message"])
355
+ ctx.record.manifest(status="failed", finished=now(), failure=failure)
356
+ ctx.record.log(f"failed in turn {ctx.turn}: {failure['type']}: {failure['message']}")
357
+ ctx.record.log(failure["traceback"].rstrip())
358
+ note_record(ctx, exc)
359
+ raise
360
+ if ctx.record is not None:
361
+ ctx.record.log(f"{'done' if ctx.stopping is None else 'stopped: ' + str(ctx.stopping)} after turn {ctx.turn}")
362
+ if history is None:
363
+ history = ctx.history
364
+ record = str(ctx.record.directory) if ctx.record is not None else None
365
+ return Result(record=record, history=list(history), context=ctx)
366
+
367
+
368
+ def run(paths=(), sets=(), params=(), mappings=(), record=None, monitor=None, resume_from=None, sweep=None):
369
+ if resume_from is not None:
370
+ return resume(resume_from, sets=sets, params=params, target=record, monitor=monitor, paths=paths)
371
+ files = tuple(str(path) for path in paths)
372
+ checked = check(paths=files, sets=pairs(sets), params=pairs(params, "param"), mappings=list(mappings))
373
+ ctx = prepare(checked, record, monitor, paths=files, sweep=sweep)
374
+ return train_into(ctx)
375
+
376
+
377
+ def config_directories(source):
378
+ found = []
379
+ for file in manifest_files(source):
380
+ directory = Path(file).parent
381
+ if directory.is_dir() and directory not in found:
382
+ found.append(directory)
383
+ return found
384
+
385
+
386
+ def record_root(record):
387
+ source = Path(record)
388
+ if not (source / "resolved.yaml").exists():
389
+ raise KalfaError(f"{record} has no resolved.yaml; it is no record directory")
390
+ plugins = source / "plugins"
391
+ if plugins.is_dir():
392
+ add_search_dir(plugins)
393
+ for directory in config_directories(source):
394
+ add_search_dir(directory)
395
+ return source
396
+
397
+
398
+ def carry_lines(source, ctx, upto):
399
+ for line in read_lines(source / "history.jsonl"):
400
+ turn = line.get("turn")
401
+ if isinstance(turn, (int, float)) and turn <= upto:
402
+ ctx.history.append(line)
403
+ ctx.record.append("history.jsonl", line)
404
+ for line in read_lines(source / "updates.jsonl"):
405
+ turn = line.get("turn")
406
+ if isinstance(turn, (int, float)) and turn <= upto:
407
+ ctx.record.append("updates.jsonl", line)
408
+
409
+
410
+ def fresh_directory(source, target):
411
+ directory = Path(target) if target else Path(f"{str(source).rstrip('/')}_{stamp()}")
412
+ if directory.exists() and any(directory.iterdir()):
413
+ same = directory.resolve() == source.resolve()
414
+ what = "the record itself" if same else "a directory that is not empty"
415
+ raise RunError(f"a resumed run continues into a new directory, not into {what} ({directory}); name an empty "
416
+ f"one with --record or leave it out for {source}_<stamp>")
417
+ return directory
418
+
419
+
420
+ def carry_best(source, ctx):
421
+ best = source / "checkpoints" / "best.pt"
422
+ if best.exists():
423
+ target = ctx.record.path("checkpoints/best.pt")
424
+ target.parent.mkdir(parents=True, exist_ok=True)
425
+ shutil.copyfile(best, target)
426
+
427
+
428
+ def carry_plugins(source, ctx):
429
+ plugins = source / "plugins"
430
+ if plugins.is_dir():
431
+ shutil.copytree(plugins, ctx.record.path("plugins"), ignore=shutil.ignore_patterns("__pycache__"),
432
+ dirs_exist_ok=True)
433
+
434
+
435
+ def carry_params(source, ctx):
436
+ before = (RecordWriter(source).read_json("manifest.json") or {}).get("params")
437
+ asked = dict(getattr(ctx.config, "params", None) or {})
438
+ ctx.record.manifest(params={**(before if isinstance(before, dict) else {}), **asked})
439
+
440
+
441
+ def resume(record, sets=(), params=(), target=None, monitor=None, paths=()):
442
+ source = record_root(record)
443
+ checkpoint = checkpoint_path(source, None)
444
+ directory = fresh_directory(source, target)
445
+ files = tuple(str(path) for path in paths) or (str(source / "resolved.yaml"),)
446
+ checked = check(paths=files, sets=pairs(sets), params=pairs(params, "param"))
447
+ ctx = prepare(checked, directory, monitor, parent=str(source), paths=files)
448
+ carry_params(source, ctx)
449
+ carry_plugins(source, ctx)
450
+ load_checkpoint(ctx, checkpoint, counters=True, rng=True)
451
+ upto = int(ctx.turn)
452
+ replay(ctx, read_lines(source / "events.jsonl"), upto)
453
+ carry_lines(source, ctx, upto)
454
+ carry_best(source, ctx)
455
+ loop.replan(ctx, None)
456
+ ctx.record.event("resume", turn=upto, source=str(source), checkpoint=checkpoint.name)
457
+ return train_into(ctx)
458
+
459
+
460
+ def report_choice(ctx, source):
461
+ if ctx.train.report == "best" and (source / "checkpoints" / "best.pt").exists():
462
+ return "best"
463
+ return None
464
+
465
+
466
+ def open_record(record, which=None, sets=(), params=(), device=None, data=True):
467
+ source = record_root(record)
468
+ if which is not None and which not in CHECKPOINTS:
469
+ raise KalfaError(f"which must be best, last or final, got {which!r}")
470
+ layer = pairs(sets)
471
+ if device is not None:
472
+ layer.append(("device", device))
473
+ files = (str(source / "resolved.yaml"),)
474
+ checked = check(paths=files, sets=layer, params=pairs(params, "param"))
475
+ if checked.errors:
476
+ raise ProblemsError(checked.problems)
477
+ ctx = prepare(checked, False, None, paths=files, fitted=source, sets=data)
478
+ path = checkpoint_path(source, which or report_choice(ctx, source))
479
+ load_checkpoint(ctx, path, only=["models"], counters=False, rng=False)
480
+ ctx.set_modes(False)
481
+ return ctx
482
+
483
+
484
+ def model_entry(ctx, model=None):
485
+ if model is None:
486
+ entry = ctx.predicts()
487
+ if entry is None:
488
+ raise KalfaError("the run has several models and no train.predicts; name one with model")
489
+ return entry
490
+ if model not in ctx.models:
491
+ raise KalfaError(f"model {model!r} is not in the models section, which has {sorted(ctx.models)}")
492
+ return ctx.models[model]
493
+
494
+
495
+ def predict_frame(ctx, loader, frame=None, entry=None):
496
+ return predict.predictions_table(ctx, "test", loader=loader, frame=frame, entry=entry)
497
+
498
+
499
+ def read_frame(path):
500
+ suffix = Path(path).suffix.lower()
501
+ if suffix in (".parquet", ".pq"):
502
+ return pandas.read_parquet(path)
503
+ if suffix == ".tsv":
504
+ return pandas.read_csv(path, sep="\t")
505
+ return pandas.read_csv(path)
506
+
507
+
508
+ def source_of(data):
509
+ if isinstance(data, pandas.DataFrame):
510
+ return data, data, "_frame"
511
+ path = Path(str(data)).expanduser()
512
+ if not path.exists():
513
+ raise KalfaError(f"{data} does not exist; predict takes a .parquet or .csv table, or what the data class "
514
+ f"reads, such as a folder of images or a text file")
515
+ if path.is_file() and path.suffix.lower() in TABLES:
516
+ frame = read_frame(path)
517
+ return frame, frame, f"_{path.stem}"
518
+ return None, str(path), f"_{path.stem or path.name}"
519
+
520
+
521
+ def predict_record(record, data=None, model=None, which=None, device=None):
522
+ ctx = open_record(record, which=which, device=device, data=data is None)
523
+ if not getattr(ctx.data, "predictions", True):
524
+ raise KalfaError(f"{record}: the data class {type(ctx.data).__name__} writes no predictions table; kalfa "
525
+ f"generate draws samples from a record whose config has a generate section")
526
+ source = Path(record)
527
+ entry = None if model is None else model_entry(ctx, model)
528
+ if data is None:
529
+ if "test" not in ctx.loaders():
530
+ raise KalfaError(f"{record}: the run has no test set to predict on; predict on a file with data")
531
+ table = predict.predictions_table(ctx, "test", entry=entry)
532
+ tag = f"_{checkpoint_path(source, which or report_choice(ctx, source)).stem}"
533
+ else:
534
+ frame, given, tag = source_of(data)
535
+ loader = ctx.data.loader_for(given)
536
+ if frame is None:
537
+ frame = getattr(getattr(loader, "dataset", None), "frame", None)
538
+ table = predict_frame(ctx, loader, frame, entry)
539
+ if model is not None:
540
+ tag += f"_{model}"
541
+ if not len(table):
542
+ raise KalfaError(f"{record}: there is nothing to predict; the model gave no outputs")
543
+ path = source / f"predictions{tag}.parquet"
544
+ with atomic(path) as temporary:
545
+ table.to_parquet(temporary, index=False)
546
+ return path, table
547
+
548
+
549
+ def chosen_plots(ctx, only):
550
+ if only is None or only == "all":
551
+ return list(ctx.plots)
552
+ names = [name for name in only.split(",") if name] if isinstance(only, str) else list(only)
553
+ unknown = [name for name in names if name not in ctx.plots]
554
+ if unknown:
555
+ raise KalfaError(f"plots {unknown} are not in the plots section, which has {sorted(ctx.plots)}")
556
+ return names
557
+
558
+
559
+ def draw_plots(ctx, record, only=None):
560
+ names = chosen_plots(ctx, only)
561
+ plots, writer = ctx.plots, ctx.record
562
+ ctx.plots = {name: plots[name] for name in names}
563
+ if ctx.record is None:
564
+ ctx.record = RecordWriter(record)
565
+ try:
566
+ return loop.draw_plots(ctx)
567
+ finally:
568
+ ctx.plots, ctx.record = plots, writer