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,179 @@
1
+ import { computed, defineAsyncComponent, onBeforeUnmount, onErrorCaptured, onMounted, ref, watch }
2
+ from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
3
+
4
+ import { client as api } from "../api.js";
5
+ import { Badge } from "../components/badge.js";
6
+ import { ErrorBox } from "../components/error.js";
7
+ import { Loading } from "../components/loading.js";
8
+ import { Progress } from "../components/progress.js";
9
+ import { bestText, liveNote, nonfiniteText, seconds, when } from "../format.js";
10
+ import { TABS, entryOf, isSweepEntry, onLive, recordLink, sweepLink, touches } from "../store.js";
11
+
12
+
13
+ function lazy(loader) {
14
+ return defineAsyncComponent({ loader, errorComponent: ErrorBox, loadingComponent: Loading, delay: 150 });
15
+ }
16
+
17
+ export const TAB_VIEWS = {
18
+ overview: lazy(() => import("./record/overview.js")),
19
+ curves: lazy(() => import("./record/curves.js")),
20
+ model: lazy(() => import("./record/model.js")),
21
+ data: lazy(() => import("./record/data.js")),
22
+ predictions: lazy(() => import("./record/predictions.js")),
23
+ plots: lazy(() => import("./record/plots.js")),
24
+ config: lazy(() => import("./record/config.js")),
25
+ files: lazy(() => import("./record/files.js")),
26
+ log: lazy(() => import("./record/log.js")),
27
+ };
28
+ const STOPPABLE = ["running", "stale"];
29
+
30
+ export function parentOf(path) {
31
+ const parts = String(path || "").split("/").filter(Boolean);
32
+ return parts.slice(0, -1).join("/");
33
+ }
34
+
35
+ export const Record = {
36
+ name: "RecordView",
37
+ components: { KBadge: Badge, KError: ErrorBox, KProgress: Progress },
38
+ props: {
39
+ path: { type: String, default: "" },
40
+ tab: { type: String, default: "overview" },
41
+ },
42
+ setup(props) {
43
+ const record = ref(null);
44
+ const error = ref("");
45
+ const loading = ref(false);
46
+ const version = ref(0);
47
+ const tabError = ref("");
48
+ const stopping = ref(false);
49
+ const stopNote = ref("");
50
+ let ticket = 0;
51
+ let off = null;
52
+
53
+ const isRecord = async (path) => {
54
+ try {
55
+ await api.getJson(api.recordUrl(path));
56
+ return true;
57
+ } catch (failure) {
58
+ return false;
59
+ }
60
+ };
61
+ const load = async () => {
62
+ const mine = ++ticket;
63
+ loading.value = true;
64
+ try {
65
+ const found = await api.getJson(api.recordUrl(props.path));
66
+ if (mine !== ticket) return;
67
+ record.value = found;
68
+ version.value += 1;
69
+ error.value = "";
70
+ } catch (failure) {
71
+ if (mine !== ticket) return;
72
+ const longer = props.path + "/" + props.tab;
73
+ if (failure.status === 404 && await isRecord(longer)) {
74
+ window.location.hash = recordLink(longer, "overview");
75
+ return;
76
+ }
77
+ error.value = failure.message;
78
+ } finally {
79
+ if (mine === ticket) loading.value = false;
80
+ }
81
+ };
82
+
83
+ const summary = computed(() => (record.value && record.value.summary) || {});
84
+ const progress = computed(() => (record.value && record.value.progress) || {});
85
+ const best = computed(() => (record.value && record.value.best) || summary.value.best || null);
86
+ const status = computed(() => summary.value.status || progress.value.status || "unknown");
87
+ const current = computed(() => (TABS.includes(props.tab) ? props.tab : "overview"));
88
+ const view = computed(() => TAB_VIEWS[current.value] || TAB_VIEWS.overview);
89
+ const canStop = computed(() => STOPPABLE.includes(status.value));
90
+ const asSweep = computed(() => isSweepEntry(entryOf(props.path)));
91
+ const sweepParent = computed(() => (summary.value.sweep ? parentOf(props.path) : ""));
92
+ const tabLink = (tab) => recordLink(props.path, tab);
93
+ const note = computed(() => liveNote(progress.value));
94
+ const warning = computed(() => nonfiniteText(summary.value.nonfinite));
95
+
96
+ const stop = async () => {
97
+ if (stopping.value) return;
98
+ stopping.value = true;
99
+ stopNote.value = "";
100
+ try {
101
+ const written = await api.postJson(api.recordUrl(props.path, "stop"), {});
102
+ const paths = (written && written.paths) || [];
103
+ stopNote.value = paths.length ? "asked to stop: " + paths.join(", ") : "nothing to stop";
104
+ load();
105
+ } catch (failure) {
106
+ stopNote.value = "";
107
+ error.value = failure.message;
108
+ } finally {
109
+ stopping.value = false;
110
+ }
111
+ };
112
+
113
+ watch(() => props.tab, () => {
114
+ tabError.value = "";
115
+ });
116
+ onErrorCaptured((failure) => {
117
+ tabError.value = "the " + current.value + " tab failed: " + (failure && failure.message ? failure.message
118
+ : String(failure));
119
+ return false;
120
+ });
121
+ onMounted(() => {
122
+ load();
123
+ off = onLive((changed) => {
124
+ if (touches(changed, [props.path])) load();
125
+ });
126
+ });
127
+ onBeforeUnmount(() => {
128
+ ticket += 1;
129
+ if (off) off();
130
+ });
131
+ return {
132
+ api, record, error, loading, version, tabError, stopping, stopNote, summary, progress, best, status, current,
133
+ view, canStop, asSweep, sweepParent, tabLink, note, warning, stop, TABS, bestText, seconds, when, sweepLink,
134
+ };
135
+ },
136
+ template: `
137
+ <section class="page record">
138
+ <k-error :error="error"></k-error>
139
+ <div v-if="asSweep" class="notice">
140
+ <span class="mono">{{ path }}</span> is a sweep: <a class="link" :href="sweepLink(path)">open the sweep page</a>
141
+ </div>
142
+ <div v-if="loading && !record && !error" class="loading">reading the record</div>
143
+ <template v-if="record">
144
+ <header class="record-head card">
145
+ <div class="record-title">
146
+ <h2>{{ summary.name || path }}</h2>
147
+ <k-badge :status="status"></k-badge>
148
+ <span class="spacer"></span>
149
+ <button v-if="canStop" type="button" class="button danger" :disabled="stopping" @click="stop"
150
+ title="write stop.json; the run stops after its current turn">{{ stopping ? "stopping" : "stop" }}</button>
151
+ </div>
152
+ <div class="record-meta">
153
+ <span class="mono muted">{{ path }}</span>
154
+ <span v-if="sweepParent">point of <a class="link" :href="sweepLink(sweepParent)">{{ sweepParent }}</a></span>
155
+ <k-progress :value="progress.turn || summary.turn || 0" :total="progress.total || summary.total || null"
156
+ :part="progress.fraction"
157
+ :label="(progress.turn || summary.turn || 0) + ' / ' + (progress.total || summary.total || '?') + ' ' + (progress.unit || summary.unit || 'turns')">
158
+ </k-progress>
159
+ <span v-if="note" class="muted">{{ note }}</span>
160
+ <span v-if="best && best.series">best <b>{{ bestText(best) }}</b><span v-if="best.turn" class="muted"> at turn {{ best.turn }}</span></span>
161
+ <span class="muted">{{ seconds(progress.seconds || summary.seconds) }}</span>
162
+ <span class="muted">started {{ when(summary.started) }}</span>
163
+ </div>
164
+ <div v-if="summary.failure" class="error-box">{{ summary.failure }}</div>
165
+ <div v-if="warning" class="error-box">{{ warning }}</div>
166
+ <div v-if="stopNote" class="notice small">{{ stopNote }}</div>
167
+ </header>
168
+ <nav class="tabs">
169
+ <a v-for="name in TABS" :key="name" class="tab" :class="{ active: name === current }" :href="tabLink(name)">{{ name }}</a>
170
+ </nav>
171
+ <k-error :error="tabError"></k-error>
172
+ <div class="tab-body">
173
+ <component :is="view" :key="current" :record="record" :path="path" :api="api" :version="version"></component>
174
+ </div>
175
+ </template>
176
+ </section>`,
177
+ };
178
+
179
+ export default Record;
@@ -0,0 +1,124 @@
1
+ import { computed } from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
2
+
3
+ import { ErrorBox } from "../components/error.js";
4
+ import { Table } from "../components/table.js";
5
+ import { EMPTY, seconds, text, when } from "../format.js";
6
+ import {
7
+ compareLink,
8
+ isSweepEntry,
9
+ nameOf,
10
+ progressOf,
11
+ recordLink,
12
+ setSelected,
13
+ statusOf,
14
+ store,
15
+ sweepLink,
16
+ } from "../store.js";
17
+
18
+
19
+ const summaryOf = (entry) => entry.summary || {};
20
+ const bestOf = (entry) => summaryOf(entry).best || {};
21
+ const paramsOf = (entry) => summaryOf(entry).params || {};
22
+
23
+ export function paramKeys(entries) {
24
+ const keys = [];
25
+ for (const entry of entries) {
26
+ for (const key of Object.keys(paramsOf(entry))) {
27
+ if (!keys.includes(key)) keys.push(key);
28
+ }
29
+ }
30
+ return keys;
31
+ }
32
+
33
+ export function recordColumns(entries) {
34
+ const columns = [
35
+ { key: "name", label: "name", value: nameOf, href: (entry) => recordLink(entry.path, "overview"),
36
+ title: (entry) => entry.path },
37
+ { key: "status", label: "status", value: statusOf, badge: true },
38
+ { key: "progress", label: "turn/total", value: (entry) => summaryOf(entry).turn,
39
+ progress: (entry) => progressOf(entry), numeric: true },
40
+ { key: "seconds", label: "seconds", value: (entry) => summaryOf(entry).seconds,
41
+ text: (entry) => seconds(summaryOf(entry).seconds), numeric: true },
42
+ { key: "best_series", label: "best of", value: (entry) => bestOf(entry).series },
43
+ { key: "best", label: "best", value: (entry) => bestOf(entry).value, numeric: true,
44
+ title: (entry) => (bestOf(entry).turn ? "at turn " + bestOf(entry).turn : "") },
45
+ { key: "started", label: "started", value: (entry) => summaryOf(entry).started,
46
+ text: (entry) => when(summaryOf(entry).started), descending: true },
47
+ ];
48
+ for (const key of paramKeys(entries)) {
49
+ columns.push({ key: "param:" + key, label: key, value: (entry) => paramsOf(entry)[key],
50
+ text: (entry) => text(paramsOf(entry)[key]), numeric: true, hint: "param " + key });
51
+ }
52
+ return columns;
53
+ }
54
+
55
+ export function failedText(sweep) {
56
+ const parts = [];
57
+ if (sweep.failed) parts.push(sweep.failed + " failed");
58
+ if (sweep.stale) parts.push(sweep.stale + " stale");
59
+ return parts.join(", ") || EMPTY;
60
+ }
61
+
62
+ export function sweepColumns() {
63
+ const sweepOf = (entry) => entry.sweep || {};
64
+ const objectiveOf = (entry) => sweepOf(entry).objective || {};
65
+ return [
66
+ { key: "name", label: "sweep", value: (entry) => entry.path, href: (entry) => sweepLink(entry.path) },
67
+ { key: "status", label: "status", value: statusOf, badge: true },
68
+ { key: "points", label: "points done", value: (entry) => sweepOf(entry).done,
69
+ progress: (entry) => progressOf(entry), numeric: true },
70
+ { key: "running", label: "running", value: (entry) => sweepOf(entry).running, numeric: true },
71
+ { key: "failed", label: "failed", value: (entry) => sweepOf(entry).failed, numeric: true,
72
+ hint: "points that failed or went stale", text: (entry) => failedText(sweepOf(entry)) },
73
+ { key: "strategy", label: "strategy", value: (entry) => (sweepOf(entry).strategy || {}).use },
74
+ { key: "objective", label: "objective", value: (entry) => objectiveOf(entry).watch,
75
+ text: (entry) => (objectiveOf(entry).watch ? objectiveOf(entry).watch + " (" + (objectiveOf(entry).mode || "min")
76
+ + ")" : EMPTY) },
77
+ { key: "started", label: "started", value: (entry) => sweepOf(entry).started,
78
+ text: (entry) => when(sweepOf(entry).started), descending: true },
79
+ ];
80
+ }
81
+
82
+ export const Runs = {
83
+ name: "RunsView",
84
+ components: { KError: ErrorBox, KTable: Table },
85
+ setup() {
86
+ const records = computed(() => store.records.filter((entry) => !isSweepEntry(entry)));
87
+ const sweeps = computed(() => store.records.filter(isSweepEntry));
88
+ const columns = computed(() => recordColumns(records.value));
89
+ const sweepTable = sweepColumns();
90
+ const chosen = computed(() => store.selected.filter((path) => records.value.some((entry) => entry.path === path)));
91
+ const compareHref = computed(() => compareLink(chosen.value));
92
+ const select = (paths) => setSelected(paths);
93
+ const clear = () => setSelected([]);
94
+ return { store, records, sweeps, columns, sweepTable, chosen, compareHref, select, clear };
95
+ },
96
+ template: `
97
+ <section class="page runs">
98
+ <k-error :error="store.error"></k-error>
99
+ <div v-if="!store.loaded" class="loading">reading the records</div>
100
+ <template v-else>
101
+ <div class="page-head">
102
+ <h2 class="section-title">records</h2>
103
+ <span class="muted small mono">{{ store.root }}</span>
104
+ </div>
105
+ <k-table :columns="columns" :rows="records" row-key="path" selectable :selected="store.selected"
106
+ @update:selected="select" sort-key="started" :sort-desc="true" placeholder="filter by name, status or param"
107
+ empty="no records under this root">
108
+ <template #tools>
109
+ <span class="spacer"></span>
110
+ <span class="muted small" v-if="chosen.length">{{ chosen.length }} selected</span>
111
+ <a class="button" :class="{ disabled: !chosen.length }" :href="chosen.length ? compareHref : null">compare</a>
112
+ <button type="button" class="button ghost" v-if="chosen.length" @click="clear">clear</button>
113
+ </template>
114
+ </k-table>
115
+ <template v-if="sweeps.length">
116
+ <h2 class="section-title">sweeps</h2>
117
+ <k-table :columns="sweepTable" :rows="sweeps" row-key="path" :filterable="false" sort-key="started"
118
+ :sort-desc="true"></k-table>
119
+ </template>
120
+ </template>
121
+ </section>`,
122
+ };
123
+
124
+ export default Runs;
@@ -0,0 +1,282 @@
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 { fileUrl, getJson, getText, postJson, sweepUrl } from "../api.js";
5
+ import { Chart } from "../components/chart.js";
6
+ import { ErrorBox } from "../components/error.js";
7
+ import { Table } from "../components/table.js";
8
+ import { EMPTY, number, text, when } from "../format.js";
9
+ import { compareLink, onLive, recordLink, setSelected, store, touches } from "../store.js";
10
+
11
+
12
+ const POLL = 1000;
13
+ const BEST = [5, 10, 20];
14
+
15
+ export function pointPath(root, point) {
16
+ return root && root !== "." ? root + "/" + point.path : point.path;
17
+ }
18
+
19
+ export function baseName(path) {
20
+ const parts = String(path || "").split(/[\\/]/).filter(Boolean);
21
+ return parts.length ? parts[parts.length - 1] : "";
22
+ }
23
+
24
+ export function isLogSpace(spec) {
25
+ return Boolean(spec) && typeof spec === "object" && !Array.isArray(spec) && Array.isArray(spec.log);
26
+ }
27
+
28
+ export function pointColumns(root, answer) {
29
+ const ranked = (answer && answer.ranked) || [];
30
+ const keys = Object.keys((answer && answer.params) || {});
31
+ const objective = ((answer && answer.sweep) || {}).objective || {};
32
+ const columns = [
33
+ { key: "id", label: "point", value: (point) => point.id, href: (point) => recordLink(pointPath(root, point)),
34
+ numeric: true, title: (point) => point.path },
35
+ { key: "rank", label: "rank", value: (point) => (ranked.indexOf(point.id) >= 0 ? ranked.indexOf(point.id) + 1 : null),
36
+ numeric: true },
37
+ { key: "status", label: "status", value: (point) => point.status, badge: true },
38
+ { key: "progress", label: "turn/total", value: (point) => point.turn,
39
+ progress: (point) => ({ value: point.turn || 0, total: point.total || null }), numeric: true },
40
+ { key: "objective", label: objective.watch || "objective", value: (point) => point.objective, numeric: true,
41
+ descending: objective.mode === "max", hint: "objective (" + (objective.mode || "min") + ")" },
42
+ ];
43
+ for (const key of keys) {
44
+ columns.push({ key: "param:" + key, label: key, value: (point) => (point.values || {})[key],
45
+ text: (point) => text((point.values || {})[key]), numeric: true });
46
+ }
47
+ return columns;
48
+ }
49
+
50
+ export function orderLines(answer) {
51
+ const order = (answer && answer.order) || {};
52
+ const ids = Array.isArray(order.ids) ? order.ids : [];
53
+ if (!ids.length) return [];
54
+ return [
55
+ { name: "objective", x: ids, y: order.objective || [], mode: "markers" },
56
+ { name: "best so far", x: ids, y: order.best || [], mode: "lines" },
57
+ ];
58
+ }
59
+
60
+ export function scatters(answer) {
61
+ if (!answer) return [];
62
+ const params = answer.params || {};
63
+ const ids = answer.ids || [];
64
+ const objectives = answer.objectives || [];
65
+ const space = ((answer.sweep || {}).space) || {};
66
+ const objective = ((answer.sweep || {}).objective) || {};
67
+ const best = answer.best;
68
+ return Object.keys(params).map((key) => {
69
+ const groups = [{ name: "points", x: params[key], y: objectives, text: ids.map((id) => "point " + id) }];
70
+ if (best && best.values && best.values[key] !== undefined) {
71
+ groups.push({ name: "best", x: [best.values[key]], y: [best.objective], text: ["best: point " + best.id],
72
+ size: 12 });
73
+ }
74
+ return { key, groups, options: { xlabel: key, ylabel: objective.watch || "objective", logx: isLogSpace(space[key]) } };
75
+ });
76
+ }
77
+
78
+ export const Sweep = {
79
+ name: "SweepView",
80
+ components: { KChart: Chart, KError: ErrorBox, KTable: Table },
81
+ props: { path: { type: String, default: "" } },
82
+ setup(props) {
83
+ const answer = ref(null);
84
+ const error = ref("");
85
+ const loading = ref(false);
86
+ const job = ref(null);
87
+ const report = ref({ status: "", path: "", error: "", url: "", name: "" });
88
+ let ticket = 0;
89
+ let timer = null;
90
+ let off = null;
91
+ let alive = true;
92
+
93
+ const load = async () => {
94
+ const mine = ++ticket;
95
+ loading.value = true;
96
+ try {
97
+ const found = await getJson(sweepUrl(props.path));
98
+ if (mine !== ticket) return;
99
+ answer.value = found;
100
+ error.value = "";
101
+ } catch (failure) {
102
+ if (mine === ticket) error.value = failure.message;
103
+ } finally {
104
+ if (mine === ticket) loading.value = false;
105
+ }
106
+ };
107
+
108
+ const forget = () => {
109
+ if (report.value.url) URL.revokeObjectURL(report.value.url);
110
+ };
111
+ const fetchReport = async (path) => {
112
+ const name = baseName(path) || "report.html";
113
+ try {
114
+ const body = await getText(fileUrl(props.path, name));
115
+ if (!alive) return;
116
+ forget();
117
+ const url = URL.createObjectURL(new Blob([body], { type: "text/html" }));
118
+ report.value = { status: "done", path, error: "", url, name };
119
+ } catch (failure) {
120
+ report.value = { status: "done", path, error: "written, but the board cannot read it back: " + failure.message,
121
+ url: "", name };
122
+ }
123
+ };
124
+ const poll = async () => {
125
+ timer = null;
126
+ if (!alive || job.value === null) return;
127
+ try {
128
+ const state = await getJson("/api/report/" + encodeURIComponent(job.value));
129
+ if (!alive) return;
130
+ if (state.status === "running") {
131
+ timer = setTimeout(poll, POLL);
132
+ return;
133
+ }
134
+ job.value = null;
135
+ if (state.status === "done") await fetchReport(state.path || "");
136
+ else report.value = { status: "failed", path: "", error: state.error || "the report failed", url: "", name: "" };
137
+ } catch (failure) {
138
+ job.value = null;
139
+ report.value = { status: "failed", path: "", error: failure.message, url: "", name: "" };
140
+ }
141
+ };
142
+ const writeReport = async () => {
143
+ if (job.value !== null) return;
144
+ report.value = { status: "running", path: "", error: "", url: "", name: "" };
145
+ try {
146
+ const started = await postJson("/api/report", { records: [props.path], out: null });
147
+ job.value = started && started.job !== undefined ? started.job : null;
148
+ if (job.value === null) throw new Error("the server did not start a report job");
149
+ timer = setTimeout(poll, POLL);
150
+ } catch (failure) {
151
+ report.value = { status: "failed", path: "", error: failure.message, url: "", name: "" };
152
+ }
153
+ };
154
+
155
+ const info = computed(() => (answer.value && answer.value.sweep) || {});
156
+ const objective = computed(() => info.value.objective || {});
157
+ const points = computed(() => ((answer.value && answer.value.points) || [])
158
+ .map((point) => ({ ...point, full: pointPath(props.path, point) })));
159
+ const paths = computed(() => points.value.map((point) => point.full));
160
+ const chosen = computed(() => store.selected.filter((path) => paths.value.includes(path)));
161
+ const compareHref = computed(() => compareLink(chosen.value));
162
+ const select = (found) => setSelected(found);
163
+ const clear = () => setSelected(store.selected.filter((path) => !paths.value.includes(path)));
164
+ const pickBest = (count) => {
165
+ const ranked = (answer.value && answer.value.ranked) || [];
166
+ const best = ranked.slice(0, count).map((id) => points.value.find((point) => point.id === id))
167
+ .filter((point) => point && point.objective !== null && point.objective !== undefined);
168
+ setSelected(best.map((point) => point.full));
169
+ };
170
+ const order = computed(() => orderLines(answer.value));
171
+ const orderOptions = computed(() => ({ xlabel: "point", ylabel: objective.value.watch || "objective" }));
172
+ const space = computed(() => (answer.value && answer.value.space) || { axes: [], left_out: 0 });
173
+ const spaceNote = computed(() => (space.value.left_out
174
+ ? space.value.left_out + " points without an objective are not drawn; the color is the objective, the best bright"
175
+ : "a line per point; the color is the objective, the best bright; drag along an axis to keep a range"));
176
+ const best = computed(() => (answer.value && answer.value.best) || null);
177
+ const columns = computed(() => pointColumns(props.path, answer.value));
178
+ const charts = computed(() => scatters(answer.value));
179
+ const counts = computed(() => {
180
+ const found = {};
181
+ for (const point of points.value) found[point.status || "unknown"] = (found[point.status || "unknown"] || 0) + 1;
182
+ return Object.entries(found);
183
+ });
184
+ const bestLink = computed(() => (best.value ? recordLink(pointPath(props.path, best.value)) : ""));
185
+ const valuesOf = (point) => Object.entries((point && point.values) || {});
186
+ const strategy = computed(() => (info.value.strategy && info.value.strategy.use) || EMPTY);
187
+
188
+ onMounted(() => {
189
+ load();
190
+ off = onLive((changed) => {
191
+ if (touches(changed, [props.path])) load();
192
+ });
193
+ });
194
+ onBeforeUnmount(() => {
195
+ alive = false;
196
+ ticket += 1;
197
+ if (timer) clearTimeout(timer);
198
+ if (off) off();
199
+ forget();
200
+ });
201
+ return {
202
+ answer, error, loading, job, report, info, objective, points, best, columns, charts, counts, bestLink, valuesOf,
203
+ strategy, writeReport, number, text, when, store, chosen, compareHref, select, clear, pickBest, order,
204
+ orderOptions, space, spaceNote, BEST,
205
+ };
206
+ },
207
+ template: `
208
+ <section class="page sweep">
209
+ <div class="page-head">
210
+ <h2 class="section-title">sweep <span class="mono">{{ path }}</span></h2>
211
+ <span class="spacer"></span>
212
+ <button type="button" class="button" :disabled="report.status === 'running'" @click="writeReport">
213
+ {{ report.status === "running" ? "writing report.html" : "write report.html" }}
214
+ </button>
215
+ </div>
216
+ <div v-if="report.status === 'done'" class="notice">
217
+ <span>report written to <span class="mono">{{ report.path }}</span></span>
218
+ <a v-if="report.url" class="link" :href="report.url" target="_blank" rel="noopener">open</a>
219
+ <a v-if="report.url" class="link" :href="report.url" :download="report.name">download</a>
220
+ </div>
221
+ <k-error :error="report.error"></k-error>
222
+ <k-error :error="error"></k-error>
223
+ <div v-if="loading && !answer" class="loading">reading the sweep</div>
224
+ <template v-if="answer">
225
+ <div class="cards">
226
+ <div class="card stat">
227
+ <div class="stat-label">strategy</div>
228
+ <div class="stat-value">{{ strategy }}</div>
229
+ <div class="muted small">started {{ when(info.started) }}</div>
230
+ </div>
231
+ <div class="card stat">
232
+ <div class="stat-label">points</div>
233
+ <div class="stat-value">{{ points.length }} / {{ info.points || points.length }}</div>
234
+ <div class="muted small"><span v-for="pair in counts" :key="pair[0]" class="chip">{{ pair[1] }} {{ pair[0] }}</span></div>
235
+ </div>
236
+ <div class="card stat">
237
+ <div class="stat-label">objective</div>
238
+ <div class="stat-value mono">{{ objective.watch || "none" }}</div>
239
+ <div class="muted small">{{ objective.mode || "min" }} at {{ objective.at || "best" }}</div>
240
+ </div>
241
+ <div class="card stat best-card">
242
+ <div class="stat-label">best</div>
243
+ <template v-if="best">
244
+ <div class="stat-value"><a class="link" :href="bestLink">point {{ best.id }}</a> {{ number(best.objective) }}</div>
245
+ <div class="small"><span v-for="pair in valuesOf(best)" :key="pair[0]" class="chip">{{ pair[0] }} = {{ text(pair[1]) }}</span></div>
246
+ </template>
247
+ <div v-else class="muted">no point has an objective yet</div>
248
+ </div>
249
+ </div>
250
+ <h3 class="section-title">points</h3>
251
+ <k-table :columns="columns" :rows="points" row-key="full" sort-key="rank" placeholder="filter points"
252
+ selectable :selected="store.selected" @update:selected="select" empty="the sweep has no points yet">
253
+ <template #tools>
254
+ <span class="spacer"></span>
255
+ <button v-for="count in BEST" :key="count" type="button" class="tool" @click="pickBest(count)"
256
+ title="select the best points by the objective">best {{ count }}</button>
257
+ <span class="muted small" v-if="chosen.length">{{ chosen.length }} selected</span>
258
+ <a class="button" :class="{ disabled: !chosen.length }" :href="chosen.length ? compareHref : null">compare</a>
259
+ <button type="button" class="button ghost" v-if="chosen.length" @click="clear">clear</button>
260
+ </template>
261
+ </k-table>
262
+ <h3 class="section-title">the search over its order</h3>
263
+ <div v-if="!order.length" class="empty">no point has an objective yet</div>
264
+ <div class="grid" v-else>
265
+ <k-chart title="objective by point" kind="lines" :data="order" :options="orderOptions"
266
+ note="the objective of every point in the order the sweep made them, and the best value so far"></k-chart>
267
+ </div>
268
+ <h3 class="section-title">every point across the space</h3>
269
+ <div v-if="space.axes.length < 2" class="empty">no point has an objective yet</div>
270
+ <k-chart v-else title="params and objective" kind="parallel" :data="space" :height="340" :log-toggle="false"
271
+ :note="spaceNote"></k-chart>
272
+ <h3 class="section-title">objective against each param</h3>
273
+ <div v-if="!charts.length" class="empty">the sweep has no params</div>
274
+ <div class="grid">
275
+ <k-chart v-for="chart in charts" :key="chart.key" kind="scatter" :title="chart.key" :data="chart.groups"
276
+ :options="chart.options"></k-chart>
277
+ </div>
278
+ </template>
279
+ </section>`,
280
+ };
281
+
282
+ export default Sweep;
kalfa/board/watch.py ADDED
@@ -0,0 +1,87 @@
1
+ import logging
2
+ import threading
3
+ from collections import deque
4
+
5
+
6
+ KEPT = 256
7
+ LOGGER = logging.getLogger("kalfa.board")
8
+
9
+
10
+ class Watcher:
11
+ def __init__(self, board, every=2.0):
12
+ self.board = board
13
+ self.every = float(every)
14
+ self.condition = threading.Condition()
15
+ self.version = 0
16
+ self.changed = []
17
+ self.stamps = {}
18
+ self.thread = None
19
+ self.stopping = False
20
+ self.log = deque(maxlen=KEPT)
21
+ self.failure = None
22
+
23
+ def start(self):
24
+ with self.condition:
25
+ if self.thread is not None and self.thread.is_alive():
26
+ return self
27
+ self.stopping = False
28
+ found = self.read()
29
+ if found is not None:
30
+ self.stamps = found
31
+ self.thread = threading.Thread(target=self.run, name="kalfa-watch", daemon=True)
32
+ self.thread.start()
33
+ return self
34
+
35
+ def stop(self):
36
+ with self.condition:
37
+ self.stopping = True
38
+ self.condition.notify_all()
39
+ thread, self.thread = self.thread, None
40
+ if thread is not None and thread is not threading.current_thread():
41
+ thread.join()
42
+
43
+ def wait(self, version, timeout):
44
+ with self.condition:
45
+ self.condition.wait_for(lambda: self.version > version or self.stopping, timeout)
46
+ return self.version if self.version > version else None
47
+
48
+ def since(self, version):
49
+ with self.condition:
50
+ changed = {path for number, paths in self.log if number > version for path in paths}
51
+ missed = self.version > version and (not self.log or self.log[0][0] > version + 1)
52
+ if missed:
53
+ changed.update(self.stamps)
54
+ return self.version, sorted(changed)
55
+
56
+ def run(self):
57
+ while True:
58
+ with self.condition:
59
+ if self.condition.wait_for(lambda: self.stopping, self.every):
60
+ return
61
+ self.poll()
62
+
63
+ def poll(self):
64
+ found = self.read()
65
+ if found is None or found == self.stamps:
66
+ return False
67
+ with self.condition:
68
+ changed = sorted(path for path in found.keys() | self.stamps.keys()
69
+ if found.get(path) != self.stamps.get(path))
70
+ self.stamps = found
71
+ self.changed = changed
72
+ self.version += 1
73
+ self.log.append((self.version, changed))
74
+ self.condition.notify_all()
75
+ return True
76
+
77
+ def read(self):
78
+ try:
79
+ found = dict(self.board.stamps())
80
+ except Exception as error:
81
+ text = f"{type(error).__name__}: {error}"
82
+ if text != self.failure:
83
+ LOGGER.warning("the board could not read its stamps: %s", text)
84
+ self.failure = text
85
+ return None
86
+ self.failure = None
87
+ return found