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/board/answers.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
import numbers
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from datetime import date, datetime, time
|
|
8
|
+
from pathlib import Path, PurePath
|
|
9
|
+
|
|
10
|
+
import numpy
|
|
11
|
+
|
|
12
|
+
from kalfa.commands.report import write_report
|
|
13
|
+
from kalfa.commands.stop import stop_record
|
|
14
|
+
from kalfa.core.errors import KalfaError
|
|
15
|
+
from kalfa.records import compare as comparing
|
|
16
|
+
from kalfa.records import reader, stats
|
|
17
|
+
from kalfa.records import sweep as sweeping
|
|
18
|
+
from kalfa.records.reader import file_stamp, last_lines, record_stamp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
TEXT_SUFFIXES = (".yaml", ".yml", ".json", ".jsonl", ".txt", ".md", ".csv", ".py", ".sh", ".sub", ".log")
|
|
22
|
+
IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".gif", ".svg")
|
|
23
|
+
TEXT_LIMIT = 200_000
|
|
24
|
+
EVENTS_LIMIT = 50
|
|
25
|
+
TEXT_TYPE = "text/plain; charset=utf-8"
|
|
26
|
+
JSON_TYPE = "application/json"
|
|
27
|
+
BINARY_TYPE = "application/octet-stream"
|
|
28
|
+
IMAGE_TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
|
|
29
|
+
".svg": "image/svg+xml"}
|
|
30
|
+
GALLERIES = ("plots/", "samples/")
|
|
31
|
+
STATES = ("done", "running", "failed", "stopped", "stale")
|
|
32
|
+
SAMPLE = 2000
|
|
33
|
+
BINS = 40
|
|
34
|
+
MAX_BINS = stats.MAX_BINS
|
|
35
|
+
SERIES_LIMIT = 2000
|
|
36
|
+
WHERE_NODES = (ast.Expression, ast.BoolOp, ast.UnaryOp, ast.BinOp, ast.Compare, ast.Name, ast.Constant, ast.List,
|
|
37
|
+
ast.Tuple, ast.Load, ast.And, ast.Or, ast.Not, ast.Invert, ast.UAdd, ast.USub, ast.Add, ast.Sub,
|
|
38
|
+
ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow, ast.BitAnd, ast.BitOr, ast.BitXor, ast.Eq,
|
|
39
|
+
ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn, ast.Is, ast.IsNot)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EscapeError(KalfaError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BadValue(KalfaError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def segments(relative):
|
|
51
|
+
text = "" if relative is None else str(relative)
|
|
52
|
+
if text.startswith("/") or "\\" in text or "\x00" in text:
|
|
53
|
+
raise EscapeError(f"{text!r} is not a relative posix path")
|
|
54
|
+
parts = text.split("/")
|
|
55
|
+
if parts[-1] == "":
|
|
56
|
+
parts.pop()
|
|
57
|
+
found = []
|
|
58
|
+
for part in parts:
|
|
59
|
+
if part == "..":
|
|
60
|
+
raise EscapeError(f"{text!r} climbs out of the board root")
|
|
61
|
+
if part == "":
|
|
62
|
+
raise EscapeError(f"{text!r} has an empty segment")
|
|
63
|
+
if part != ".":
|
|
64
|
+
found.append(part)
|
|
65
|
+
return found
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def inside(base, relative):
|
|
69
|
+
target = base.joinpath(*segments(relative)).resolve()
|
|
70
|
+
if target != base and base not in target.parents:
|
|
71
|
+
raise EscapeError(f"{relative} lies outside {base}")
|
|
72
|
+
return target
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def joined(base, name):
|
|
76
|
+
return name if base in ("", ".") else f"{base}/{name}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def count_of(value, default, label, minimum=None):
|
|
80
|
+
if value is None or value == "":
|
|
81
|
+
return default
|
|
82
|
+
try:
|
|
83
|
+
found = int(value)
|
|
84
|
+
except (TypeError, ValueError) as error:
|
|
85
|
+
raise BadValue(f"{label} must be an integer, not {value!r}") from error
|
|
86
|
+
if minimum is not None and found < minimum:
|
|
87
|
+
raise BadValue(f"{label} must be at least {minimum}, not {value!r}")
|
|
88
|
+
return found
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def checked_where(where):
|
|
92
|
+
if where is None or where == "":
|
|
93
|
+
return None
|
|
94
|
+
text = str(where)
|
|
95
|
+
try:
|
|
96
|
+
tree = ast.parse(text, mode="eval")
|
|
97
|
+
except (SyntaxError, ValueError) as error:
|
|
98
|
+
raise BadValue(f"bad where {text!r}: {error}") from error
|
|
99
|
+
for node in ast.walk(tree):
|
|
100
|
+
if not isinstance(node, WHERE_NODES):
|
|
101
|
+
raise BadValue(f"bad where {text!r}: {type(node).__name__} is not allowed")
|
|
102
|
+
return text
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def listed(values):
|
|
106
|
+
return [values] if isinstance(values, str) else list(values or [])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def finite_number(value, label):
|
|
110
|
+
try:
|
|
111
|
+
found = float(value)
|
|
112
|
+
except (TypeError, ValueError):
|
|
113
|
+
raise BadValue(f"{label} must be a number, not {value!r}") from None
|
|
114
|
+
if not math.isfinite(found):
|
|
115
|
+
raise BadValue(f"{label} must be a finite number, not {value!r}")
|
|
116
|
+
return found
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def histograms_of(histograms):
|
|
120
|
+
if histograms is None or histograms == "" or histograms == {}:
|
|
121
|
+
return None
|
|
122
|
+
if not isinstance(histograms, Mapping):
|
|
123
|
+
raise BadValue("histograms must map histogram names to bin counts, like {\"residual_hist\": 60}")
|
|
124
|
+
return {str(name): count for name, count in histograms.items()}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def overrides_of(pair, **choice):
|
|
128
|
+
found = {key: value for key, value in choice.items() if value is not None and value != ""}
|
|
129
|
+
if "threshold" in found:
|
|
130
|
+
found["threshold"] = finite_number(found["threshold"], "threshold")
|
|
131
|
+
if not found:
|
|
132
|
+
return None
|
|
133
|
+
if pair is None or pair == "":
|
|
134
|
+
named = ["histograms" if key == "bins" else key for key in found]
|
|
135
|
+
raise BadValue(f"{', '.join(named)} needs pair, the prediction column it applies to")
|
|
136
|
+
return {str(pair): found}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def manifest_in(directory):
|
|
140
|
+
return reader.safe_json(directory, reader.MANIFEST)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def image_bases(directory):
|
|
144
|
+
manifest = manifest_in(directory)
|
|
145
|
+
host = manifest.get("host")
|
|
146
|
+
files = manifest.get("config_files")
|
|
147
|
+
found = [directory]
|
|
148
|
+
if isinstance(host, dict) and isinstance(host.get("cwd"), str) and host["cwd"]:
|
|
149
|
+
found.append(Path(host["cwd"]))
|
|
150
|
+
if isinstance(files, list):
|
|
151
|
+
found.extend(Path(file).parent for file in files if isinstance(file, str) and file)
|
|
152
|
+
return [base for base in found if base.is_absolute()]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def image_file(directory, value):
|
|
156
|
+
path = Path(value).expanduser()
|
|
157
|
+
if path.is_absolute():
|
|
158
|
+
return path
|
|
159
|
+
for base in image_bases(directory):
|
|
160
|
+
if (base / path).is_file():
|
|
161
|
+
return base / path
|
|
162
|
+
return directory / path
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def summary_or_note(directory, cache=None, manifest=None):
|
|
166
|
+
try:
|
|
167
|
+
return reader.summary_with(cache, directory, manifest)
|
|
168
|
+
except (OSError, ValueError) as error:
|
|
169
|
+
return {"path": str(directory), "name": Path(directory).name, "status": "unknown", "error": str(error)}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def counts_of(root):
|
|
173
|
+
states = [reader.state_of(manifest_in(point)) for point in sweeping.point_dirs(root)]
|
|
174
|
+
return {"count": len(states), **{state: states.count(state) for state in STATES}}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def gallery(files):
|
|
178
|
+
return [entry["path"] for entry in files
|
|
179
|
+
if entry["path"].startswith(GALLERIES) and entry["path"].lower().endswith(IMAGE_SUFFIXES)]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def clipped(data, size):
|
|
183
|
+
cut = data.find(b"\n")
|
|
184
|
+
if 0 <= cut < len(data) - 1:
|
|
185
|
+
data = data[cut + 1:]
|
|
186
|
+
return f"... {size - len(data)} bytes skipped\n".encode("utf-8") + data
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def read_text(path, tail=None):
|
|
190
|
+
if tail is None:
|
|
191
|
+
size = path.stat().st_size
|
|
192
|
+
with path.open("rb") as handle:
|
|
193
|
+
if size > TEXT_LIMIT:
|
|
194
|
+
handle.seek(size - TEXT_LIMIT)
|
|
195
|
+
data = handle.read(TEXT_LIMIT)
|
|
196
|
+
else:
|
|
197
|
+
data = handle.read()
|
|
198
|
+
else:
|
|
199
|
+
data = last_lines(path, tail)
|
|
200
|
+
size = len(data)
|
|
201
|
+
data = data[-TEXT_LIMIT:]
|
|
202
|
+
if len(data) >= size:
|
|
203
|
+
return data, tail is None
|
|
204
|
+
return clipped(data, size), False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class Board:
|
|
208
|
+
def __init__(self, root):
|
|
209
|
+
self.root = Path(root).resolve()
|
|
210
|
+
if not self.root.is_dir():
|
|
211
|
+
raise KalfaError(f"{root} is not a directory")
|
|
212
|
+
self.cache = reader.Cache()
|
|
213
|
+
|
|
214
|
+
def resolve(self, relative):
|
|
215
|
+
directory = inside(self.root, relative)
|
|
216
|
+
if not directory.is_dir():
|
|
217
|
+
raise KalfaError(f"no record directory {relative} under {self.root}")
|
|
218
|
+
return directory
|
|
219
|
+
|
|
220
|
+
def name_of(self, path):
|
|
221
|
+
return Path(path).relative_to(self.root).as_posix()
|
|
222
|
+
|
|
223
|
+
def record_dir(self, relative):
|
|
224
|
+
directory = self.resolve(relative)
|
|
225
|
+
if not reader.is_record(directory):
|
|
226
|
+
raise KalfaError(f"{relative} is not a record directory")
|
|
227
|
+
return directory
|
|
228
|
+
|
|
229
|
+
def records(self):
|
|
230
|
+
found = []
|
|
231
|
+
for entry in reader.records_under(self.root):
|
|
232
|
+
directory = self.root / entry["path"]
|
|
233
|
+
if entry["kind"] == "sweep":
|
|
234
|
+
found.append({"path": entry["path"], "kind": "sweep",
|
|
235
|
+
"sweep": {**entry["manifest"], **counts_of(directory)}})
|
|
236
|
+
else:
|
|
237
|
+
found.append({"path": entry["path"], "kind": "record",
|
|
238
|
+
"summary": summary_or_note(directory, self.cache, entry["manifest"])})
|
|
239
|
+
return {"root": str(self.root), "records": found}
|
|
240
|
+
|
|
241
|
+
def record(self, relative):
|
|
242
|
+
directory = self.record_dir(relative)
|
|
243
|
+
manifest = reader.read_manifest(directory)
|
|
244
|
+
plan = reader.read_plan(directory)
|
|
245
|
+
history = reader.read_history(directory)
|
|
246
|
+
files = reader.files_of(directory)
|
|
247
|
+
return {"path": self.name_of(directory), "summary": reader.summary_of(directory), "plan": plan,
|
|
248
|
+
"progress": reader.progress_from(manifest, history), "best": reader.best_from(plan, history),
|
|
249
|
+
"model_summary": reader.model_summary_of(directory), "data_report": reader.data_report_of(directory),
|
|
250
|
+
"events": reader.events_of(directory, limit=EVENTS_LIMIT), "files": files,
|
|
251
|
+
"config": comparing.config_of(directory), "fields": reader.fields_of(directory),
|
|
252
|
+
"has_predictions": (directory / stats.PREDICTIONS).is_file(), "plots": gallery(files),
|
|
253
|
+
"tables": stats.prediction_tables(directory), "failure": reader.failure_of(directory),
|
|
254
|
+
"marks": reader.rule_turns(history)}
|
|
255
|
+
|
|
256
|
+
def series(self, relative, keys=None, limit=SERIES_LIMIT):
|
|
257
|
+
return reader.series_of(self.resolve(relative), keys or None, count_of(limit, SERIES_LIMIT, "limit"))
|
|
258
|
+
|
|
259
|
+
def updates(self, relative, keys=None, limit=SERIES_LIMIT):
|
|
260
|
+
return reader.updates_of(self.resolve(relative), keys or None, count_of(limit, SERIES_LIMIT, "limit"))
|
|
261
|
+
|
|
262
|
+
def events(self, relative, kinds=None, limit=None):
|
|
263
|
+
return reader.events_of(self.resolve(relative), kinds or None, count_of(limit, None, "limit"))
|
|
264
|
+
|
|
265
|
+
def files(self, relative):
|
|
266
|
+
return reader.files_of(self.resolve(relative))
|
|
267
|
+
|
|
268
|
+
def file(self, relative, name, tail=None):
|
|
269
|
+
directory = self.resolve(relative)
|
|
270
|
+
path = inside(directory, name)
|
|
271
|
+
if not path.is_file():
|
|
272
|
+
raise KalfaError(f"no file {name} in {self.name_of(directory)}")
|
|
273
|
+
suffix = path.suffix.lower()
|
|
274
|
+
if suffix not in TEXT_SUFFIXES:
|
|
275
|
+
return path.read_bytes(), IMAGE_TYPES.get(suffix, BINARY_TYPE)
|
|
276
|
+
data, whole = read_text(path, count_of(tail, None, "tail"))
|
|
277
|
+
return data, JSON_TYPE if suffix == ".json" and whole else TEXT_TYPE
|
|
278
|
+
|
|
279
|
+
def predictions(self, relative, where=None, sample=SAMPLE, bins=BINS, name=stats.PREDICTIONS, pair=None,
|
|
280
|
+
task=None, score=None, positive=None, threshold=None, histograms=None):
|
|
281
|
+
directory = self.resolve(relative)
|
|
282
|
+
path = inside(directory, name or stats.PREDICTIONS)
|
|
283
|
+
if not path.is_file():
|
|
284
|
+
return None
|
|
285
|
+
options = {"where": checked_where(where), "sample": count_of(sample, SAMPLE, "sample", 0),
|
|
286
|
+
"bins": min(count_of(bins, BINS, "bins", 1), MAX_BINS),
|
|
287
|
+
"overrides": overrides_of(pair, task=task, score=score, positive=positive, threshold=threshold,
|
|
288
|
+
bins=histograms_of(histograms))}
|
|
289
|
+
try:
|
|
290
|
+
return stats.prediction_stats(directory, plan=reader.read_plan(directory),
|
|
291
|
+
name=path.relative_to(directory).as_posix(), **options)
|
|
292
|
+
except KalfaError as error:
|
|
293
|
+
raise BadValue(str(error)) from error
|
|
294
|
+
|
|
295
|
+
def image(self, relative, index, name=stats.PREDICTIONS):
|
|
296
|
+
directory = self.resolve(relative)
|
|
297
|
+
path = inside(directory, name or stats.PREDICTIONS)
|
|
298
|
+
table = path.relative_to(directory).as_posix()
|
|
299
|
+
if not path.is_file():
|
|
300
|
+
raise KalfaError(f"no table {table} in {self.name_of(directory)}")
|
|
301
|
+
position = count_of(index, None, "index", 0)
|
|
302
|
+
if position is None:
|
|
303
|
+
raise BadValue("index is required")
|
|
304
|
+
frame = stats.load_table(directory, table)
|
|
305
|
+
if frame is None:
|
|
306
|
+
raise KalfaError(f"{table} cannot be read")
|
|
307
|
+
if position >= len(frame):
|
|
308
|
+
raise BadValue(f"index {position} is past the {len(frame)} rows of {table}")
|
|
309
|
+
value = frame["path"].iloc[position] if "path" in frame.columns else None
|
|
310
|
+
suffix = PurePath(value).suffix.lower() if isinstance(value, str) else ""
|
|
311
|
+
if suffix not in stats.IMAGE_TYPES:
|
|
312
|
+
raise BadValue(f"row {position} of {table} names no image file in its path column")
|
|
313
|
+
found = image_file(directory, value)
|
|
314
|
+
if not found.is_file():
|
|
315
|
+
raise KalfaError(f"no image file {value}")
|
|
316
|
+
return found.read_bytes(), stats.IMAGE_TYPES[suffix]
|
|
317
|
+
|
|
318
|
+
def compare(self, relatives, keys=None):
|
|
319
|
+
relatives = listed(relatives)
|
|
320
|
+
if not relatives:
|
|
321
|
+
raise KalfaError("compare needs at least one record")
|
|
322
|
+
directories = [self.record_dir(relative) for relative in relatives]
|
|
323
|
+
found = comparing.compare(directories, keys or None)
|
|
324
|
+
return {**found, "names": [self.name_of(directory) for directory in directories]}
|
|
325
|
+
|
|
326
|
+
def sweep(self, relative):
|
|
327
|
+
root = self.resolve(relative)
|
|
328
|
+
if not sweeping.is_sweep(root):
|
|
329
|
+
raise KalfaError(f"{relative} is not a sweep root")
|
|
330
|
+
spec = sweeping.read_sweep(root)
|
|
331
|
+
objective = spec.get("objective") if isinstance(spec, dict) else None
|
|
332
|
+
points = sorted(sweeping.sweep_points(root, self.cache), key=sweeping.id_key)
|
|
333
|
+
names = []
|
|
334
|
+
for point in points:
|
|
335
|
+
names.extend(key for key in point["values"] if key not in names)
|
|
336
|
+
return {"root": self.name_of(root), "sweep": spec, "points": points,
|
|
337
|
+
"ranked": [point["id"] for point in sweeping.rank_points(points, objective)],
|
|
338
|
+
"best": sweeping.best_point(points, objective),
|
|
339
|
+
"params": {key: [point["values"].get(key) for point in points] for key in names},
|
|
340
|
+
"objectives": [point["objective"] for point in points], "ids": [point["id"] for point in points],
|
|
341
|
+
"order": sweeping.running_best(points, objective), "space": sweeping.axes_of(points, spec)}
|
|
342
|
+
|
|
343
|
+
def stop(self, relative):
|
|
344
|
+
written = stop_record(self.resolve(relative), by="board")
|
|
345
|
+
return {"paths": [self.name_of(path) for path in written]}
|
|
346
|
+
|
|
347
|
+
def report(self, relatives, out=None):
|
|
348
|
+
directories = [self.resolve(relative) for relative in listed(relatives)]
|
|
349
|
+
target = None if out in (None, "") else inside(self.root, out)
|
|
350
|
+
return str(write_report(directories, out=target))
|
|
351
|
+
|
|
352
|
+
def stamps(self):
|
|
353
|
+
found = {}
|
|
354
|
+
for entry in reader.records_under(self.root):
|
|
355
|
+
directory = self.root / entry["path"]
|
|
356
|
+
if entry["kind"] != "sweep":
|
|
357
|
+
found[entry["path"]] = record_stamp(directory, entry["manifest"])
|
|
358
|
+
continue
|
|
359
|
+
parts = [file_stamp(directory / reader.SWEEP)]
|
|
360
|
+
for point in sweeping.point_dirs(directory):
|
|
361
|
+
stamp = record_stamp(point, manifest_in(point))
|
|
362
|
+
found[joined(entry["path"], point.name)] = stamp
|
|
363
|
+
parts.append(f"{point.name}={stamp}")
|
|
364
|
+
found[entry["path"]] = ";".join(parts)
|
|
365
|
+
return found
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def key_of(key):
|
|
369
|
+
if isinstance(key, str):
|
|
370
|
+
return str(key)
|
|
371
|
+
found = jsonable(key)
|
|
372
|
+
return found if isinstance(found, str) else json.dumps(found)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def ordered(values):
|
|
376
|
+
try:
|
|
377
|
+
return sorted(values)
|
|
378
|
+
except TypeError:
|
|
379
|
+
return list(values)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def is_tensor(value):
|
|
383
|
+
torch = sys.modules.get("torch")
|
|
384
|
+
return torch is not None and isinstance(value, torch.Tensor)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def jsonable(value):
|
|
388
|
+
if value is None or isinstance(value, bool):
|
|
389
|
+
return value
|
|
390
|
+
if isinstance(value, str):
|
|
391
|
+
return str(value)
|
|
392
|
+
if isinstance(value, int):
|
|
393
|
+
return int(value)
|
|
394
|
+
if isinstance(value, float):
|
|
395
|
+
return float(value) if math.isfinite(value) else None
|
|
396
|
+
if isinstance(value, Mapping):
|
|
397
|
+
return {key_of(key): jsonable(item) for key, item in value.items()}
|
|
398
|
+
if isinstance(value, (list, tuple)):
|
|
399
|
+
return [jsonable(item) for item in value]
|
|
400
|
+
if isinstance(value, (set, frozenset)):
|
|
401
|
+
return [jsonable(item) for item in ordered(value)]
|
|
402
|
+
if isinstance(value, PurePath):
|
|
403
|
+
return str(value)
|
|
404
|
+
if isinstance(value, (datetime, date, time)):
|
|
405
|
+
return value.isoformat()
|
|
406
|
+
if isinstance(value, numpy.ndarray):
|
|
407
|
+
return jsonable(value.tolist())
|
|
408
|
+
if isinstance(value, numpy.generic):
|
|
409
|
+
return jsonable(value.item())
|
|
410
|
+
if is_tensor(value):
|
|
411
|
+
return jsonable(value.detach().cpu().tolist())
|
|
412
|
+
if isinstance(value, (bytes, bytearray)):
|
|
413
|
+
return bytes(value).decode("utf-8", errors="replace")
|
|
414
|
+
if isinstance(value, numbers.Real):
|
|
415
|
+
return jsonable(float(value))
|
|
416
|
+
tolist = getattr(value, "tolist", None)
|
|
417
|
+
if callable(tolist):
|
|
418
|
+
return jsonable(tolist())
|
|
419
|
+
return str(value)
|