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,356 @@
1
+ import numbers
2
+ from pathlib import Path
3
+
4
+ from matplotlib.ticker import MaxNLocator
5
+
6
+ from kalfa import summary as summarizing
7
+ from kalfa.commands.report.page import (
8
+ color_of,
9
+ embedded,
10
+ escape,
11
+ finite,
12
+ grid,
13
+ new_figure,
14
+ paragraph,
15
+ plural,
16
+ shown,
17
+ table,
18
+ )
19
+ from kalfa.commands.report.predictions import predictions_part
20
+ from kalfa.core.errors import KalfaError
21
+ from kalfa.core.record import now, version
22
+ from kalfa.records import reader
23
+ from kalfa.records import sweep as sweeping
24
+ from kalfa.std.plots import LEGEND_LIMIT, dress, spans_decades
25
+
26
+
27
+ CURVES = 8
28
+ DETAILS = 20
29
+ LISTED = 6
30
+ KNOWN = 12
31
+ CURVE_PREFIXES = ("val/", "train/")
32
+
33
+
34
+ def sources(summary):
35
+ if summary.sweep is not None:
36
+ for record in summary.records:
37
+ if isinstance(record.get("sweep"), dict):
38
+ return [str(Path(record["path"]).parent)]
39
+ return [str(record["path"]) for record in summary.records]
40
+
41
+
42
+ def title_of(summary):
43
+ if summary.sweep is not None and summary.records:
44
+ return f"kalfa report: {Path(sources(summary)[0]).name}"
45
+ if len(summary.records) == 1:
46
+ return f"kalfa report: {summary.records[0].get('name')}"
47
+ return f"kalfa report: {plural(len(summary.records), 'record')}"
48
+
49
+
50
+ def heading(summary, title, sections):
51
+ paths = sources(summary)
52
+ listed = ", ".join(paths[:LISTED])
53
+ if len(paths) > LISTED:
54
+ listed += f" and {len(paths) - LISTED} more"
55
+ links = "".join(f'<a href="#{key}">{escape(name)}</a>' for key, name in sections)
56
+ return (f"<header><h1>{escape(title)}</h1>"
57
+ f"{paragraph(f'written {now()} by kalfa {version()}', 'muted')}"
58
+ f"{paragraph(listed, 'muted')}<nav>{links}</nav></header>")
59
+
60
+
61
+ def ranked(summary):
62
+ pool = list(summary.records)
63
+ names = summary.table["name"].tolist() if "name" in summary.table.columns else []
64
+ ordered = []
65
+ for name in names:
66
+ for position, record in enumerate(pool):
67
+ if record.get("name") == name:
68
+ ordered.append(pool.pop(position))
69
+ break
70
+ return ordered
71
+
72
+
73
+ def best_line(summary):
74
+ if summary.by is None:
75
+ return "no series to rank by"
76
+ best = summary.best
77
+ if best is None:
78
+ return f"no record has a finite {summary.by} yet"
79
+ turn = f" at turn {shown(best.get('best_turn'))}" if best.get("best_turn") is not None else ""
80
+ return f"best: {best.get('name')}, {summary.by} {shown(best.get('best'))}{turn} ({summary.mode})"
81
+
82
+
83
+ def summary_section(summary):
84
+ columns = summarizing.columns_of(summary, "all")
85
+ rows = list(summary.table[columns].itertuples(index=False, name=None)) if columns else []
86
+ parts = ['<h2 id="summary">Summary</h2>']
87
+ if summary.by is not None:
88
+ parts.append(paragraph(f"{plural(len(rows), 'record')} ranked by {summary.by} ({summary.mode})", "muted"))
89
+ parts.append(table(columns, rows) if rows else paragraph("no records", "note"))
90
+ parts.append(paragraph(best_line(summary), "best"))
91
+ return "\n".join(parts)
92
+
93
+
94
+ def folds_section(summary):
95
+ parts = ['<h2 id="folds">Folds</h2>']
96
+ for group in summary.folds or []:
97
+ names = summarizing.fold_series(summary, group)
98
+ count = len(group.get("members") or [])
99
+ parts.append(paragraph(f"{summarizing.fold_label(group)}: mean and std over {plural(count, 'record')}",
100
+ "muted"))
101
+ parts.append(table(["series", "mean", "std"], [(name, group["mean"][name], group["std"].get(name))
102
+ for name in names]))
103
+ return "\n".join(parts)
104
+
105
+
106
+ def history_tables(records):
107
+ return [(str(record.get("name")), reader.series_of(record["path"])) for record in records]
108
+
109
+
110
+ def curve_names(tables, by, curves):
111
+ if curves is False:
112
+ return [], 0
113
+ if isinstance(curves, str):
114
+ curves = [part.strip() for part in curves.split(",") if part.strip()]
115
+ if curves is not None:
116
+ return list(dict.fromkeys(str(name) for name in curves)), 0
117
+ present = []
118
+ for _, found in tables:
119
+ for key, values in found.items():
120
+ if key in present or not str(key).startswith(CURVE_PREFIXES):
121
+ continue
122
+ if any(finite(value) for value in values):
123
+ present.append(key)
124
+ present.sort(key=lambda key: (key != by, not key.startswith("val/"), key))
125
+ return present[:CURVES], max(len(present) - CURVES, 0)
126
+
127
+
128
+ def curve_figure(name, tables, word):
129
+ figure, axis = new_figure()
130
+ drawn, values = 0, []
131
+ for position, (label, found) in enumerate(tables):
132
+ pairs = [(turn, value) for turn, value in zip(found.get("turn") or [], found.get(name) or [])
133
+ if turn is not None and finite(value)]
134
+ if not pairs:
135
+ continue
136
+ turns, ys = zip(*pairs)
137
+ axis.plot(turns, ys, label=label, color=color_of(position), linewidth=1.6,
138
+ marker="o" if len(pairs) < 3 else None, markersize=3)
139
+ drawn += 1
140
+ values.extend(ys)
141
+ if not drawn:
142
+ figure.clear()
143
+ return None
144
+ if spans_decades(values):
145
+ axis.set_yscale("log")
146
+ if drawn <= LEGEND_LIMIT:
147
+ axis.legend(frameon=False, fontsize="small")
148
+ axis.xaxis.set_major_locator(MaxNLocator(integer=True))
149
+ axis.set_title(name, loc="left")
150
+ axis.set_xlabel(word)
151
+ axis.set_ylabel(name)
152
+ dress(axis)
153
+ return embedded(figure, f"{name} over the {word}s of {plural(drawn, 'record')}")
154
+
155
+
156
+ def curves_section(summary, records, curves):
157
+ tables = history_tables(records)
158
+ names, extra = curve_names(tables, summary.by, curves)
159
+ word = "epoch" if records and all(record.get("unit") == "epochs" for record in records) else "turn"
160
+ figures = [figure for figure in (curve_figure(name, tables, word) for name in names) if figure]
161
+ parts = ['<h2 id="curves">Curves</h2>']
162
+ if figures:
163
+ parts.append(grid(figures))
164
+ elif names and curves is not None:
165
+ parts.append(paragraph(f"no finite values for {', '.join(names)} in any history", "note"))
166
+ else:
167
+ parts.append(paragraph("no train/ or val/ series in any history yet", "note"))
168
+ if extra:
169
+ parts.append(paragraph(f"{extra} more series not drawn; the report draws at most {CURVES}", "muted"))
170
+ return "\n".join(parts)
171
+
172
+
173
+ def point_label(point):
174
+ identity = point.get("id") if isinstance(point, dict) else None
175
+ if isinstance(identity, numbers.Integral) and not isinstance(identity, bool):
176
+ return f"{int(identity):04d}"
177
+ return str(identity)
178
+
179
+
180
+ def is_log(spec, positions):
181
+ if isinstance(spec, dict) and "log" in spec:
182
+ return all(position > 0 for position in positions)
183
+ return all(position > 0 for position in positions) and max(positions) / min(positions) >= 10.0
184
+
185
+
186
+ def param_figure(name, points, watch, spec, best):
187
+ pairs = [((point.get("values") or {}).get(name), point["objective"], point is best) for point in points
188
+ if sweeping.is_valued(point) and name in (point.get("values") or {})]
189
+ if not pairs:
190
+ return None
191
+ figure, axis = new_figure(5.6, 3.6)
192
+ values = [value for value, _, _ in pairs]
193
+ if all(finite(value) for value in values):
194
+ positions = [float(value) for value in values]
195
+ if is_log(spec, positions):
196
+ axis.set_xscale("log")
197
+ dress(axis)
198
+ else:
199
+ labels = [shown(value) for value in spec] if isinstance(spec, list) else []
200
+ for value in values:
201
+ if shown(value) not in labels:
202
+ labels.append(shown(value))
203
+ positions = [labels.index(shown(value)) for value in values]
204
+ dress(axis)
205
+ axis.set_xticks(range(len(labels)), labels, rotation=30 if len(labels) > 6 else 0)
206
+ objectives = [objective for _, objective, _ in pairs]
207
+ axis.scatter(positions, objectives, s=30, color=color_of(0), alpha=0.8, linewidths=0, label="points")
208
+ chosen = [(position, objective) for position, (_, objective, top) in zip(positions, pairs) if top]
209
+ if chosen:
210
+ axis.scatter([chosen[0][0]], [chosen[0][1]], s=110, facecolors="none", edgecolors=color_of(1),
211
+ linewidths=1.8, label="best")
212
+ axis.legend(frameon=False, fontsize="small")
213
+ axis.set_title(f"{watch} against {name}", loc="left")
214
+ axis.set_xlabel(name)
215
+ axis.set_ylabel(watch)
216
+ return embedded(figure, f"{watch} against {name} over {plural(len(pairs), 'point')} with an objective")
217
+
218
+
219
+ def varied(name, points, spec):
220
+ seen = {shown((point.get("values") or {}).get(name)) for point in points if name in (point.get("values") or {})}
221
+ if isinstance(spec, dict):
222
+ return True
223
+ if isinstance(spec, list):
224
+ seen.update(shown(value) for value in spec)
225
+ return len(seen) > 1
226
+
227
+
228
+ def sweep_section(summary):
229
+ sweep = summary.sweep if isinstance(summary.sweep, dict) else {}
230
+ objective = sweep.get("objective") if isinstance(sweep.get("objective"), dict) else {}
231
+ space = sweep.get("space") if isinstance(sweep.get("space"), dict) else {}
232
+ points = sorted(summary.points, key=sweeping.id_key)
233
+ names = [str(name) for name in space]
234
+ for point in points:
235
+ for name in point.get("values") or {}:
236
+ if name not in names:
237
+ names.append(name)
238
+ watch = str(objective.get("watch") or "objective")
239
+ strategy = sweep.get("strategy")
240
+ strategy = strategy.get("use") if isinstance(strategy, dict) else strategy
241
+ states = {}
242
+ for point in points:
243
+ states[str(point.get("status"))] = states.get(str(point.get("status")), 0) + 1
244
+ planned = sweep.get("points")
245
+ counted = plural(len(points), "point")
246
+ if isinstance(planned, numbers.Integral) and not isinstance(planned, bool):
247
+ counted += f" of {planned} planned"
248
+ facts = [f"strategy {strategy or 'unknown'}",
249
+ f"objective {watch} ({sweeping.mode_of(objective)}, at {objective.get('at') or 'best'})", counted,
250
+ *(f"{count} {state}" for state, count in sorted(states.items()))]
251
+ best = sweeping.best_point(points, objective)
252
+ parts = ['<h2 id="sweep">Sweep</h2>', paragraph(", ".join(facts), "muted")]
253
+ if best is not None:
254
+ parts.append(paragraph(f"best point {point_label(best)}: {watch} {shown(best.get('objective'))}", "best"))
255
+ else:
256
+ parts.append(paragraph(f"no point has a finite {watch} yet", "best"))
257
+ rows = [[point_label(point), *((point.get("values") or {}).get(name) for name in names), point.get("status"),
258
+ point.get("turn"), point.get("objective")] for point in points]
259
+ parts.append(table(["id", *names, "status", "turn", watch], rows))
260
+ figures = [figure for figure in (param_figure(name, points, watch, space.get(name), best) for name in names
261
+ if varied(name, points, space.get(name))) if figure]
262
+ if figures:
263
+ parts.append(grid(figures))
264
+ return "\n".join(parts)
265
+
266
+
267
+ def normal_key(key):
268
+ key = str(key).strip()
269
+ return str(int(key)) if key.isdigit() else key
270
+
271
+
272
+ def wanted_of(points):
273
+ if points is None:
274
+ return None
275
+ if isinstance(points, str):
276
+ if points.strip().lower() == "all":
277
+ return "all"
278
+ points = points.split(",")
279
+ elif not isinstance(points, (list, tuple, set)):
280
+ points = [points]
281
+ keys = [normal_key(item) for item in points if str(item).strip()]
282
+ return list(dict.fromkeys(keys)) or None
283
+
284
+
285
+ def record_keys(record):
286
+ keys = {normal_key(record.get("name")), normal_key(Path(record["path"]).name)}
287
+ sweep = record.get("sweep")
288
+ if isinstance(sweep, dict) and sweep.get("id") is not None:
289
+ keys.add(normal_key(sweep["id"]))
290
+ return keys
291
+
292
+
293
+ def chosen_records(summary, records, points):
294
+ wanted = wanted_of(points)
295
+ if wanted is None:
296
+ return records[:DETAILS], max(len(records) - DETAILS, 0)
297
+ if wanted == "all":
298
+ return list(records), 0
299
+ chosen, seen, missing = [], set(), []
300
+ for key in wanted:
301
+ found = [record for record in summary.records if key in record_keys(record)]
302
+ if not found:
303
+ missing.append(key)
304
+ for record in found:
305
+ if id(record) not in seen:
306
+ seen.add(id(record))
307
+ chosen.append(record)
308
+ if missing:
309
+ known = [str(record.get("name")) for record in summary.records[:KNOWN]]
310
+ more = " and more" if len(summary.records) > KNOWN else ""
311
+ raise KalfaError(f"no record or point {', '.join(missing)} in the report; it holds {', '.join(known)}{more}")
312
+ return chosen, 0
313
+
314
+
315
+ def best_text(best):
316
+ if not isinstance(best, dict) or not best.get("series"):
317
+ return "no checkpoint watch to name a best"
318
+ if best.get("value") is None:
319
+ return f"best {best['series']}: no finite value yet"
320
+ return f"best {best['series']} {shown(best['value'])} at turn {shown(best.get('turn'))} ({best.get('mode')})"
321
+
322
+
323
+ def record_section(record, stats):
324
+ path = str(record["path"])
325
+ total = f" of {record['total']}" if record.get("total") is not None else ""
326
+ facts = (f"{record.get('status')}, turn {record.get('turn')}{total}, "
327
+ f"{shown(record.get('seconds'), 'seconds') or 0} s, {path}")
328
+ parts = [f'<section class="record"><h3>{escape(record.get("name"))}</h3>', paragraph(facts, "muted")]
329
+ if record.get("failure"):
330
+ parts.append(paragraph(f"failed: {record['failure']}", "note"))
331
+ parts.append(paragraph(best_text(record.get("best")), "best"))
332
+ params = record.get("params") or {}
333
+ last = record.get("last") or {}
334
+ blocks = []
335
+ if params:
336
+ blocks.append("<div><h4>params</h4>" + table(["param", "value"], list(params.items())) + "</div>")
337
+ if last:
338
+ rows = sorted(last.items(), key=lambda item: summarizing.series_key(item[0]))
339
+ blocks.append("<div><h4>last values</h4>" + table(["series", "last"], rows) + "</div>")
340
+ if blocks:
341
+ parts.append('<div class="pair">' + "".join(blocks) + "</div>")
342
+ if stats:
343
+ parts.append(predictions_part(path))
344
+ parts.append("</section>")
345
+ return "\n".join(part for part in parts if part)
346
+
347
+
348
+ def details_section(chosen, rest, stats):
349
+ parts = ['<h2 id="records">Records</h2>']
350
+ if rest:
351
+ parts.append(paragraph(f"the first {len(chosen)} of {len(chosen) + rest} records in the ranking; "
352
+ f"--points IDS names the others", "muted"))
353
+ if not chosen:
354
+ parts.append(paragraph("no records", "note"))
355
+ parts.extend(record_section(record, stats) for record in chosen)
356
+ return "\n".join(parts)
kalfa/commands/stop.py ADDED
@@ -0,0 +1,60 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from kalfa.core.errors import KalfaError
5
+ from kalfa.core.record import STOP_FILE, RecordWriter, now
6
+ from kalfa.records import reader
7
+ from kalfa.records import sweep as sweeping
8
+
9
+
10
+ STOPPABLE = ("running", "stale")
11
+ LOGGER = logging.getLogger("kalfa.commands")
12
+
13
+
14
+ def kind_of(record):
15
+ directory = Path(record)
16
+ if reader.is_record(directory):
17
+ return "record"
18
+ if sweeping.is_sweep(directory):
19
+ return "sweep"
20
+ raise KalfaError(f"{record} is neither a record directory nor a sweep root")
21
+
22
+
23
+ def write_stop(directory, by):
24
+ writer = RecordWriter(directory)
25
+ writer.write_json(STOP_FILE, {"by": by, "at": now()})
26
+ return str(writer.path(STOP_FILE))
27
+
28
+
29
+ def state_in(directory):
30
+ try:
31
+ return reader.state_of(reader.read_manifest(directory))
32
+ except (OSError, ValueError):
33
+ return "unreadable"
34
+
35
+
36
+ def stoppable(root):
37
+ return [point for point in sweeping.point_dirs(root) if state_in(point) in STOPPABLE]
38
+
39
+
40
+ def stop_record(record, by="cli"):
41
+ directory = Path(record)
42
+ if kind_of(directory) == "record":
43
+ state = state_in(directory)
44
+ if state not in STOPPABLE:
45
+ LOGGER.warning("%s is %s; nothing to stop", record, state)
46
+ return []
47
+ return [write_stop(directory, by)]
48
+ points = stoppable(directory)
49
+ if not points:
50
+ LOGGER.warning("nothing to stop under %s: no running or stale points", record)
51
+ return [write_stop(point, by) for point in points]
52
+
53
+
54
+ def stop_many(records, by="cli"):
55
+ for record in records:
56
+ kind_of(record)
57
+ written = []
58
+ for record in records:
59
+ written.extend(stop_record(record, by=by))
60
+ return written
@@ -0,0 +1,202 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from kalfa.commands.sweep.checks import (
5
+ MODES,
6
+ MOMENTS,
7
+ OBJECTIVE_KEYS,
8
+ SWEEP_KEYS,
9
+ check_objective,
10
+ check_space,
11
+ check_strategy,
12
+ check_sweep,
13
+ series_problem,
14
+ )
15
+ from kalfa.commands.sweep.plan import (
16
+ MAPPING_NOTE,
17
+ Plan,
18
+ command_args,
19
+ condor_arg,
20
+ executable,
21
+ objective_note,
22
+ plan_of,
23
+ plan_seed,
24
+ points_text,
25
+ previous_note,
26
+ script_text,
27
+ submit_text,
28
+ value_text,
29
+ write_plan,
30
+ write_text,
31
+ )
32
+ from kalfa.commands.sweep.points import (
33
+ LOGS,
34
+ OUTSIDE_STOPS,
35
+ POINTS_FILE,
36
+ REPAIRED,
37
+ STOP_FILE,
38
+ SWEEP_FILE,
39
+ abandoned,
40
+ append_point,
41
+ clear_point,
42
+ complete,
43
+ failed_points,
44
+ manifest_of,
45
+ next_index,
46
+ planned_values,
47
+ point_dir,
48
+ recorded_points,
49
+ recorded_values,
50
+ repairable,
51
+ stopped_by_rule,
52
+ )
53
+ from kalfa.commands.sweep.runner import (
54
+ LOGGER,
55
+ child,
56
+ in_children,
57
+ interrupted,
58
+ resumed,
59
+ run_point,
60
+ say_of,
61
+ silent,
62
+ values_text,
63
+ )
64
+ from kalfa.commands.sweep.space import (
65
+ RANGES,
66
+ SPACE_HINT,
67
+ bounds_of,
68
+ dimension,
69
+ draw,
70
+ is_int,
71
+ space_values,
72
+ spread,
73
+ )
74
+ from kalfa.commands.sweep.strategies import (
75
+ OPTUNA_MISSING,
76
+ SAMPLERS,
77
+ STRATEGIES,
78
+ STRATEGY_HINT,
79
+ Grid,
80
+ Optuna,
81
+ Random,
82
+ distribution_of,
83
+ finished_trial,
84
+ is_plain,
85
+ optuna_module,
86
+ resumed_seed,
87
+ seed_value,
88
+ strategy_call,
89
+ strategy_names,
90
+ strategy_note,
91
+ strategy_of,
92
+ strategy_target,
93
+ suggest,
94
+ )
95
+ from kalfa.core import aliases
96
+ from kalfa.core.errors import KalfaError
97
+ from kalfa.records import reader
98
+ from kalfa.records.sweep import objective_of
99
+ from kalfa.std.common import positive_int as positive
100
+
101
+
102
+ __all__ = ["LOGGER", "LOGS", "MAPPING_NOTE", "MODES", "MOMENTS", "OBJECTIVE_KEYS", "OPTUNA_MISSING", "OUTSIDE_STOPS",
103
+ "POINTS_FILE", "RANGES", "REPAIRED", "SAMPLERS", "SPACE_HINT", "STOP_FILE", "STRATEGIES", "STRATEGY_HINT",
104
+ "SWEEP_FILE", "SWEEP_KEYS", "Grid", "Optuna", "Plan", "Random", "abandoned", "append_point", "asking",
105
+ "attempt", "bounds_of", "check_objective", "check_space", "check_strategy", "check_sweep", "child",
106
+ "clear_point", "command_args", "complete", "condor_arg", "dimension", "distribution_of", "draw",
107
+ "executable", "failed_points", "finished_trial", "in_children", "in_turn", "interrupted", "is_int",
108
+ "is_plain", "manifest_of", "next_index", "objective_note", "optuna_module", "plan_of", "plan_seed",
109
+ "planned_values", "point_dir", "points_text", "positive", "previous_note", "recorded_points",
110
+ "recorded_values", "repairable", "resumed", "resumed_seed", "run_local", "run_point", "say_of",
111
+ "script_text", "seed_value", "series_problem", "silent", "space_values", "spread", "stopped_by_rule",
112
+ "strategy_call", "strategy_names", "strategy_note", "strategy_of", "strategy_target", "submit_text",
113
+ "suggest", "value_text", "values_text", "write_plan", "write_text"]
114
+
115
+
116
+ def attempt(plan, index, values, monitor):
117
+ try:
118
+ values = values if values is not None else planned_values(plan, index)
119
+ say_of(monitor)(f"point {index:04d} {values_text(values)}")
120
+ return run_point(plan, index, values, monitor)
121
+ except Exception as exc:
122
+ LOGGER.warning("point %04d failed: %s: %s", index, type(exc).__name__, exc,
123
+ exc_info=LOGGER.isEnabledFor(logging.DEBUG))
124
+ return None
125
+
126
+
127
+ def in_turn(plan, indices, monitor, log):
128
+ results = []
129
+ for index in indices:
130
+ result = attempt(plan, index, None, monitor)
131
+ results.append(result)
132
+ if interrupted(result):
133
+ log(f"point {index:04d} was stopped ({result.context.stopping}); the sweep stops here and --repair runs "
134
+ f"the rest")
135
+ break
136
+ return results
137
+
138
+
139
+ def asking(plan, monitor, log):
140
+ trials = int(plan.strategy.trials)
141
+ start = next_index(plan)
142
+ if start >= trials:
143
+ log(f"{plan.root}: the {trials} trials have run")
144
+ return []
145
+ if start:
146
+ told = resumed(plan)
147
+ if told is None:
148
+ log(f"{plan.root}: {start} points exist; trials go on from point {start:04d} and the strategy, which has "
149
+ f"no resume(space, objective, points), does not know the points already run")
150
+ else:
151
+ log(f"{plan.root}: {start} points exist and the strategy was told {told} of them; trials go on from point "
152
+ f"{start:04d}")
153
+ results = []
154
+ for index in range(start, trials):
155
+ trial, values = plan.strategy.ask(plan.space, plan.objective)
156
+ result = attempt(plan, index, values, monitor)
157
+ value = None if result is None else objective_of(reader.summary_of(result.record), plan.objective)
158
+ plan.strategy.tell(trial, value)
159
+ append_point(plan.root, index, values)
160
+ results.append(result)
161
+ if interrupted(result):
162
+ log(f"point {index:04d} was stopped ({result.context.stopping}); the sweep stops here")
163
+ break
164
+ return results
165
+
166
+
167
+ def run_local(plan, monitor=None, jobs=1, repair=False, log=print):
168
+ jobs = max(1, int(jobs or 1))
169
+ if jobs > 1 and not plan.paths:
170
+ raise KalfaError("jobs above 1 run the points in child processes, which read config files; a mapping config "
171
+ "runs with jobs 1")
172
+ if not (Path(plan.root) / SWEEP_FILE).exists():
173
+ write_plan(plan, log)
174
+ if repair:
175
+ indices = failed_points(plan)
176
+ total = plan.count if plan.count is not None else len(recorded_values(plan))
177
+ log(f"{plan.root}: {len(indices)} of {total} points to repair"
178
+ + (": " + ", ".join(f"{index:04d}" for index in indices) if indices else ""))
179
+ elif plan.adaptive:
180
+ return asking(plan, monitor, log)
181
+ else:
182
+ indices = []
183
+ for index in range(len(plan.points)):
184
+ directory = Path(point_dir(plan.root, index))
185
+ if complete(directory):
186
+ log(f"point {index:04d} is {manifest_of(directory).get('status')}, skipped")
187
+ else:
188
+ indices.append(index)
189
+ if jobs > 1:
190
+ return in_children(plan, indices, jobs, log)
191
+ return in_turn(plan, indices, monitor, log)
192
+
193
+
194
+ Grid.__module__ = Optuna.__module__ = Random.__module__ = __name__
195
+
196
+ aliases.register("grid", "kalfa.commands.sweep.Grid", slot="strategy",
197
+ doc="every combination of the space; a range entry needs steps")
198
+ aliases.register("optuna", "kalfa.commands.sweep.Optuna", slot="strategy",
199
+ doc="trials points asked from an optuna study (tpe or random sampler), one after another")
200
+ aliases.register("random_search", "kalfa.commands.sweep.Random", slot="strategy",
201
+ doc="trials points drawn at random over the space with numpy's default_rng(seed), repeats drawn "
202
+ "again; a sweep section also takes the short name random")