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.
- kalfa/__init__.py +19 -0
- kalfa/api.py +284 -0
- kalfa/board/__init__.py +1 -0
- kalfa/board/answers.py +419 -0
- kalfa/board/server.py +536 -0
- kalfa/board/static/api.js +176 -0
- kalfa/board/static/app.js +122 -0
- kalfa/board/static/board.css +228 -0
- kalfa/board/static/charts.css +46 -0
- kalfa/board/static/components/badge.js +14 -0
- kalfa/board/static/components/chart.js +204 -0
- kalfa/board/static/components/diagram.js +857 -0
- kalfa/board/static/components/error.js +17 -0
- kalfa/board/static/components/loading.js +7 -0
- kalfa/board/static/components/progress.js +25 -0
- kalfa/board/static/components/settings.js +418 -0
- kalfa/board/static/components/spark.js +43 -0
- kalfa/board/static/components/table.js +156 -0
- kalfa/board/static/diagram.css +76 -0
- kalfa/board/static/format.js +138 -0
- kalfa/board/static/index.html +34 -0
- kalfa/board/static/plot.js +594 -0
- kalfa/board/static/predictions.css +20 -0
- kalfa/board/static/store.js +215 -0
- kalfa/board/static/views/compare.js +185 -0
- kalfa/board/static/views/home.js +90 -0
- kalfa/board/static/views/record/config.js +81 -0
- kalfa/board/static/views/record/curves.js +137 -0
- kalfa/board/static/views/record/data.js +378 -0
- kalfa/board/static/views/record/files.js +128 -0
- kalfa/board/static/views/record/log.js +164 -0
- kalfa/board/static/views/record/model.js +286 -0
- kalfa/board/static/views/record/overview.js +216 -0
- kalfa/board/static/views/record/plots.js +103 -0
- kalfa/board/static/views/record/predictions.js +834 -0
- kalfa/board/static/views/record.js +179 -0
- kalfa/board/static/views/runs.js +124 -0
- kalfa/board/static/views/sweep.js +282 -0
- kalfa/board/watch.py +87 -0
- kalfa/cli.py +371 -0
- kalfa/commands/__init__.py +0 -0
- kalfa/commands/common.py +8 -0
- kalfa/commands/export.py +117 -0
- kalfa/commands/generate.py +31 -0
- kalfa/commands/guide/__init__.py +101 -0
- kalfa/commands/guide/about.py +93 -0
- kalfa/commands/guide/calls.py +102 -0
- kalfa/commands/guide/sections.py +186 -0
- kalfa/commands/guide/seen.py +307 -0
- kalfa/commands/guide/show.py +106 -0
- kalfa/commands/guide/training.py +333 -0
- kalfa/commands/guide/words.py +97 -0
- kalfa/commands/legos.py +508 -0
- kalfa/commands/plots.py +27 -0
- kalfa/commands/report/__init__.py +64 -0
- kalfa/commands/report/page.py +112 -0
- kalfa/commands/report/predictions.py +216 -0
- kalfa/commands/report/sections.py +356 -0
- kalfa/commands/stop.py +60 -0
- kalfa/commands/sweep/__init__.py +202 -0
- kalfa/commands/sweep/checks.py +98 -0
- kalfa/commands/sweep/plan.py +183 -0
- kalfa/commands/sweep/points.py +123 -0
- kalfa/commands/sweep/runner.py +100 -0
- kalfa/commands/sweep/space.py +67 -0
- kalfa/commands/sweep/strategies.py +228 -0
- kalfa/core/__init__.py +0 -0
- kalfa/core/aliases.py +229 -0
- kalfa/core/build.py +727 -0
- kalfa/core/check.py +550 -0
- kalfa/core/coerce.py +36 -0
- kalfa/core/context.py +149 -0
- kalfa/core/device.py +63 -0
- kalfa/core/errors.py +45 -0
- kalfa/core/graph.py +304 -0
- kalfa/core/history.py +70 -0
- kalfa/core/loop.py +710 -0
- kalfa/core/make.py +121 -0
- kalfa/core/plan.py +156 -0
- kalfa/core/predict.py +290 -0
- kalfa/core/protocols.py +122 -0
- kalfa/core/record.py +347 -0
- kalfa/core/refs.py +88 -0
- kalfa/core/rng.py +100 -0
- kalfa/core/rules.py +513 -0
- kalfa/core/run.py +568 -0
- kalfa/core/spec.py +413 -0
- kalfa/core/state.py +100 -0
- kalfa/core/wrap.py +671 -0
- kalfa/records/__init__.py +24 -0
- kalfa/records/compare.py +86 -0
- kalfa/records/reader.py +561 -0
- kalfa/records/stats/__init__.py +36 -0
- kalfa/records/stats/choices.py +282 -0
- kalfa/records/stats/classes.py +236 -0
- kalfa/records/stats/measures.py +144 -0
- kalfa/records/stats/pairs.py +225 -0
- kalfa/records/stats/tables.py +153 -0
- kalfa/records/sweep.py +244 -0
- kalfa/std/__init__.py +17 -0
- kalfa/std/actions.py +35 -0
- kalfa/std/common.py +56 -0
- kalfa/std/data/__init__.py +1 -0
- kalfa/std/data/base.py +45 -0
- kalfa/std/data/columns.py +190 -0
- kalfa/std/data/flow.py +317 -0
- kalfa/std/data/images.py +443 -0
- kalfa/std/data/options.py +147 -0
- kalfa/std/data/pre.py +84 -0
- kalfa/std/data/splits.py +140 -0
- kalfa/std/data/stream.py +294 -0
- kalfa/std/data/table.py +642 -0
- kalfa/std/data/text.py +436 -0
- kalfa/std/data/transforms.py +327 -0
- kalfa/std/data/weights.py +113 -0
- kalfa/std/data/window.py +135 -0
- kalfa/std/figure.py +37 -0
- kalfa/std/init.py +72 -0
- kalfa/std/loggers.py +105 -0
- kalfa/std/losses.py +161 -0
- kalfa/std/metrics.py +463 -0
- kalfa/std/models.py +122 -0
- kalfa/std/notes.py +1553 -0
- kalfa/std/objectives.py +360 -0
- kalfa/std/plots/__init__.py +75 -0
- kalfa/std/plots/classification.py +207 -0
- kalfa/std/plots/common.py +44 -0
- kalfa/std/plots/curves.py +56 -0
- kalfa/std/plots/images.py +188 -0
- kalfa/std/plots/pairs.py +66 -0
- kalfa/std/plots/predictions.py +154 -0
- kalfa/std/samplers.py +261 -0
- kalfa/std/samples.py +72 -0
- kalfa/std/schedules.py +74 -0
- kalfa/std/steps.py +133 -0
- kalfa/std/triggers.py +135 -0
- kalfa/summary.py +296 -0
- kalfa/tui/__init__.py +4 -0
- kalfa/tui/app.py +107 -0
- kalfa/tui/config.py +61 -0
- kalfa/tui/guide.py +100 -0
- kalfa/tui/jobs.py +97 -0
- kalfa/tui/legos.py +62 -0
- kalfa/tui/records.py +439 -0
- kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
- kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
- kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
- 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")
|