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