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
@@ -0,0 +1,86 @@
1
+ import difflib
2
+ from pathlib import Path
3
+
4
+ from .reader import series_of, summary_of
5
+
6
+
7
+ RESOLVED = "resolved.yaml"
8
+
9
+
10
+ def differing_params(summaries):
11
+ params = [dict((summary or {}).get("params") or {}) for summary in summaries]
12
+ keys = []
13
+ for item in params:
14
+ for key in item:
15
+ if key not in keys:
16
+ keys.append(key)
17
+ shared = {}
18
+ differing = {}
19
+ for key in keys:
20
+ values = [item.get(key) for item in params]
21
+ everywhere = all(key in item for item in params)
22
+ if everywhere and all(value == values[0] for value in values[1:]):
23
+ shared[key] = values[0]
24
+ else:
25
+ differing[key] = values
26
+ return {"shared": shared, "differing": differing}
27
+
28
+
29
+ def aligned_series(directories, keys):
30
+ if isinstance(keys, str):
31
+ keys = [keys]
32
+ names = [str(directory) for directory in directories]
33
+ tables = {}
34
+ for name in names:
35
+ table = series_of(name, keys)
36
+ tables[name] = table if isinstance(table, dict) else {}
37
+ if keys is None:
38
+ keys = [key for key in tables[name] if key != "turn"]
39
+ keys = list(keys or [])
40
+ turns = set()
41
+ for table in tables.values():
42
+ turns.update(turn for turn in table.get("turn") or [] if turn is not None)
43
+ turns = sorted(turns)
44
+ aligned = {"turn": turns}
45
+ for name in names:
46
+ table = tables[name]
47
+ positions = {turn: index for index, turn in enumerate(table.get("turn") or []) if turn is not None}
48
+ aligned[name] = {}
49
+ for key in keys:
50
+ values = table.get(key) or []
51
+ aligned[name][key] = [values[positions[turn]] if turn in positions and positions[turn] < len(values)
52
+ else None for turn in turns]
53
+ return aligned
54
+
55
+
56
+ def config_of(directory):
57
+ path = Path(directory) / RESOLVED
58
+ if not path.is_file():
59
+ return None
60
+ try:
61
+ return path.read_text(encoding="utf-8", errors="replace")
62
+ except OSError:
63
+ return None
64
+
65
+
66
+ def config_diff(first, second):
67
+ texts = []
68
+ for directory in (first, second):
69
+ text = config_of(directory)
70
+ if text is None:
71
+ return None
72
+ texts.append(text.splitlines())
73
+ lines = difflib.unified_diff(texts[0], texts[1], fromfile=Path(first).name, tofile=Path(second).name,
74
+ lineterm="")
75
+ return "\n".join(lines)
76
+
77
+
78
+ def compare(directories, keys=None):
79
+ directories = list(directories)
80
+ records = [summary_of(directory) for directory in directories]
81
+ names = [Path(directory).name for directory in directories]
82
+ diffs = {}
83
+ for directory, name in zip(directories[1:], names[1:]):
84
+ diffs[f"{names[0]} vs {name}"] = config_diff(directories[0], directory)
85
+ return {"records": records, "params": differing_params(records), "series": aligned_series(directories, keys),
86
+ "diffs": diffs}
@@ -0,0 +1,561 @@
1
+ import json
2
+ import math
3
+ import os
4
+ import threading
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ import numpy
9
+
10
+
11
+ MANIFEST = "manifest.json"
12
+ PLAN = "plan.json"
13
+ HISTORY = "history.jsonl"
14
+ EVENTS = "events.jsonl"
15
+ UPDATES = "updates.jsonl"
16
+ MODEL_SUMMARY = "model_summary.json"
17
+ FIELDS = "fitted/fields.json"
18
+ PLOTS = "plots/"
19
+ IMAGES = (".png", ".svg", ".jpg", ".jpeg", ".gif", ".pdf", ".html")
20
+ SWEEP = "sweep.json"
21
+ LOG = "log.txt"
22
+ STALE_AFTER = 120.0
23
+ RECENT_TURNS = 5
24
+ AFTER = ("after", "predictions", "samples", "plots")
25
+ BLOCK = 65_536
26
+ MISSING = "-"
27
+ STAMPED = (MANIFEST, HISTORY, EVENTS, UPDATES)
28
+ FINAL = ("done", "failed", "stopped")
29
+ HISTORY_AXES = ("turn", "update", "seconds", "rules")
30
+ UPDATE_AXES = ("update", "turn")
31
+ MINIMIZES = "minimizes/"
32
+
33
+
34
+ def is_number(value):
35
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
36
+
37
+
38
+ def is_finite(value):
39
+ return is_number(value) and math.isfinite(value)
40
+
41
+
42
+ def is_record(directory):
43
+ return (Path(directory) / MANIFEST).is_file()
44
+
45
+
46
+ def is_sweep_root(directory):
47
+ return (Path(directory) / SWEEP).is_file()
48
+
49
+
50
+ def read_json(directory, name):
51
+ path = Path(directory) / name
52
+ if not path.is_file():
53
+ return None
54
+ return json.loads(path.read_text(encoding="utf-8"))
55
+
56
+
57
+ def read_lines(path):
58
+ path = Path(path)
59
+ if not path.is_file():
60
+ return []
61
+ lines = []
62
+ with path.open(encoding="utf-8") as handle:
63
+ for text in handle:
64
+ if not text.strip():
65
+ continue
66
+ try:
67
+ line = json.loads(text)
68
+ except json.JSONDecodeError:
69
+ continue
70
+ if isinstance(line, dict):
71
+ lines.append(line)
72
+ return lines
73
+
74
+
75
+ def read_manifest(directory):
76
+ return read_json(directory, MANIFEST)
77
+
78
+
79
+ def read_plan(directory):
80
+ return read_json(directory, PLAN)
81
+
82
+
83
+ def read_history(directory):
84
+ return read_lines(Path(directory) / HISTORY)
85
+
86
+
87
+ def read_events(directory):
88
+ return read_lines(Path(directory) / EVENTS)
89
+
90
+
91
+ def read_updates(directory):
92
+ return read_lines(Path(directory) / UPDATES)
93
+
94
+
95
+ def model_summary_of(directory):
96
+ return read_json(directory, MODEL_SUMMARY)
97
+
98
+
99
+ def data_report_of(directory):
100
+ return read_json(directory, "data.json")
101
+
102
+
103
+ def fields_of(directory):
104
+ try:
105
+ fields = read_json(directory, FIELDS)
106
+ except (OSError, ValueError):
107
+ return None
108
+ return fields if isinstance(fields, dict) else None
109
+
110
+
111
+ def records_under(root):
112
+ root = Path(root)
113
+ found = []
114
+ gather(root, root, found)
115
+ found.sort(key=lambda entry: entry["path"])
116
+ found.sort(key=lambda entry: str(entry["manifest"].get("started") or ""), reverse=True)
117
+ return found
118
+
119
+
120
+ def safe_json(directory, name):
121
+ try:
122
+ value = read_json(directory, name)
123
+ except (OSError, ValueError):
124
+ value = None
125
+ return value if isinstance(value, dict) else {}
126
+
127
+
128
+ def gather(root, directory, found):
129
+ if is_sweep_root(directory):
130
+ found.append({"path": directory.relative_to(root).as_posix(), "manifest": safe_json(directory, SWEEP),
131
+ "kind": "sweep"})
132
+ return
133
+ if is_record(directory):
134
+ found.append({"path": directory.relative_to(root).as_posix(), "manifest": safe_json(directory, MANIFEST),
135
+ "kind": "record"})
136
+ return
137
+ try:
138
+ with os.scandir(directory) as entries:
139
+ names = sorted(entry.name for entry in entries
140
+ if entry.is_dir(follow_symlinks=False) and not entry.name.startswith("."))
141
+ except OSError:
142
+ return
143
+ for name in names:
144
+ gather(root, directory / name, found)
145
+
146
+
147
+ def moment_of(value):
148
+ if isinstance(value, datetime):
149
+ moment = value
150
+ elif isinstance(value, str):
151
+ try:
152
+ moment = datetime.fromisoformat(value.strip())
153
+ except ValueError:
154
+ return None
155
+ else:
156
+ return None
157
+ if moment.tzinfo is not None:
158
+ moment = moment.astimezone().replace(tzinfo=None)
159
+ return moment
160
+
161
+
162
+ def state_of(manifest, now=None, stale_after=STALE_AFTER):
163
+ if not isinstance(manifest, dict):
164
+ return "unknown"
165
+ status = manifest.get("status")
166
+ if status in FINAL:
167
+ return status
168
+ if status != "running":
169
+ return "unknown"
170
+ updated = moment_of(manifest.get("updated"))
171
+ if updated is None:
172
+ return "stale"
173
+ moment = moment_of(now) if now is not None else None
174
+ if moment is None:
175
+ moment = datetime.now()
176
+ age = (moment - updated).total_seconds()
177
+ return "stale" if age > float(stale_after) else "running"
178
+
179
+
180
+ def value_of(value):
181
+ return value if is_finite(value) else None
182
+
183
+
184
+ def numeric_in(line, excluded):
185
+ return {key: value_of(value) for key, value in line.items() if key not in excluded and is_number(value)}
186
+
187
+
188
+ def turn_of(line, position):
189
+ return int(line["turn"]) if is_number(line.get("turn")) else position + 1
190
+
191
+
192
+ def normal_mode(mode):
193
+ return "max" if str(mode or "min").lower().startswith("max") else "min"
194
+
195
+
196
+ def within_of(manifest, turn):
197
+ within = manifest.get("within") if isinstance(manifest, dict) else None
198
+ if not isinstance(within, dict) or manifest.get("status") != "running":
199
+ return None
200
+ if not is_number(within.get("turn")):
201
+ return None
202
+ current, phase = int(within["turn"]), str(within.get("phase") or "train")
203
+ if current != turn + 1 and not (current == turn and phase in AFTER):
204
+ return None
205
+ done, total = within.get("done"), within.get("total")
206
+ return {"turn": current, "phase": phase, "done": int(done) if is_finite(done) else None,
207
+ "total": int(total) if is_finite(total) else None, "unit": within.get("unit"),
208
+ "started": within.get("started")}
209
+
210
+
211
+ def share_of(within):
212
+ if within is None or within["phase"] in AFTER:
213
+ return 0.0
214
+ if within["phase"] != "train":
215
+ return 1.0
216
+ done, total = within["done"], within["total"]
217
+ if done is None or not total or total <= 0:
218
+ return 0.0
219
+ return max(0.0, min(1.0, done / total))
220
+
221
+
222
+ def fraction_of(turn, total, share):
223
+ if not is_finite(total) or total <= 0:
224
+ return None
225
+ return max(0.0, min(1.0, (turn + share) / total))
226
+
227
+
228
+ def turn_seconds(manifest, history, within, share):
229
+ recent = [float(line["seconds"]) for line in history[-RECENT_TURNS:] if is_finite(line.get("seconds"))]
230
+ if recent:
231
+ return sum(recent) / len(recent)
232
+ if within is None or share <= 0:
233
+ return None
234
+ started, updated = moment_of(within.get("started")), moment_of(manifest.get("updated"))
235
+ if started is None or updated is None or updated <= started:
236
+ return None
237
+ return (updated - started).total_seconds() / share
238
+
239
+
240
+ def eta_of(manifest, history, turn, within, share):
241
+ total = manifest.get("turns_planned")
242
+ if manifest.get("status") != "running" or not is_finite(total) or total <= 0:
243
+ return None
244
+ each = turn_seconds(manifest, history, within, share)
245
+ if each is None:
246
+ return None
247
+ return max(0.0, each * (total - turn - share))
248
+
249
+
250
+ def progress_from(manifest, history):
251
+ manifest = manifest if isinstance(manifest, dict) else {}
252
+ last = history[-1] if history else None
253
+ if last is not None:
254
+ turn = turn_of(last, len(history) - 1)
255
+ elif is_number(manifest.get("turn")):
256
+ turn = int(manifest["turn"])
257
+ else:
258
+ turn = 0
259
+ seconds = sum((float(line["seconds"]) for line in history if is_finite(line.get("seconds"))), 0.0)
260
+ within = within_of(manifest, turn)
261
+ share = share_of(within)
262
+ return {"turn": turn, "total": manifest.get("turns_planned"), "status": state_of(manifest),
263
+ "manifest_status": manifest.get("status"), "updated": manifest.get("updated"), "seconds": seconds,
264
+ "last": numeric_in(last or {}, HISTORY_AXES), "unit": manifest.get("unit"), "within": within,
265
+ "fraction": fraction_of(turn, manifest.get("turns_planned"), share),
266
+ "eta": eta_of(manifest, history, turn, within, share)}
267
+
268
+
269
+ def progress_of(directory):
270
+ return progress_from(read_manifest(directory), read_history(directory))
271
+
272
+
273
+ def best_in(history, series, mode="min"):
274
+ maximize = normal_mode(mode) == "max"
275
+ best = None
276
+ for position, line in enumerate(history):
277
+ value = line.get(series)
278
+ if not is_finite(value):
279
+ continue
280
+ if best is None or (value > best[0] if maximize else value < best[0]):
281
+ best = (float(value), turn_of(line, position))
282
+ return best if best is not None else (None, None)
283
+
284
+
285
+ def watched(plan):
286
+ train = (plan or {}).get("train")
287
+ checkpoint = train.get("checkpoint") if isinstance(train, dict) else None
288
+ if isinstance(checkpoint, str):
289
+ checkpoint = {"watch": checkpoint}
290
+ if not isinstance(checkpoint, dict) or not checkpoint.get("watch"):
291
+ return None, None
292
+ return str(checkpoint["watch"]), normal_mode(checkpoint.get("mode"))
293
+
294
+
295
+ def best_from(plan, history, series=None, mode=None):
296
+ if series is None:
297
+ series, watched_mode = watched(plan)
298
+ mode = mode if mode is not None else watched_mode
299
+ if series is None:
300
+ return None
301
+ mode = normal_mode(mode)
302
+ value, turn = best_in(history, series, mode)
303
+ return {"series": str(series), "mode": mode, "value": value, "turn": turn}
304
+
305
+
306
+ def best_of(directory):
307
+ plan = read_plan(directory)
308
+ if watched(plan)[0] is None:
309
+ return None
310
+ return best_from(plan, read_history(directory))
311
+
312
+
313
+ def rules_in(history):
314
+ fired = []
315
+ for line in history:
316
+ names = line.get("rules")
317
+ if not isinstance(names, (list, tuple)):
318
+ continue
319
+ for name in names:
320
+ if isinstance(name, str) and name not in fired:
321
+ fired.append(name)
322
+ return fired
323
+
324
+
325
+ def rule_turns(history):
326
+ found = []
327
+ for position, line in enumerate(history):
328
+ names = line.get("rules")
329
+ if isinstance(names, (list, tuple)) and names:
330
+ found.append({"turn": turn_of(line, position), "update": value_of(line.get("update")),
331
+ "rules": [str(name) for name in names]})
332
+ return found
333
+
334
+
335
+ def number_word(value):
336
+ if math.isnan(value):
337
+ return "nan"
338
+ return "inf" if value > 0 else "-inf"
339
+
340
+
341
+ def nonfinite_in(history):
342
+ for position, line in enumerate(history):
343
+ bad = [key for key, value in line.items()
344
+ if key not in HISTORY_AXES and is_number(value) and not math.isfinite(value)]
345
+ if bad:
346
+ return {"turn": turn_of(line, position), "series": bad, "value": number_word(line[bad[0]])}
347
+ return None
348
+
349
+
350
+ def failure_of(directory):
351
+ try:
352
+ manifest = read_manifest(directory)
353
+ except (OSError, ValueError):
354
+ return None
355
+ failure = manifest.get("failure") if isinstance(manifest, dict) else None
356
+ if isinstance(failure, dict):
357
+ return {"type": failure.get("type"), "message": failure.get("message"),
358
+ "traceback": failure.get("traceback")}
359
+ return {"type": None, "message": str(failure), "traceback": None} if failure else None
360
+
361
+
362
+ def summary_of(directory):
363
+ manifest = read_manifest(directory)
364
+ manifest = manifest if isinstance(manifest, dict) else {}
365
+ plan = read_plan(directory)
366
+ history = read_history(directory)
367
+ progress = progress_from(manifest, history)
368
+ last = history[-1] if history else {}
369
+ host = manifest.get("host")
370
+ failure = manifest.get("failure")
371
+ return {
372
+ "path": str(directory),
373
+ "name": manifest.get("name") or Path(directory).name,
374
+ "status": progress["status"],
375
+ "unit": progress["unit"],
376
+ "turn": progress["turn"],
377
+ "total": progress["total"],
378
+ "started": manifest.get("started"),
379
+ "updated": progress["updated"],
380
+ "finished": manifest.get("finished"),
381
+ "seconds": progress["seconds"],
382
+ "params": dict(manifest.get("params") or {}),
383
+ "best": best_from(plan, history),
384
+ "last": progress["last"],
385
+ "minimizes": {key[len(MINIMIZES):]: value for key, value in last.items()
386
+ if key.startswith(MINIMIZES) and isinstance(value, str)},
387
+ "rules": rules_in(history),
388
+ "device": manifest.get("device"),
389
+ "host": host.get("hostname") if isinstance(host, dict) else host if isinstance(host, str) else None,
390
+ "failure": failure.get("message") if isinstance(failure, dict) else str(failure) if failure else None,
391
+ "sweep": manifest.get("sweep") if isinstance(manifest.get("sweep"), dict) else None,
392
+ "resumed_from": manifest.get("resumed_from"),
393
+ "stop_reason": manifest.get("stop_reason"),
394
+ "stopped_by": manifest.get("stopped_by"),
395
+ "within": progress["within"],
396
+ "fraction": progress["fraction"],
397
+ "eta": progress["eta"],
398
+ "nonfinite": nonfinite_in(history),
399
+ }
400
+
401
+
402
+ def numeric_keys(lines, excluded):
403
+ keys = []
404
+ for line in lines:
405
+ for key, value in line.items():
406
+ if key not in excluded and key not in keys and is_number(value):
407
+ keys.append(key)
408
+ return keys
409
+
410
+
411
+ def thinned(count, limit):
412
+ if not limit or limit < 1 or count <= limit:
413
+ return range(count)
414
+ positions = numpy.unique(numpy.rint(numpy.linspace(0, count - 1, num=int(limit))).astype(int))
415
+ return [int(position) for position in positions]
416
+
417
+
418
+ def history_axis(line, position, axis):
419
+ return turn_of(line, position)
420
+
421
+
422
+ def update_axis(line, position, axis):
423
+ return value_of(line.get(axis))
424
+
425
+
426
+ def table_of(lines, axes, axis_of, excluded, keys, limit):
427
+ if isinstance(keys, str):
428
+ keys = [keys]
429
+ keys = numeric_keys(lines, excluded) if keys is None else [str(key) for key in keys]
430
+ positions = thinned(len(lines), limit)
431
+ table = {axis: [] for axis in axes}
432
+ for key in keys:
433
+ table.setdefault(key, [])
434
+ for position in positions:
435
+ line = lines[position]
436
+ for axis in axes:
437
+ table[axis].append(axis_of(line, position, axis))
438
+ for key in keys:
439
+ table[key].append(value_of(line.get(key)))
440
+ return table
441
+
442
+
443
+ def series_of(directory, keys=None, limit=2000):
444
+ return table_of(read_history(directory), ("turn",), history_axis, HISTORY_AXES, keys, limit)
445
+
446
+
447
+ def updates_of(directory, keys=None, limit=2000):
448
+ return table_of(read_updates(directory), UPDATE_AXES, update_axis, UPDATE_AXES, keys, limit)
449
+
450
+
451
+ def events_of(directory, kinds=None, limit=None):
452
+ events = read_events(directory)
453
+ if kinds is not None:
454
+ wanted = {kinds} if isinstance(kinds, str) else set(kinds)
455
+ events = [event for event in events if event.get("kind") in wanted]
456
+ if limit:
457
+ events = events[-int(limit):]
458
+ return events
459
+
460
+
461
+ def files_of(directory):
462
+ root = Path(directory)
463
+ if not root.is_dir():
464
+ return []
465
+ found = []
466
+ for current, names, files in os.walk(root):
467
+ names[:] = sorted(name for name in names if not name.startswith("."))
468
+ for name in files:
469
+ if name.startswith("."):
470
+ continue
471
+ path = Path(current) / name
472
+ try:
473
+ info = path.stat()
474
+ except OSError:
475
+ continue
476
+ found.append({"path": path.relative_to(root).as_posix(), "size": int(info.st_size),
477
+ "modified": datetime.fromtimestamp(info.st_mtime).isoformat(timespec="seconds")})
478
+ found.sort(key=lambda entry: entry["path"])
479
+ return found
480
+
481
+
482
+ def plots_of(directory):
483
+ found = []
484
+ for entry in files_of(directory):
485
+ path = entry["path"]
486
+ if path.startswith(PLOTS) and path.lower().endswith(IMAGES):
487
+ found.append({"name": Path(path[len(PLOTS):]).with_suffix("").as_posix(), **entry})
488
+ return found
489
+
490
+
491
+ def last_lines(path, count):
492
+ if count <= 0:
493
+ return b""
494
+ with Path(path).open("rb") as handle:
495
+ position = handle.seek(0, os.SEEK_END)
496
+ blocks = []
497
+ newlines = 0
498
+ while position > 0 and newlines <= count:
499
+ step = min(BLOCK, position)
500
+ position -= step
501
+ handle.seek(position)
502
+ block = handle.read(step)
503
+ newlines += block.count(b"\n")
504
+ blocks.append(block)
505
+ data = b"".join(reversed(blocks))
506
+ return b"".join(data.splitlines(keepends=True)[-count:])
507
+
508
+
509
+ def log_of(directory, count=200):
510
+ path = Path(directory) / LOG
511
+ try:
512
+ data = last_lines(path, count) if path.is_file() else b""
513
+ except OSError:
514
+ return []
515
+ return data.decode("utf-8", errors="replace").splitlines()
516
+
517
+
518
+ def file_stamp(path):
519
+ try:
520
+ info = Path(path).stat()
521
+ except OSError:
522
+ return MISSING
523
+ return f"{info.st_mtime_ns}:{info.st_size}"
524
+
525
+
526
+ def record_stamp(directory, manifest):
527
+ directory = Path(directory)
528
+ manifest = manifest if isinstance(manifest, dict) else {}
529
+ files = "|".join(file_stamp(directory / name) for name in STAMPED)
530
+ return f"{manifest.get('updated')}|{state_of(manifest)}|{files}"
531
+
532
+
533
+ class Cache:
534
+ def __init__(self, limit=8192):
535
+ self.limit = int(limit)
536
+ self.kept = {}
537
+ self.lock = threading.Lock()
538
+ self.hits = 0
539
+ self.misses = 0
540
+
541
+ def get(self, key, stamp, compute):
542
+ with self.lock:
543
+ found = self.kept.get(key)
544
+ if found is not None and found[0] == stamp:
545
+ self.hits += 1
546
+ return found[1]
547
+ self.misses += 1
548
+ value = compute()
549
+ with self.lock:
550
+ if len(self.kept) >= self.limit:
551
+ self.kept.clear()
552
+ self.kept[key] = (stamp, value)
553
+ return value
554
+
555
+
556
+ def summary_with(cache, directory, manifest=None):
557
+ if cache is None:
558
+ return summary_of(directory)
559
+ if manifest is None:
560
+ manifest = safe_json(directory, MANIFEST)
561
+ return cache.get(("summary", str(directory)), record_stamp(directory, manifest), lambda: summary_of(directory))
@@ -0,0 +1,36 @@
1
+ from ..reader import read_plan
2
+ from .choices import HISTOGRAMS, MAX_BINS, prediction_stats
3
+ from .classes import best_threshold, classification_stats, positive_column
4
+ from .measures import histogram, regression_stats, values_stats
5
+ from .pairs import (
6
+ classes_for,
7
+ fitted_classes,
8
+ kind_of,
9
+ label_columns,
10
+ plan_pairings,
11
+ plan_targets,
12
+ prediction_pairs,
13
+ score_labels,
14
+ spread_member,
15
+ task_pairs,
16
+ )
17
+ from .tables import (
18
+ IMAGE_SUFFIXES,
19
+ IMAGE_TYPES,
20
+ PREDICTIONS,
21
+ image_column,
22
+ label_text,
23
+ labels,
24
+ load_table,
25
+ ordered_labels,
26
+ prediction_tables,
27
+ read_fitted_fields,
28
+ read_predictions,
29
+ )
30
+
31
+
32
+ __all__ = ["HISTOGRAMS", "IMAGE_SUFFIXES", "IMAGE_TYPES", "MAX_BINS", "PREDICTIONS", "best_threshold", "classes_for",
33
+ "classification_stats", "fitted_classes", "histogram", "image_column", "kind_of", "label_columns",
34
+ "label_text", "labels", "load_table", "ordered_labels", "plan_pairings", "plan_targets", "positive_column",
35
+ "prediction_pairs", "prediction_stats", "prediction_tables", "read_fitted_fields", "read_plan",
36
+ "read_predictions", "regression_stats", "score_labels", "spread_member", "task_pairs", "values_stats"]