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,215 @@
|
|
|
1
|
+
import { reactive } from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
|
|
2
|
+
|
|
3
|
+
import { encodePath, getJson, live } from "./api.js";
|
|
4
|
+
import { isNumber } from "./format.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const TABS = ["overview", "curves", "model", "data", "predictions", "plots", "config", "files", "log"];
|
|
8
|
+
export const ACTIVE = ["running", "stale"];
|
|
9
|
+
export const EVERY = [1, 2, 5, 10, 30];
|
|
10
|
+
export const THEMES = ["auto", "light", "dark"];
|
|
11
|
+
|
|
12
|
+
function readSetting(key, fallback) {
|
|
13
|
+
try {
|
|
14
|
+
const value = window.localStorage.getItem("kalfa." + key);
|
|
15
|
+
return value === null ? fallback : value;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeSetting(key, value) {
|
|
22
|
+
try {
|
|
23
|
+
window.localStorage.setItem("kalfa." + key, String(value));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function startEvery() {
|
|
30
|
+
const value = Number(readSetting("every", "2"));
|
|
31
|
+
return EVERY.includes(value) ? value : 2;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function startTheme() {
|
|
35
|
+
const value = readSetting("theme", "auto");
|
|
36
|
+
return THEMES.includes(value) ? value : "auto";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const store = reactive({
|
|
40
|
+
root: "",
|
|
41
|
+
records: [],
|
|
42
|
+
loaded: false,
|
|
43
|
+
error: "",
|
|
44
|
+
selected: [],
|
|
45
|
+
version: 0,
|
|
46
|
+
changed: [],
|
|
47
|
+
connected: false,
|
|
48
|
+
every: startEvery(),
|
|
49
|
+
theme: startTheme(),
|
|
50
|
+
paint: 0,
|
|
51
|
+
fault: "",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const listeners = new Set();
|
|
55
|
+
let stopLive = null;
|
|
56
|
+
let loading = 0;
|
|
57
|
+
|
|
58
|
+
export function onLive(callback) {
|
|
59
|
+
listeners.add(callback);
|
|
60
|
+
return () => listeners.delete(callback);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function notify(changed) {
|
|
64
|
+
for (const callback of [...listeners]) {
|
|
65
|
+
try {
|
|
66
|
+
callback(changed);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function refresh() {
|
|
74
|
+
const ticket = ++loading;
|
|
75
|
+
try {
|
|
76
|
+
const answer = await getJson("/api/records");
|
|
77
|
+
if (ticket !== loading) return;
|
|
78
|
+
store.root = answer && answer.root ? answer.root : "";
|
|
79
|
+
store.records = answer && Array.isArray(answer.records) ? answer.records : [];
|
|
80
|
+
store.error = "";
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (ticket === loading) store.error = error.message;
|
|
83
|
+
} finally {
|
|
84
|
+
store.loaded = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function changedHandler(changed) {
|
|
89
|
+
store.version += 1;
|
|
90
|
+
store.changed = Array.isArray(changed) ? changed : [];
|
|
91
|
+
refresh();
|
|
92
|
+
notify(changed);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function startLive() {
|
|
96
|
+
if (stopLive) stopLive();
|
|
97
|
+
stopLive = live(changedHandler, (connected) => {
|
|
98
|
+
store.connected = connected;
|
|
99
|
+
}, store.every);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function setEvery(value) {
|
|
103
|
+
const every = Number(value);
|
|
104
|
+
store.every = EVERY.includes(every) ? every : 2;
|
|
105
|
+
writeSetting("every", store.every);
|
|
106
|
+
startLive();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function applyTheme() {
|
|
110
|
+
const root = document.documentElement;
|
|
111
|
+
if (store.theme === "auto") root.removeAttribute("data-theme");
|
|
112
|
+
else root.setAttribute("data-theme", store.theme);
|
|
113
|
+
store.paint += 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function cycleTheme() {
|
|
117
|
+
const next = THEMES[(THEMES.indexOf(store.theme) + 1) % THEMES.length];
|
|
118
|
+
store.theme = next;
|
|
119
|
+
writeSetting("theme", next);
|
|
120
|
+
applyTheme();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function watchScheme() {
|
|
124
|
+
if (!window.matchMedia) return;
|
|
125
|
+
const scheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
126
|
+
const repaint = () => {
|
|
127
|
+
if (store.theme === "auto") store.paint += 1;
|
|
128
|
+
};
|
|
129
|
+
if (scheme.addEventListener) scheme.addEventListener("change", repaint);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function touches(changed, paths) {
|
|
133
|
+
if (!Array.isArray(changed)) return true;
|
|
134
|
+
if (!changed.length) return false;
|
|
135
|
+
if (paths.some((path) => path === "" || path === ".")) return true;
|
|
136
|
+
return changed.some((item) => paths.some((path) => item === path || item.startsWith(path + "/")
|
|
137
|
+
|| path.startsWith(item + "/")));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function setSelected(paths) {
|
|
141
|
+
store.selected = [...new Set((paths || []).filter(Boolean))];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function toggleSelected(path) {
|
|
145
|
+
if (store.selected.includes(path)) setSelected(store.selected.filter((item) => item !== path));
|
|
146
|
+
else setSelected([...store.selected, path]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function recordLink(path, tab = "overview") {
|
|
150
|
+
return "#/record/" + encodePath(path) + "/" + (tab || "overview");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function sweepLink(path) {
|
|
154
|
+
return "#/sweep/" + encodePath(path);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function compareLink(paths, keys = "") {
|
|
158
|
+
const search = new URLSearchParams();
|
|
159
|
+
search.set("records", (paths || []).join(","));
|
|
160
|
+
if (keys) search.set("keys", keys);
|
|
161
|
+
return "#/compare?" + search.toString();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function isSweepEntry(entry) {
|
|
165
|
+
return Boolean(entry) && entry.kind === "sweep";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function statusOf(entry) {
|
|
169
|
+
if (!entry) return "unknown";
|
|
170
|
+
if (isSweepEntry(entry)) {
|
|
171
|
+
const sweep = entry.sweep || {};
|
|
172
|
+
if (sweep.running > 0) return "running";
|
|
173
|
+
if (sweep.stale > 0) return "stale";
|
|
174
|
+
const planned = sweep.points || sweep.count || 0;
|
|
175
|
+
if (sweep.count > 0 && sweep.done >= sweep.count && sweep.count >= planned) return "done";
|
|
176
|
+
return sweep.count > 0 ? "partial" : "empty";
|
|
177
|
+
}
|
|
178
|
+
return (entry.summary && entry.summary.status) || "unknown";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function isActive(entry) {
|
|
182
|
+
return ACTIVE.includes(statusOf(entry));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function nameOf(entry) {
|
|
186
|
+
if (!entry) return "";
|
|
187
|
+
if (entry.summary && entry.summary.name) return entry.summary.name;
|
|
188
|
+
return entry.path;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function progressOf(entry) {
|
|
192
|
+
if (isSweepEntry(entry)) {
|
|
193
|
+
const sweep = entry.sweep || {};
|
|
194
|
+
return { value: sweep.done || 0, total: sweep.points || sweep.count || null, unit: "points" };
|
|
195
|
+
}
|
|
196
|
+
const summary = (entry && entry.summary) || {};
|
|
197
|
+
return {
|
|
198
|
+
value: summary.turn || 0,
|
|
199
|
+
total: summary.total || null,
|
|
200
|
+
unit: summary.unit || "turns",
|
|
201
|
+
part: isNumber(summary.fraction) ? summary.fraction : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function linkOf(entry) {
|
|
206
|
+
return isSweepEntry(entry) ? sweepLink(entry.path) : recordLink(entry.path, "overview");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function entryOf(path) {
|
|
210
|
+
return store.records.find((entry) => entry.path === path) || null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function latestSweep() {
|
|
214
|
+
return store.records.find(isSweepEntry) || null;
|
|
215
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { computed, onBeforeUnmount, onMounted, ref }
|
|
2
|
+
from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
|
|
3
|
+
|
|
4
|
+
import { getJson, query } from "../api.js";
|
|
5
|
+
import { Badge } from "../components/badge.js";
|
|
6
|
+
import { Chart } from "../components/chart.js";
|
|
7
|
+
import { ErrorBox } from "../components/error.js";
|
|
8
|
+
import { Progress } from "../components/progress.js";
|
|
9
|
+
import { bestText, seconds, text } from "../format.js";
|
|
10
|
+
import { compareLink, onLive, recordLink, touches } from "../store.js";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export function listOf(value) {
|
|
14
|
+
return String(value || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function tablesOf(series) {
|
|
18
|
+
return Object.keys(series || {}).filter((key) => key !== "turn").map((key) => ({ key, table: series[key] || {} }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasValue(values) {
|
|
22
|
+
return Array.isArray(values) && values.some((value) => value !== null && value !== undefined);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function seriesKeys(tables) {
|
|
26
|
+
const keys = [];
|
|
27
|
+
for (const entry of tables) {
|
|
28
|
+
for (const key of Object.keys(entry.table)) {
|
|
29
|
+
if (!keys.includes(key)) keys.push(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return keys;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function diffLines(textValue) {
|
|
36
|
+
return String(textValue || "").split("\n").map((line) => {
|
|
37
|
+
let tone = "";
|
|
38
|
+
if (line.startsWith("+++") || line.startsWith("---")) tone = "head";
|
|
39
|
+
else if (line.startsWith("@@")) tone = "hunk";
|
|
40
|
+
else if (line.startsWith("+")) tone = "add";
|
|
41
|
+
else if (line.startsWith("-")) tone = "del";
|
|
42
|
+
return { line, tone };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Compare = {
|
|
47
|
+
name: "CompareView",
|
|
48
|
+
components: { KBadge: Badge, KChart: Chart, KError: ErrorBox, KProgress: Progress },
|
|
49
|
+
props: { query: { type: Object, default: () => ({}) } },
|
|
50
|
+
setup(props) {
|
|
51
|
+
const paths = listOf(props.query.records);
|
|
52
|
+
const keys = ref(props.query.keys || "");
|
|
53
|
+
const answer = ref(null);
|
|
54
|
+
const error = ref("");
|
|
55
|
+
const loading = ref(false);
|
|
56
|
+
let ticket = 0;
|
|
57
|
+
let off = null;
|
|
58
|
+
|
|
59
|
+
const load = async () => {
|
|
60
|
+
if (!paths.length) return;
|
|
61
|
+
const mine = ++ticket;
|
|
62
|
+
loading.value = true;
|
|
63
|
+
try {
|
|
64
|
+
const found = await getJson("/api/compare" + query({ records: paths, keys: listOf(keys.value) }));
|
|
65
|
+
if (mine !== ticket) return;
|
|
66
|
+
answer.value = found;
|
|
67
|
+
error.value = "";
|
|
68
|
+
} catch (failure) {
|
|
69
|
+
if (mine === ticket) error.value = failure.message;
|
|
70
|
+
} finally {
|
|
71
|
+
if (mine === ticket) loading.value = false;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const labels = computed(() => {
|
|
76
|
+
const found = answer.value || {};
|
|
77
|
+
const names = Array.isArray(found.names) ? found.names : [];
|
|
78
|
+
const records = Array.isArray(found.records) ? found.records : [];
|
|
79
|
+
return tablesOf(found.series).map((entry, index) => names[index] || (records[index] || {}).name || paths[index]
|
|
80
|
+
|| entry.key);
|
|
81
|
+
});
|
|
82
|
+
const tables = computed(() => tablesOf((answer.value || {}).series));
|
|
83
|
+
const turns = computed(() => ((answer.value || {}).series || {}).turn || []);
|
|
84
|
+
const common = computed(() => seriesKeys(tables.value)
|
|
85
|
+
.filter((key) => tables.value.every((entry) => hasValue(entry.table[key]))));
|
|
86
|
+
const partial = computed(() => seriesKeys(tables.value).filter((key) => !common.value.includes(key)
|
|
87
|
+
&& tables.value.some((entry) => hasValue(entry.table[key]))));
|
|
88
|
+
const charts = computed(() => common.value.map((key) => ({
|
|
89
|
+
key,
|
|
90
|
+
lines: tables.value.map((entry, index) => ({ name: labels.value[index], x: turns.value, y: entry.table[key] })),
|
|
91
|
+
})));
|
|
92
|
+
const records = computed(() => ((answer.value || {}).records || []).map((summary, index) => ({
|
|
93
|
+
summary, path: paths[index] || summary.path, label: labels.value[index] || summary.name,
|
|
94
|
+
})));
|
|
95
|
+
const differing = computed(() => Object.entries(((answer.value || {}).params || {}).differing || {}));
|
|
96
|
+
const shared = computed(() => Object.entries(((answer.value || {}).params || {}).shared || {}));
|
|
97
|
+
const diffs = computed(() => Object.entries((answer.value || {}).diffs || {})
|
|
98
|
+
.map(([title, body]) => ({ title, body, lines: body === null || body === undefined ? [] : diffLines(body) })));
|
|
99
|
+
const apply = () => {
|
|
100
|
+
window.location.hash = compareLink(paths, listOf(keys.value).join(","));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
onMounted(() => {
|
|
104
|
+
load();
|
|
105
|
+
off = onLive((changed) => {
|
|
106
|
+
if (touches(changed, paths)) load();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
onBeforeUnmount(() => {
|
|
110
|
+
ticket += 1;
|
|
111
|
+
if (off) off();
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
paths, keys, answer, error, loading, charts, partial, records, differing, shared, diffs, apply, text, seconds,
|
|
115
|
+
bestText, recordLink,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
template: `
|
|
119
|
+
<section class="page compare">
|
|
120
|
+
<div class="page-head">
|
|
121
|
+
<h2 class="section-title">compare</h2>
|
|
122
|
+
<span class="muted small">{{ paths.length }} records</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div v-if="!paths.length" class="empty">pick two or more records on the <a class="link" href="#/runs">runs page</a>
|
|
125
|
+
and press compare</div>
|
|
126
|
+
<template v-else>
|
|
127
|
+
<form class="toolbar" @submit.prevent="apply">
|
|
128
|
+
<label class="small muted" for="compare-keys">series</label>
|
|
129
|
+
<input id="compare-keys" class="filter" v-model="keys" placeholder="val/rmse, train/mse (blank: every series of the first record)">
|
|
130
|
+
<button type="submit" class="button">apply</button>
|
|
131
|
+
</form>
|
|
132
|
+
<k-error :error="error"></k-error>
|
|
133
|
+
<div v-if="loading && !answer" class="loading">reading the records</div>
|
|
134
|
+
<template v-if="answer">
|
|
135
|
+
<div class="table-wrap">
|
|
136
|
+
<table class="data">
|
|
137
|
+
<thead><tr><th>record</th><th>status</th><th>progress</th><th>best</th><th>seconds</th></tr></thead>
|
|
138
|
+
<tbody>
|
|
139
|
+
<tr v-for="item in records" :key="item.path">
|
|
140
|
+
<td><a class="link" :href="recordLink(item.path, 'overview')">{{ item.label }}</a></td>
|
|
141
|
+
<td><k-badge :status="item.summary.status"></k-badge></td>
|
|
142
|
+
<td><k-progress :value="item.summary.turn" :total="item.summary.total"></k-progress></td>
|
|
143
|
+
<td class="num">{{ bestText(item.summary.best) }}</td>
|
|
144
|
+
<td class="num">{{ seconds(item.summary.seconds) }}</td>
|
|
145
|
+
</tr>
|
|
146
|
+
</tbody>
|
|
147
|
+
</table>
|
|
148
|
+
</div>
|
|
149
|
+
<h3 class="section-title">series</h3>
|
|
150
|
+
<div v-if="!charts.length" class="empty">the records share no series with values</div>
|
|
151
|
+
<div class="grid">
|
|
152
|
+
<k-chart v-for="chart in charts" :key="chart.key" :title="chart.key" :data="chart.lines"
|
|
153
|
+
:options="{ xlabel: 'turn' }"></k-chart>
|
|
154
|
+
</div>
|
|
155
|
+
<div v-if="partial.length" class="muted small">not in every record: {{ partial.join(", ") }}</div>
|
|
156
|
+
<h3 class="section-title">params that differ</h3>
|
|
157
|
+
<div v-if="!differing.length" class="empty">every param is the same</div>
|
|
158
|
+
<div v-else class="table-wrap">
|
|
159
|
+
<table class="data">
|
|
160
|
+
<thead><tr><th>param</th><th v-for="item in records" :key="item.path">{{ item.label }}</th></tr></thead>
|
|
161
|
+
<tbody>
|
|
162
|
+
<tr v-for="pair in differing" :key="pair[0]">
|
|
163
|
+
<td class="mono">{{ pair[0] }}</td>
|
|
164
|
+
<td v-for="(value, index) in pair[1]" :key="index" class="num">{{ text(value) }}</td>
|
|
165
|
+
</tr>
|
|
166
|
+
</tbody>
|
|
167
|
+
</table>
|
|
168
|
+
</div>
|
|
169
|
+
<div v-if="shared.length" class="muted small shared">shared:
|
|
170
|
+
<span v-for="pair in shared" :key="pair[0]" class="chip">{{ pair[0] }} = {{ text(pair[1]) }}</span>
|
|
171
|
+
</div>
|
|
172
|
+
<h3 class="section-title">config diff</h3>
|
|
173
|
+
<div v-if="!diffs.length" class="empty">one record has nothing to diff against</div>
|
|
174
|
+
<div v-for="diff in diffs" :key="diff.title" class="card">
|
|
175
|
+
<div class="card-head"><span class="card-title">{{ diff.title }}</span></div>
|
|
176
|
+
<div v-if="diff.body === null || diff.body === undefined" class="empty">a resolved.yaml is missing</div>
|
|
177
|
+
<div v-else-if="!diff.body" class="empty">the resolved configs are the same</div>
|
|
178
|
+
<pre v-else class="diff"><span v-for="(item, index) in diff.lines" :key="index" :class="item.tone">{{ item.line }}</span></pre>
|
|
179
|
+
</div>
|
|
180
|
+
</template>
|
|
181
|
+
</template>
|
|
182
|
+
</section>`,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default Compare;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { computed } from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "../components/badge.js";
|
|
4
|
+
import { ErrorBox } from "../components/error.js";
|
|
5
|
+
import { Progress } from "../components/progress.js";
|
|
6
|
+
import { EMPTY, ago, bestText, liveNote, nonfiniteText, seconds, when } from "../format.js";
|
|
7
|
+
import { isActive, isSweepEntry, linkOf, nameOf, progressOf, statusOf, store } from "../store.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const RECENT = 20;
|
|
11
|
+
|
|
12
|
+
export function metaOf(entry) {
|
|
13
|
+
if (isSweepEntry(entry)) {
|
|
14
|
+
const sweep = entry.sweep || {};
|
|
15
|
+
const strategy = sweep.strategy && sweep.strategy.use ? sweep.strategy.use : "sweep";
|
|
16
|
+
const parts = [strategy + " sweep", sweep.running ? sweep.running + " running" : "none running"];
|
|
17
|
+
if (sweep.stale) parts.push(sweep.stale + " stale");
|
|
18
|
+
if (sweep.failed) parts.push(sweep.failed + " failed");
|
|
19
|
+
return parts.join(", ");
|
|
20
|
+
}
|
|
21
|
+
const summary = entry.summary || {};
|
|
22
|
+
const parts = [];
|
|
23
|
+
if (liveNote(summary)) parts.push(liveNote(summary));
|
|
24
|
+
if (summary.best && summary.best.series) parts.push("best " + bestText(summary.best));
|
|
25
|
+
if (summary.updated) parts.push("updated " + ago(summary.updated));
|
|
26
|
+
if (summary.seconds) parts.push(seconds(summary.seconds));
|
|
27
|
+
return parts.join(", ") || EMPTY;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Home = {
|
|
31
|
+
name: "HomeView",
|
|
32
|
+
components: { KBadge: Badge, KError: ErrorBox, KProgress: Progress },
|
|
33
|
+
setup() {
|
|
34
|
+
const active = computed(() => store.records.filter(isActive));
|
|
35
|
+
const recent = computed(() => store.records.filter((entry) => !isActive(entry)).slice(0, RECENT));
|
|
36
|
+
const kindOf = (entry) => (isSweepEntry(entry) ? "sweep" : "record");
|
|
37
|
+
const startedOf = (entry) => when(isSweepEntry(entry) ? (entry.sweep || {}).started : (entry.summary || {}).started);
|
|
38
|
+
const failureOf = (entry) => (entry.summary && entry.summary.failure) || "";
|
|
39
|
+
const warningOf = (entry) => nonfiniteText(entry.summary && entry.summary.nonfinite);
|
|
40
|
+
return {
|
|
41
|
+
store, active, recent, kindOf, startedOf, failureOf, warningOf, linkOf, nameOf, progressOf, statusOf, metaOf,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
template: `
|
|
45
|
+
<section class="page home">
|
|
46
|
+
<k-error :error="store.error"></k-error>
|
|
47
|
+
<div v-if="!store.loaded" class="loading">reading the records</div>
|
|
48
|
+
<template v-else>
|
|
49
|
+
<h2 class="section-title">running</h2>
|
|
50
|
+
<div v-if="!active.length" class="empty">nothing is running under {{ store.root || "the root" }}</div>
|
|
51
|
+
<div class="live-grid">
|
|
52
|
+
<a v-for="entry in active" :key="entry.path" class="card live-card" :href="linkOf(entry)">
|
|
53
|
+
<div class="live-head">
|
|
54
|
+
<span class="live-name">{{ nameOf(entry) }}</span>
|
|
55
|
+
<k-badge :status="statusOf(entry)"></k-badge>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="muted small mono">{{ entry.path }}</div>
|
|
58
|
+
<k-progress :value="progressOf(entry).value" :total="progressOf(entry).total" :part="progressOf(entry).part"
|
|
59
|
+
:label="progressOf(entry).value + ' / ' + (progressOf(entry).total || '?') + ' ' + progressOf(entry).unit">
|
|
60
|
+
</k-progress>
|
|
61
|
+
<div class="muted small">{{ metaOf(entry) }}</div>
|
|
62
|
+
<div v-if="warningOf(entry)" class="fail-text small">{{ warningOf(entry) }}</div>
|
|
63
|
+
</a>
|
|
64
|
+
</div>
|
|
65
|
+
<h2 class="section-title">recent</h2>
|
|
66
|
+
<div v-if="!recent.length" class="empty">no other records under {{ store.root || "the root" }}</div>
|
|
67
|
+
<div v-else class="table-wrap">
|
|
68
|
+
<table class="data">
|
|
69
|
+
<thead>
|
|
70
|
+
<tr><th>name</th><th>kind</th><th>status</th><th>progress</th><th>started</th><th>details</th></tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
<tr v-for="entry in recent" :key="entry.path">
|
|
74
|
+
<td><a class="link" :href="linkOf(entry)">{{ nameOf(entry) }}</a>
|
|
75
|
+
<div class="muted small mono" v-if="entry.path !== nameOf(entry)">{{ entry.path }}</div></td>
|
|
76
|
+
<td class="muted">{{ kindOf(entry) }}</td>
|
|
77
|
+
<td><k-badge :status="statusOf(entry)"></k-badge></td>
|
|
78
|
+
<td><k-progress :value="progressOf(entry).value" :total="progressOf(entry).total"></k-progress></td>
|
|
79
|
+
<td class="muted">{{ startedOf(entry) }}</td>
|
|
80
|
+
<td class="small">{{ metaOf(entry) }} <span v-if="failureOf(entry)" class="fail-text">{{ failureOf(entry) }}</span>
|
|
81
|
+
<span v-if="warningOf(entry)" class="fail-text">{{ warningOf(entry) }}</span></td>
|
|
82
|
+
</tr>
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
</section>`,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default Home;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const CODE = "overflow: auto; max-height: 75vh; padding: 10px; background: var(--surface-2); border-radius: 6px; white-space: pre";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "ConfigTab",
|
|
5
|
+
props: {
|
|
6
|
+
record: { type: Object, default: null },
|
|
7
|
+
path: { type: String, default: "" },
|
|
8
|
+
api: { default: null },
|
|
9
|
+
version: { type: Number, default: 0 },
|
|
10
|
+
},
|
|
11
|
+
data() {
|
|
12
|
+
return { notice: "", timer: null };
|
|
13
|
+
},
|
|
14
|
+
computed: {
|
|
15
|
+
code() { return CODE; },
|
|
16
|
+
text() { return this.record && typeof this.record.config === "string" ? this.record.config : ""; },
|
|
17
|
+
lines() { return this.text ? this.text.split("\n").length : 0; },
|
|
18
|
+
raw() { return this.api ? this.api.fileUrl(this.path, "resolved.yaml") : ""; },
|
|
19
|
+
mutable() {
|
|
20
|
+
const plan = (this.record && this.record.plan) || {};
|
|
21
|
+
return Array.isArray(plan.mutable) ? plan.mutable : [];
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
beforeUnmount() {
|
|
25
|
+
if (this.timer) clearTimeout(this.timer);
|
|
26
|
+
},
|
|
27
|
+
methods: {
|
|
28
|
+
flash(message) {
|
|
29
|
+
this.notice = message;
|
|
30
|
+
if (this.timer) clearTimeout(this.timer);
|
|
31
|
+
this.timer = setTimeout(() => {
|
|
32
|
+
this.notice = "";
|
|
33
|
+
this.timer = null;
|
|
34
|
+
}, 1800);
|
|
35
|
+
},
|
|
36
|
+
selectText() {
|
|
37
|
+
const box = this.$refs.yaml;
|
|
38
|
+
const selection = window.getSelection ? window.getSelection() : null;
|
|
39
|
+
if (!box || !selection) return false;
|
|
40
|
+
const range = document.createRange();
|
|
41
|
+
range.selectNodeContents(box);
|
|
42
|
+
selection.removeAllRanges();
|
|
43
|
+
selection.addRange(range);
|
|
44
|
+
try {
|
|
45
|
+
return Boolean(document.execCommand && document.execCommand("copy"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async copy() {
|
|
51
|
+
try {
|
|
52
|
+
await navigator.clipboard.writeText(this.text);
|
|
53
|
+
this.flash("copied");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.flash(this.selectText() ? "copied" : "selected; copy it with the keyboard");
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
template: `
|
|
60
|
+
<div class="page pane-config">
|
|
61
|
+
<div class="muted small">The config as the run resolved it (resolved.yaml), after the sets and params of the command line;
|
|
62
|
+
continuing the record reads this file.</div>
|
|
63
|
+
<div class="empty" v-if="!text">this record has no resolved.yaml</div>
|
|
64
|
+
<section class="card" v-else>
|
|
65
|
+
<div class="card-head">
|
|
66
|
+
<span class="card-title mono">resolved.yaml</span>
|
|
67
|
+
<span class="card-tools">
|
|
68
|
+
<span class="muted small">{{ lines }} lines</span>
|
|
69
|
+
<span class="muted small" v-if="notice">{{ notice }}</span>
|
|
70
|
+
<button type="button" class="tool" @click="copy">copy</button>
|
|
71
|
+
<a class="tool" :href="raw" download="resolved.yaml">save</a>
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<pre class="mono" ref="yaml" :style="code">{{ text }}</pre>
|
|
75
|
+
</section>
|
|
76
|
+
<section class="card" v-if="mutable.length">
|
|
77
|
+
<div class="card-head"><span class="card-title">mutable paths</span><span class="muted small">what a rule may patch while the run trains</span></div>
|
|
78
|
+
<div><span class="chip mono" v-for="item in mutable" :key="item">{{ item }}</span></div>
|
|
79
|
+
</section>
|
|
80
|
+
</div>`,
|
|
81
|
+
};
|