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/commands/legos.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import textwrap
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Group
|
|
8
|
+
from rich.padding import Padding
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from ruamel.yaml import YAML
|
|
13
|
+
from ruamel.yaml.error import YAMLError
|
|
14
|
+
|
|
15
|
+
from kalfa.core import aliases
|
|
16
|
+
from kalfa.core.build import node_signature
|
|
17
|
+
from kalfa.core.errors import BuildError, KalfaError
|
|
18
|
+
from kalfa.std import notes
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
SLOT_ORDER = ("data", "model", "loss", "metric", "optimizer", "schedule", "step", "trigger", "action", "logger",
|
|
22
|
+
"sampler", "plot", "strategy", "value")
|
|
23
|
+
UNKNOWN = "unknown"
|
|
24
|
+
SWEEP_MODULE = "kalfa.commands.sweep"
|
|
25
|
+
REGISTRARS = (SWEEP_MODULE,)
|
|
26
|
+
REQUIRED = "required"
|
|
27
|
+
WIDTH = 100
|
|
28
|
+
NOTE_INDENT = " "
|
|
29
|
+
THEME = "ansi_dark"
|
|
30
|
+
PARENTS = Path(__file__).resolve().parents
|
|
31
|
+
ROOT = PARENTS[3] if PARENTS[2].name == "src" else None
|
|
32
|
+
EXAMPLES = "examples/*/*.yaml"
|
|
33
|
+
SECTIONS = ("models", "losses", "metrics", "optimizers", "plots")
|
|
34
|
+
SINGLE = ("data", "generate")
|
|
35
|
+
COLUMNS = ("features", "targets")
|
|
36
|
+
LEGO_KEYS = ("activation", "init", "schedule", "step", "strategy", "tokenizer", "when", "feed", "run", "log", "layers",
|
|
37
|
+
"each", "split", "transform", "train_transform")
|
|
38
|
+
SEEN = {}
|
|
39
|
+
CATEGORIES = (
|
|
40
|
+
("data", "data", "slot", ("data",)),
|
|
41
|
+
("splits", "data.split", "module", ("kalfa.std.data.splits",)),
|
|
42
|
+
("table preprocessing", "data.features and data.targets", "module",
|
|
43
|
+
("kalfa.std.data.pre", "sklearn.preprocessing")),
|
|
44
|
+
("feeds", "data.feed", "module", ("kalfa.std.data.window",)),
|
|
45
|
+
("image transforms", "data.transform and data.train_transform", "module", ("kalfa.std.data.transforms",)),
|
|
46
|
+
("tokenizers", "data.tokenizer", "module", ("kalfa.std.data.text.CharTokenizer",)),
|
|
47
|
+
("models", "models.<name>", "slot", ("model",)),
|
|
48
|
+
("layers", "layers of a stack model", "name",
|
|
49
|
+
("linear", "conv2d", "batch_norm", "layer_norm", "dropout", "embedding", "flatten", "last_step")),
|
|
50
|
+
("activations", "the activation of mlp or layers of stack", "name", ("relu", "gelu", "silu", "sigmoid", "tanh")),
|
|
51
|
+
("init", "models.<name>.init", "module", ("kalfa.std.init",)),
|
|
52
|
+
("losses", "losses.<name>", "slot", ("loss",)),
|
|
53
|
+
("objectives", "losses.<name>", "name",
|
|
54
|
+
("vae_elbo", "wgan_gp_d", "wgan_g", "ddpm", "distill", "ntxent", "mdmm", "weighted_sum")),
|
|
55
|
+
("noise schedules", "the schedule of ddpm", "name", ("cosine_beta", "linear_beta")),
|
|
56
|
+
("metrics", "metrics.<name>", "slot", ("metric",)),
|
|
57
|
+
("optimizers", "optimizers.<name>", "slot", ("optimizer",)),
|
|
58
|
+
("lr schedules", "optimizers.<name>.schedule", "slot", ("schedule",)),
|
|
59
|
+
("steps", "train.step", "slot", ("step",)),
|
|
60
|
+
("triggers", "rules.<name>.when", "slot", ("trigger",)),
|
|
61
|
+
("actions", "rules.<name>.run", "slot", ("action",)),
|
|
62
|
+
("samplers", "generate", "slot", ("sampler",)),
|
|
63
|
+
("plots", "plots.<name>", "slot", ("plot",)),
|
|
64
|
+
("loggers", "train.log", "slot", ("logger",)),
|
|
65
|
+
("sweep strategies", "sweep.strategy", "slot", ("strategy",)),
|
|
66
|
+
("other", None, "slot", ()),
|
|
67
|
+
)
|
|
68
|
+
OTHER = "other"
|
|
69
|
+
PLACES = {category: place for category, place, _, _ in CATEGORIES}
|
|
70
|
+
RULES = ("name", "module", "slot")
|
|
71
|
+
FIELD = 8
|
|
72
|
+
PLUGIN = "(plugin)"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_registrars():
|
|
76
|
+
for module in REGISTRARS:
|
|
77
|
+
importlib.import_module(module)
|
|
78
|
+
notes.apply()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def strategy_note(entry):
|
|
82
|
+
target = importlib.import_module(SWEEP_MODULE).STRATEGIES.get(entry.name)
|
|
83
|
+
if target is None or entry.slot == "strategy":
|
|
84
|
+
return None
|
|
85
|
+
path = aliases.target_path(target)
|
|
86
|
+
names = [other.name for other in aliases.catalog(slot="strategy") if aliases.target_path(other.target) == path]
|
|
87
|
+
return f"in a sweep section, {entry.name} is the {names[0]} strategy" if names else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def group_of(entry):
|
|
91
|
+
return entry.slot or UNKNOWN
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def page_entry(name):
|
|
95
|
+
load_registrars()
|
|
96
|
+
return aliases.entry_of(name) if isinstance(name, str) else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def entry_from(name_or_entry):
|
|
100
|
+
if isinstance(name_or_entry, aliases.Entry):
|
|
101
|
+
return name_or_entry
|
|
102
|
+
return page_entry(name_or_entry)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def target_in(entry, modules):
|
|
106
|
+
path = aliases.target_path(entry.target)
|
|
107
|
+
return any(path == module or path.startswith(module + ".") for module in modules)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def fits(entry, rule, members):
|
|
111
|
+
if rule == "name":
|
|
112
|
+
return entry.name in members
|
|
113
|
+
if rule == "module":
|
|
114
|
+
return target_in(entry, members)
|
|
115
|
+
return entry.slot in members
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def category_of(name_or_entry):
|
|
119
|
+
entry = entry_from(name_or_entry)
|
|
120
|
+
if entry is None:
|
|
121
|
+
return OTHER
|
|
122
|
+
for rule in ("slot",) if entry.plugin else RULES:
|
|
123
|
+
for category, _, kind, members in CATEGORIES:
|
|
124
|
+
if kind == rule and fits(entry, rule, members):
|
|
125
|
+
return category
|
|
126
|
+
return OTHER
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def place_of(name_or_entry):
|
|
130
|
+
return PLACES.get(category_of(name_or_entry))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def filters(word, slot):
|
|
134
|
+
return (word if isinstance(word, str) and word else None), (str(slot).lower() if slot else None)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def categories(word=None, slot=None):
|
|
138
|
+
load_registrars()
|
|
139
|
+
word, slot = filters(word, slot)
|
|
140
|
+
groups = {}
|
|
141
|
+
for entry in aliases.catalog(word=word):
|
|
142
|
+
if slot is None or group_of(entry) == slot:
|
|
143
|
+
groups.setdefault(category_of(entry), []).append(entry)
|
|
144
|
+
return [(category, groups[category]) for category, *_ in CATEGORIES if category in groups]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def slot_rank(slot):
|
|
148
|
+
if slot in SLOT_ORDER:
|
|
149
|
+
return (0, SLOT_ORDER.index(slot), slot)
|
|
150
|
+
if slot == UNKNOWN:
|
|
151
|
+
return (2, 0, slot)
|
|
152
|
+
return (1, 0, slot)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def flat(text):
|
|
156
|
+
return " ".join(str(text or "").split())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def wrapped(text, lead, rest=None):
|
|
160
|
+
body = flat(text)
|
|
161
|
+
if not body:
|
|
162
|
+
return [lead.rstrip()]
|
|
163
|
+
rest = " " * len(lead) if rest is None else rest
|
|
164
|
+
width = max(WIDTH, len(lead) + 40)
|
|
165
|
+
return textwrap.wrap(body, width=width, initial_indent=lead, subsequent_indent=rest, break_long_words=False,
|
|
166
|
+
break_on_hyphens=False)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def shown(value):
|
|
170
|
+
if inspect.isclass(value) or inspect.isroutine(value) or inspect.ismodule(value):
|
|
171
|
+
return aliases.target_path(value)
|
|
172
|
+
text = repr(value)
|
|
173
|
+
if " at 0x" in text:
|
|
174
|
+
return f"<{aliases.target_path(type(value))}>"
|
|
175
|
+
return text
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def parameters(entry):
|
|
179
|
+
target = aliases.import_target(entry.target)
|
|
180
|
+
signature = node_signature(target, entry.slot or "value")
|
|
181
|
+
empty = inspect.Parameter.empty
|
|
182
|
+
return [(name, parameter.default, parameter.default is empty) for name, parameter in signature.params.items()]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def signature_of(entry):
|
|
186
|
+
if isinstance(entry, str):
|
|
187
|
+
load_registrars()
|
|
188
|
+
found = aliases.entry_of(entry)
|
|
189
|
+
if found is None:
|
|
190
|
+
raise KalfaError(f"no name {entry}")
|
|
191
|
+
entry = found
|
|
192
|
+
return [(name, REQUIRED if required else default) for name, default, required in parameters(entry)]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def is_name(text):
|
|
196
|
+
load_registrars()
|
|
197
|
+
return isinstance(text, str) and text in aliases.ALIASES
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def present_slots():
|
|
201
|
+
return sorted({group_of(entry) for entry in aliases.ALIASES.values()}, key=slot_rank)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def empty_text(word, slot):
|
|
205
|
+
if word is None:
|
|
206
|
+
if slot is None:
|
|
207
|
+
return "no names are registered"
|
|
208
|
+
return f"no name of slot {slot}; the slots are " + ", ".join(present_slots())
|
|
209
|
+
lines = [f"no name matches {word}" + (f" in slot {slot}" if slot else "")]
|
|
210
|
+
hint = aliases.suggestion(word)
|
|
211
|
+
if hint:
|
|
212
|
+
lines.append(hint)
|
|
213
|
+
return "\n".join(lines)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def listed_doc(entry):
|
|
217
|
+
return f"{PLUGIN} {flat(entry.doc)}".rstrip() if entry.plugin else flat(entry.doc)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def catalog_text(word=None, slot=None):
|
|
221
|
+
word, slot = filters(word, slot)
|
|
222
|
+
found = categories(word, slot)
|
|
223
|
+
if not found:
|
|
224
|
+
return empty_text(word, slot)
|
|
225
|
+
lines = []
|
|
226
|
+
for category, members in found:
|
|
227
|
+
if lines:
|
|
228
|
+
lines.append("")
|
|
229
|
+
lines.append(category)
|
|
230
|
+
width = max(len(entry.name) for entry in members)
|
|
231
|
+
for entry in members:
|
|
232
|
+
lines.extend(wrapped(listed_doc(entry), " " + entry.name.ljust(width) + " "))
|
|
233
|
+
return "\n".join(lines)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def strings(value):
|
|
237
|
+
if isinstance(value, str):
|
|
238
|
+
return [value]
|
|
239
|
+
if isinstance(value, list):
|
|
240
|
+
return [item for item in value if isinstance(item, str)]
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def nested_words(value):
|
|
245
|
+
if isinstance(value, list):
|
|
246
|
+
return [word for item in value for word in nested_words(item)]
|
|
247
|
+
if not isinstance(value, dict):
|
|
248
|
+
return []
|
|
249
|
+
found = []
|
|
250
|
+
for key, item in value.items():
|
|
251
|
+
if key == "use" or key in LEGO_KEYS:
|
|
252
|
+
found.extend(strings(item))
|
|
253
|
+
found.extend(nested_words(item))
|
|
254
|
+
return found
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def section_words(tree):
|
|
258
|
+
found = []
|
|
259
|
+
for section in SECTIONS:
|
|
260
|
+
entries = tree.get(section)
|
|
261
|
+
if isinstance(entries, dict):
|
|
262
|
+
found.extend(value for value in entries.values() if isinstance(value, str))
|
|
263
|
+
found.extend(tree[section] for section in SINGLE if isinstance(tree.get(section), str))
|
|
264
|
+
data = tree.get("data")
|
|
265
|
+
if isinstance(data, dict):
|
|
266
|
+
for key in COLUMNS:
|
|
267
|
+
if isinstance(data.get(key), dict):
|
|
268
|
+
found.extend(word for value in data[key].values() for word in strings(value))
|
|
269
|
+
return found
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def lego_words(tree):
|
|
273
|
+
if not isinstance(tree, dict):
|
|
274
|
+
return []
|
|
275
|
+
return section_words(tree) + nested_words(tree)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def loaded(path):
|
|
279
|
+
try:
|
|
280
|
+
return YAML(typ="safe", pure=True).load(path.read_text(encoding="utf-8"))
|
|
281
|
+
except (OSError, UnicodeDecodeError, YAMLError):
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def words_in(path):
|
|
286
|
+
try:
|
|
287
|
+
stamp = path.stat().st_mtime_ns
|
|
288
|
+
except OSError:
|
|
289
|
+
return frozenset()
|
|
290
|
+
found = SEEN.get(path)
|
|
291
|
+
if found is None or found[0] != stamp:
|
|
292
|
+
found = (stamp, frozenset(lego_words(loaded(path))))
|
|
293
|
+
SEEN[path] = found
|
|
294
|
+
return found[1]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def example_files():
|
|
298
|
+
if ROOT is None or not (ROOT / "examples").is_dir():
|
|
299
|
+
return []
|
|
300
|
+
return list(ROOT.glob(EXAMPLES))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def used_in(name):
|
|
304
|
+
entry = aliases.entry_of(name) if isinstance(name, str) else None
|
|
305
|
+
spellings = {name} if entry is None else {name, aliases.target_path(entry.target)}
|
|
306
|
+
return sorted(path.relative_to(ROOT).as_posix() for path in example_files() if spellings & set(words_in(path)))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def example_source(example):
|
|
310
|
+
text = example if isinstance(example, str) else json.dumps(example, default=str)
|
|
311
|
+
return text.strip("\n")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def example_lines(source):
|
|
315
|
+
return [" " + line if line.strip() else "" for line in source.splitlines()]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def fields_of(entry):
|
|
319
|
+
category = category_of(entry)
|
|
320
|
+
rows = [("field", "target", aliases.target_path(entry.target)), ("field", "slot", group_of(entry)),
|
|
321
|
+
("field", "category", category)]
|
|
322
|
+
if PLACES.get(category):
|
|
323
|
+
rows.append(("field", "goes in", PLACES[category]))
|
|
324
|
+
if entry.plugin:
|
|
325
|
+
rows.append(("field", "from", "a plugin"))
|
|
326
|
+
note = strategy_note(entry)
|
|
327
|
+
if note:
|
|
328
|
+
rows.append(("field", "sweep", note))
|
|
329
|
+
return rows
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def param_rows(entry):
|
|
333
|
+
try:
|
|
334
|
+
found = parameters(entry)
|
|
335
|
+
except BuildError as exc:
|
|
336
|
+
return [("problem", f"not readable: {exc}")]
|
|
337
|
+
rows = []
|
|
338
|
+
for param, default, required in found:
|
|
339
|
+
rows.append(("param", param, None if required else shown(default)))
|
|
340
|
+
if flat(entry.params.get(param)):
|
|
341
|
+
rows.append(("note", flat(entry.params[param])))
|
|
342
|
+
return rows or [("problem", "none")]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def page_of(entry):
|
|
346
|
+
rows = [("title", entry.name), *fields_of(entry)]
|
|
347
|
+
if flat(entry.doc):
|
|
348
|
+
rows += [("blank",), ("doc", flat(entry.doc))]
|
|
349
|
+
rows += [("blank",), ("heading", "params:"), *param_rows(entry)]
|
|
350
|
+
if entry.examples:
|
|
351
|
+
rows += [("blank",), ("heading", "examples:")]
|
|
352
|
+
for position, example in enumerate(entry.examples):
|
|
353
|
+
if position:
|
|
354
|
+
rows.append(("blank",))
|
|
355
|
+
rows.append(("example", example_source(example)))
|
|
356
|
+
used = used_in(entry.name)
|
|
357
|
+
if used:
|
|
358
|
+
rows += [("blank",), ("heading", "used in:"), *[("path", path) for path in used]]
|
|
359
|
+
return rows
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def param_line(row):
|
|
363
|
+
return f" {row[1]} (required)" if row[2] is None else f" {row[1]} = {row[2]}"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
TEXT = {
|
|
367
|
+
"title": lambda row: [row[1]],
|
|
368
|
+
"field": lambda row: [f" {row[1].ljust(FIELD)} {row[2]}"],
|
|
369
|
+
"doc": lambda row: wrapped(row[1], " "),
|
|
370
|
+
"heading": lambda row: [row[1]],
|
|
371
|
+
"param": lambda row: [param_line(row)],
|
|
372
|
+
"note": lambda row: wrapped(row[1], NOTE_INDENT),
|
|
373
|
+
"example": lambda row: example_lines(row[1]),
|
|
374
|
+
"path": lambda row: [" " + row[1]],
|
|
375
|
+
"problem": lambda row: [" " + row[1]],
|
|
376
|
+
"blank": lambda row: [""],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def indented(renderable, width):
|
|
381
|
+
return Padding(renderable, (0, 0, 0, width))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def rich_param(row):
|
|
385
|
+
name = (" " + row[1], "bold green")
|
|
386
|
+
if row[2] is None:
|
|
387
|
+
return Text.assemble(name, (" (required)", "red"))
|
|
388
|
+
return Text.assemble(name, (" = ", "dim"), (row[2], "cyan"))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def rich_example(row):
|
|
392
|
+
return indented(Syntax(row[1], "yaml", theme=THEME, background_color="default", word_wrap=True), 2)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
RICH = {
|
|
396
|
+
"title": lambda row: Text(row[1], style="bold cyan"),
|
|
397
|
+
"field": lambda row: Text.assemble((f" {row[1].ljust(FIELD)} ", "dim"), row[2]),
|
|
398
|
+
"doc": lambda row: indented(Text(row[1]), 2),
|
|
399
|
+
"heading": lambda row: Text(row[1], style="bold yellow"),
|
|
400
|
+
"param": rich_param,
|
|
401
|
+
"note": lambda row: indented(Text(row[1]), len(NOTE_INDENT)),
|
|
402
|
+
"example": rich_example,
|
|
403
|
+
"path": lambda row: Text(" " + row[1], style="blue"),
|
|
404
|
+
"problem": lambda row: Text(" " + row[1], style="yellow"),
|
|
405
|
+
"blank": lambda row: Text(""),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def missing_text(name):
|
|
410
|
+
lines = [f"no name {name}"]
|
|
411
|
+
hint = aliases.suggestion(name)
|
|
412
|
+
if hint:
|
|
413
|
+
lines.append(hint)
|
|
414
|
+
return "\n".join(lines)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def entry_text(name):
|
|
418
|
+
entry = page_entry(name)
|
|
419
|
+
if entry is None:
|
|
420
|
+
return missing_text(name)
|
|
421
|
+
return "\n".join(line for row in page_of(entry) for line in TEXT[row[0]](row))
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def entry_renderable(name):
|
|
425
|
+
entry = page_entry(name)
|
|
426
|
+
if entry is None:
|
|
427
|
+
return Text(missing_text(name))
|
|
428
|
+
return Group(*[RICH[row[0]](row) for row in page_of(entry)])
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def entry_label(entry):
|
|
432
|
+
parts = [(entry.name, "bold")]
|
|
433
|
+
if entry.plugin:
|
|
434
|
+
parts.append((" " + PLUGIN, "magenta"))
|
|
435
|
+
if flat(entry.doc):
|
|
436
|
+
parts.append((" " + flat(entry.doc), "dim"))
|
|
437
|
+
return Text.assemble(*parts)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def category_renderable(category, entries):
|
|
441
|
+
rows = [RICH["title"](("title", category))]
|
|
442
|
+
if PLACES.get(category):
|
|
443
|
+
rows.append(RICH["field"](("field", "goes in", PLACES[category])))
|
|
444
|
+
grid = Table.grid(padding=(0, 2))
|
|
445
|
+
grid.add_column(style="bold green", no_wrap=True)
|
|
446
|
+
grid.add_column()
|
|
447
|
+
for entry in entries:
|
|
448
|
+
grid.add_row(Text(entry.name), Text(listed_doc(entry)))
|
|
449
|
+
return Group(*rows, Text(""), indented(grid, 2))
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def prose(text):
|
|
453
|
+
return flat(text).replace("<", "\\<").replace("*", "\\*")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def markdown_param(entry, param, default, required):
|
|
457
|
+
head = f"`{param}` (required)" if required else f"`{param}={shown(default)}`"
|
|
458
|
+
note = prose(entry.params.get(param))
|
|
459
|
+
return f"* {head}: {note}" if note else f"* {head}"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def markdown_params(entry):
|
|
463
|
+
try:
|
|
464
|
+
found = parameters(entry)
|
|
465
|
+
except BuildError as exc:
|
|
466
|
+
return [f"Params: not readable: {prose(exc)}"]
|
|
467
|
+
if not found:
|
|
468
|
+
return ["Params: none."]
|
|
469
|
+
return ["Params:", "", *[markdown_param(entry, *item) for item in found]]
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def markdown_examples(entry):
|
|
473
|
+
lines = ["Examples:" if len(entry.examples) > 1 else "Example:"]
|
|
474
|
+
for example in entry.examples:
|
|
475
|
+
lines += ["", "```yaml", example_source(example), "```"]
|
|
476
|
+
return lines
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def markdown_entry(entry):
|
|
480
|
+
origin = ", from a plugin" if entry.plugin else ""
|
|
481
|
+
lines = [f"### {entry.name}", "", f"Target `{aliases.target_path(entry.target)}`{origin}."]
|
|
482
|
+
note = strategy_note(entry)
|
|
483
|
+
if note:
|
|
484
|
+
lines += ["", note[0].upper() + note[1:] + "."]
|
|
485
|
+
if flat(entry.doc):
|
|
486
|
+
lines += ["", prose(entry.doc)]
|
|
487
|
+
lines += ["", *markdown_params(entry)]
|
|
488
|
+
if entry.examples:
|
|
489
|
+
lines += ["", *markdown_examples(entry)]
|
|
490
|
+
used = used_in(entry.name)
|
|
491
|
+
if used:
|
|
492
|
+
lines += ["", "Used in " + ", ".join(f"`{path}`" for path in used) + "."]
|
|
493
|
+
return lines
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def markdown_place(place):
|
|
497
|
+
return " ".join(f"`{word}`" if "." in word or " " not in place else word for word in place.split())
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def docs_markdown():
|
|
501
|
+
lines = ["# kalfa legos"]
|
|
502
|
+
for category, members in categories():
|
|
503
|
+
lines += ["", f"## {category}"]
|
|
504
|
+
if PLACES.get(category):
|
|
505
|
+
lines += ["", f"Goes in {markdown_place(PLACES[category])}."]
|
|
506
|
+
for entry in members:
|
|
507
|
+
lines += ["", *markdown_entry(entry)]
|
|
508
|
+
return "\n".join(lines).rstrip()
|
kalfa/commands/plots.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from ..core import run as running
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def signatures(directory):
|
|
7
|
+
if not directory.is_dir():
|
|
8
|
+
return {}
|
|
9
|
+
found = {}
|
|
10
|
+
for path in directory.rglob("*"):
|
|
11
|
+
if path.is_file() and not path.name.startswith("."):
|
|
12
|
+
stat = path.stat()
|
|
13
|
+
found[path] = (stat.st_ino, stat.st_mtime_ns, stat.st_size)
|
|
14
|
+
return found
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def draw(record, only=None, which=None, device=None):
|
|
18
|
+
ctx = running.open_record(record, which=which, device=device)
|
|
19
|
+
return running.draw_plots(ctx, record, only)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def redraw(record, only=None, which=None, device=None):
|
|
23
|
+
directory = Path(record) / "plots"
|
|
24
|
+
before = signatures(directory)
|
|
25
|
+
draw(record, only, which, device)
|
|
26
|
+
after = signatures(directory)
|
|
27
|
+
return [str(path) for path in sorted(after) if before.get(path) != after[path]]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from kalfa import summary as summarizing
|
|
5
|
+
from kalfa.commands.report.page import document
|
|
6
|
+
from kalfa.commands.report.sections import (
|
|
7
|
+
DETAILS,
|
|
8
|
+
chosen_records,
|
|
9
|
+
curves_section,
|
|
10
|
+
details_section,
|
|
11
|
+
folds_section,
|
|
12
|
+
heading,
|
|
13
|
+
ranked,
|
|
14
|
+
summary_section,
|
|
15
|
+
sweep_section,
|
|
16
|
+
title_of,
|
|
17
|
+
)
|
|
18
|
+
from kalfa.core.errors import KalfaError
|
|
19
|
+
from kalfa.core.record import atomic
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
REPORT = "report.html"
|
|
23
|
+
SUFFIXES = (".html", ".htm")
|
|
24
|
+
__all__ = ["DETAILS", "REPORT", "SUFFIXES", "render", "target_of", "write_report"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render(summary, points=None, curves=None, stats=True):
|
|
28
|
+
records = ranked(summary)
|
|
29
|
+
chosen, rest = chosen_records(summary, records, points)
|
|
30
|
+
title = title_of(summary)
|
|
31
|
+
sections = [("summary", "Summary"), ("curves", "Curves")]
|
|
32
|
+
if summary.folds:
|
|
33
|
+
sections.append(("folds", "Folds"))
|
|
34
|
+
if summary.sweep is not None:
|
|
35
|
+
sections.append(("sweep", "Sweep"))
|
|
36
|
+
sections.append(("records", "Records"))
|
|
37
|
+
body = [heading(summary, title, sections), summary_section(summary), curves_section(summary, records, curves)]
|
|
38
|
+
if summary.folds:
|
|
39
|
+
body.append(folds_section(summary))
|
|
40
|
+
if summary.sweep is not None:
|
|
41
|
+
body.append(sweep_section(summary))
|
|
42
|
+
body.append(details_section(chosen, rest, stats))
|
|
43
|
+
return document(title, "\n".join(body))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def target_of(paths, out):
|
|
47
|
+
if out is not None:
|
|
48
|
+
target = Path(out)
|
|
49
|
+
if target.is_dir() or target.suffix.lower() not in SUFFIXES:
|
|
50
|
+
return target / REPORT
|
|
51
|
+
return target
|
|
52
|
+
return Path(paths[0]) / REPORT
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def write_report(paths, out=None, by=None, mode=None, top=None, points=None):
|
|
56
|
+
paths = [paths] if isinstance(paths, (str, os.PathLike)) else list(paths)
|
|
57
|
+
if not paths:
|
|
58
|
+
raise KalfaError("a report needs at least one record or sweep root")
|
|
59
|
+
found = summarizing.summarize(paths, by=by, mode=mode, top=top)
|
|
60
|
+
text = render(found, points=points)
|
|
61
|
+
target = target_of(paths, out)
|
|
62
|
+
with atomic(target) as temporary:
|
|
63
|
+
temporary.write_text(text, encoding="utf-8")
|
|
64
|
+
return target
|