kalfa 3.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kalfa/__init__.py +19 -0
- kalfa/api.py +284 -0
- kalfa/board/__init__.py +1 -0
- kalfa/board/answers.py +419 -0
- kalfa/board/server.py +536 -0
- kalfa/board/static/api.js +176 -0
- kalfa/board/static/app.js +122 -0
- kalfa/board/static/board.css +228 -0
- kalfa/board/static/charts.css +46 -0
- kalfa/board/static/components/badge.js +14 -0
- kalfa/board/static/components/chart.js +204 -0
- kalfa/board/static/components/diagram.js +857 -0
- kalfa/board/static/components/error.js +17 -0
- kalfa/board/static/components/loading.js +7 -0
- kalfa/board/static/components/progress.js +25 -0
- kalfa/board/static/components/settings.js +418 -0
- kalfa/board/static/components/spark.js +43 -0
- kalfa/board/static/components/table.js +156 -0
- kalfa/board/static/diagram.css +76 -0
- kalfa/board/static/format.js +138 -0
- kalfa/board/static/index.html +34 -0
- kalfa/board/static/plot.js +594 -0
- kalfa/board/static/predictions.css +20 -0
- kalfa/board/static/store.js +215 -0
- kalfa/board/static/views/compare.js +185 -0
- kalfa/board/static/views/home.js +90 -0
- kalfa/board/static/views/record/config.js +81 -0
- kalfa/board/static/views/record/curves.js +137 -0
- kalfa/board/static/views/record/data.js +378 -0
- kalfa/board/static/views/record/files.js +128 -0
- kalfa/board/static/views/record/log.js +164 -0
- kalfa/board/static/views/record/model.js +286 -0
- kalfa/board/static/views/record/overview.js +216 -0
- kalfa/board/static/views/record/plots.js +103 -0
- kalfa/board/static/views/record/predictions.js +834 -0
- kalfa/board/static/views/record.js +179 -0
- kalfa/board/static/views/runs.js +124 -0
- kalfa/board/static/views/sweep.js +282 -0
- kalfa/board/watch.py +87 -0
- kalfa/cli.py +371 -0
- kalfa/commands/__init__.py +0 -0
- kalfa/commands/common.py +8 -0
- kalfa/commands/export.py +117 -0
- kalfa/commands/generate.py +31 -0
- kalfa/commands/guide/__init__.py +101 -0
- kalfa/commands/guide/about.py +93 -0
- kalfa/commands/guide/calls.py +102 -0
- kalfa/commands/guide/sections.py +186 -0
- kalfa/commands/guide/seen.py +307 -0
- kalfa/commands/guide/show.py +106 -0
- kalfa/commands/guide/training.py +333 -0
- kalfa/commands/guide/words.py +97 -0
- kalfa/commands/legos.py +508 -0
- kalfa/commands/plots.py +27 -0
- kalfa/commands/report/__init__.py +64 -0
- kalfa/commands/report/page.py +112 -0
- kalfa/commands/report/predictions.py +216 -0
- kalfa/commands/report/sections.py +356 -0
- kalfa/commands/stop.py +60 -0
- kalfa/commands/sweep/__init__.py +202 -0
- kalfa/commands/sweep/checks.py +98 -0
- kalfa/commands/sweep/plan.py +183 -0
- kalfa/commands/sweep/points.py +123 -0
- kalfa/commands/sweep/runner.py +100 -0
- kalfa/commands/sweep/space.py +67 -0
- kalfa/commands/sweep/strategies.py +228 -0
- kalfa/core/__init__.py +0 -0
- kalfa/core/aliases.py +229 -0
- kalfa/core/build.py +727 -0
- kalfa/core/check.py +550 -0
- kalfa/core/coerce.py +36 -0
- kalfa/core/context.py +149 -0
- kalfa/core/device.py +63 -0
- kalfa/core/errors.py +45 -0
- kalfa/core/graph.py +304 -0
- kalfa/core/history.py +70 -0
- kalfa/core/loop.py +710 -0
- kalfa/core/make.py +121 -0
- kalfa/core/plan.py +156 -0
- kalfa/core/predict.py +290 -0
- kalfa/core/protocols.py +122 -0
- kalfa/core/record.py +347 -0
- kalfa/core/refs.py +88 -0
- kalfa/core/rng.py +100 -0
- kalfa/core/rules.py +513 -0
- kalfa/core/run.py +568 -0
- kalfa/core/spec.py +413 -0
- kalfa/core/state.py +100 -0
- kalfa/core/wrap.py +671 -0
- kalfa/records/__init__.py +24 -0
- kalfa/records/compare.py +86 -0
- kalfa/records/reader.py +561 -0
- kalfa/records/stats/__init__.py +36 -0
- kalfa/records/stats/choices.py +282 -0
- kalfa/records/stats/classes.py +236 -0
- kalfa/records/stats/measures.py +144 -0
- kalfa/records/stats/pairs.py +225 -0
- kalfa/records/stats/tables.py +153 -0
- kalfa/records/sweep.py +244 -0
- kalfa/std/__init__.py +17 -0
- kalfa/std/actions.py +35 -0
- kalfa/std/common.py +56 -0
- kalfa/std/data/__init__.py +1 -0
- kalfa/std/data/base.py +45 -0
- kalfa/std/data/columns.py +190 -0
- kalfa/std/data/flow.py +317 -0
- kalfa/std/data/images.py +443 -0
- kalfa/std/data/options.py +147 -0
- kalfa/std/data/pre.py +84 -0
- kalfa/std/data/splits.py +140 -0
- kalfa/std/data/stream.py +294 -0
- kalfa/std/data/table.py +642 -0
- kalfa/std/data/text.py +436 -0
- kalfa/std/data/transforms.py +327 -0
- kalfa/std/data/weights.py +113 -0
- kalfa/std/data/window.py +135 -0
- kalfa/std/figure.py +37 -0
- kalfa/std/init.py +72 -0
- kalfa/std/loggers.py +105 -0
- kalfa/std/losses.py +161 -0
- kalfa/std/metrics.py +463 -0
- kalfa/std/models.py +122 -0
- kalfa/std/notes.py +1553 -0
- kalfa/std/objectives.py +360 -0
- kalfa/std/plots/__init__.py +75 -0
- kalfa/std/plots/classification.py +207 -0
- kalfa/std/plots/common.py +44 -0
- kalfa/std/plots/curves.py +56 -0
- kalfa/std/plots/images.py +188 -0
- kalfa/std/plots/pairs.py +66 -0
- kalfa/std/plots/predictions.py +154 -0
- kalfa/std/samplers.py +261 -0
- kalfa/std/samples.py +72 -0
- kalfa/std/schedules.py +74 -0
- kalfa/std/steps.py +133 -0
- kalfa/std/triggers.py +135 -0
- kalfa/summary.py +296 -0
- kalfa/tui/__init__.py +4 -0
- kalfa/tui/app.py +107 -0
- kalfa/tui/config.py +61 -0
- kalfa/tui/guide.py +100 -0
- kalfa/tui/jobs.py +97 -0
- kalfa/tui/legos.py +62 -0
- kalfa/tui/records.py +439 -0
- kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
- kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
- kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
- kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from kalfa.commands.guide.calls import call_notes, lego_note, legos_in, param_lines, use_of
|
|
5
|
+
from kalfa.commands.guide.seen import default_of, patch_notes, patch_text, range_text, ref_meaning, ref_name
|
|
6
|
+
from kalfa.commands.guide.words import and_list, short, unique, value_text
|
|
7
|
+
from kalfa.core import aliases
|
|
8
|
+
from kalfa.core.build import SLOT_KEYS, criterion_function
|
|
9
|
+
from kalfa.core.check import is_criterion
|
|
10
|
+
from kalfa.core.protocols import Objective
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TRIGGERS = ("after_epoch", "every", "plateau", "above", "below", "time_budget")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def criterion_notes(keys):
|
|
17
|
+
notes = []
|
|
18
|
+
if keys.get("pred") is not None:
|
|
19
|
+
notes.append(f"pred: compares the output {value_text(keys['pred'])}")
|
|
20
|
+
if keys.get("target") is not None:
|
|
21
|
+
notes.append(f"target: against the batch field {value_text(keys['target'])}")
|
|
22
|
+
on = keys.get("on")
|
|
23
|
+
if on is not None:
|
|
24
|
+
listed = on if isinstance(on, list) else [on]
|
|
25
|
+
notes.append(f"on: runs on the {and_list(listed)} {'set' if len(listed) == 1 else 'sets'} only")
|
|
26
|
+
if keys.get("every") is not None:
|
|
27
|
+
notes.append(f"every: computed every {value_text(keys['every'])} turns")
|
|
28
|
+
if keys.get("rescale") is True:
|
|
29
|
+
notes.append("rescale: compares in the original units, after undoing the preprocessor of the target")
|
|
30
|
+
elif keys.get("rescale") is False:
|
|
31
|
+
notes.append("rescale: false compares the preprocessed values")
|
|
32
|
+
if keys.get("sample_weight") is not None:
|
|
33
|
+
notes.append(f"sample_weight: the loss of every row is multiplied by the batch field "
|
|
34
|
+
f"{value_text(keys['sample_weight'])} before it is averaged")
|
|
35
|
+
return notes
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_notes(seen, path, keys, metric):
|
|
39
|
+
node = seen.node(path)
|
|
40
|
+
if node is None or node.target is None or not is_criterion(node):
|
|
41
|
+
return []
|
|
42
|
+
targets = seen.targets()
|
|
43
|
+
target = keys.get("target")
|
|
44
|
+
if target is None and targets is not None and len(targets) == 1:
|
|
45
|
+
target = targets[0]
|
|
46
|
+
notes = []
|
|
47
|
+
missing = [key for key in ("pred", "target") if keys.get(key) is None]
|
|
48
|
+
model = seen.predicts()
|
|
49
|
+
pred_text = None
|
|
50
|
+
if model is not None:
|
|
51
|
+
output = seen.first_output(model)
|
|
52
|
+
pred_text = (f"the output of {model} (the predicts model)" if output == model
|
|
53
|
+
else f"{output} (the first output of the predicts model {model})")
|
|
54
|
+
target_text = f"the target {target}" if target is not None and keys.get("target") is None else None
|
|
55
|
+
if targets is None and keys.get("target") is None:
|
|
56
|
+
target_text = "the only target of the data"
|
|
57
|
+
if missing == ["pred", "target"] and pred_text and target_text:
|
|
58
|
+
notes.append(f"pred and target are not written, so kalfa compares {pred_text} with {target_text}")
|
|
59
|
+
elif "pred" in missing and pred_text:
|
|
60
|
+
notes.append(f"pred is not written, so kalfa takes {pred_text}")
|
|
61
|
+
elif "target" in missing and target_text:
|
|
62
|
+
notes.append(f"target is not written, so kalfa takes {target_text}")
|
|
63
|
+
if metric and keys.get("rescale") is None and isinstance(target, str):
|
|
64
|
+
notes += rescale_notes(seen, target, node.name)
|
|
65
|
+
return notes
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def inverts(name):
|
|
69
|
+
entry = aliases.entry_of(name)
|
|
70
|
+
try:
|
|
71
|
+
target = aliases.import_target(entry.target if entry is not None else name)
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
if not inspect.isclass(target):
|
|
75
|
+
return None
|
|
76
|
+
return hasattr(target, "inverse_transform") and bool(getattr(target, "rescales", True))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rescale_notes(seen, target, name):
|
|
80
|
+
targets = seen.mapping("data", "targets")
|
|
81
|
+
if target not in targets:
|
|
82
|
+
return []
|
|
83
|
+
chain = targets[target]
|
|
84
|
+
names = [use_of(item) for item in (chain if isinstance(chain, list) else [chain]) if item is not None]
|
|
85
|
+
if not names:
|
|
86
|
+
return [f"rescale stays off: {target} is kept raw"]
|
|
87
|
+
if "label" in names:
|
|
88
|
+
return []
|
|
89
|
+
names = [item for item in names if item]
|
|
90
|
+
answers = [inverts(item) for item in names]
|
|
91
|
+
able = [item for item, answer in zip(names, answers) if answer]
|
|
92
|
+
if able:
|
|
93
|
+
return [f"rescale is on because {and_list(able)} can be inverted, so {name} speaks the original units of "
|
|
94
|
+
f"{target}"]
|
|
95
|
+
if answers and all(answer is False for answer in answers):
|
|
96
|
+
return [f"rescale stays off: {and_list(names)} cannot be inverted"]
|
|
97
|
+
return [f"rescale is on when a preprocessor of {target} can be inverted, so {name} speaks its original units"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def kind_notes(seen, path, use):
|
|
101
|
+
target = seen.target(path)
|
|
102
|
+
if target is None or not use or aliases.entry_of(use) is not None:
|
|
103
|
+
return []
|
|
104
|
+
if inspect.isclass(target) and issubclass(target, Objective):
|
|
105
|
+
return ["an objective: it reads the outputs, the batch and the run context itself"]
|
|
106
|
+
if inspect.isroutine(target):
|
|
107
|
+
if criterion_function(target):
|
|
108
|
+
return ["a function of (pred, target), so it is the criterion itself and the other params are bound to it"]
|
|
109
|
+
return ["a function without two required params: kalfa calls it and uses what it returns"]
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def loss_notes(seen, key, value, keys, path):
|
|
114
|
+
name = str(key)
|
|
115
|
+
resolved = seen.mapping(*keys)
|
|
116
|
+
notes = call_notes(seen, path, value, SLOT_KEYS["loss"]) + kind_notes(seen, path, use_of(value))
|
|
117
|
+
notes += criterion_notes(resolved) + default_notes(seen, path, resolved, False)
|
|
118
|
+
minimizers = seen.minimizers(name)
|
|
119
|
+
if minimizers:
|
|
120
|
+
notes.append(f"{and_list(minimizers)} {'minimizes' if len(minimizers) == 1 else 'minimize'} it, so it gives "
|
|
121
|
+
f"the gradients")
|
|
122
|
+
elif any(reader.startswith("losses.") for reader in seen.readers(path)):
|
|
123
|
+
notes.append("no optimizer minimizes it directly")
|
|
124
|
+
elif seen.mapping("optimizers"):
|
|
125
|
+
notes.append("no optimizer minimizes it at the start, so it only observes")
|
|
126
|
+
for rule, written, item in seen.patches():
|
|
127
|
+
if written.endswith(".minimizes") and ref_name(item, "losses") == name:
|
|
128
|
+
notes.append(f"rule {rule} makes {written.split('.')[1]} minimize it")
|
|
129
|
+
return notes
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def metric_notes(seen, key, value, keys, path):
|
|
133
|
+
resolved = seen.mapping(*keys)
|
|
134
|
+
notes = call_notes(seen, path, value, SLOT_KEYS["metric"])
|
|
135
|
+
return notes + criterion_notes(resolved) + default_notes(seen, path, resolved, True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def over_note(over):
|
|
139
|
+
items = over if isinstance(over, list) else [over]
|
|
140
|
+
plain_models = [ref_name(item, "models") for item in items]
|
|
141
|
+
if len(items) > 1 and all(plain_models) and all(item == f"@models.{name}" for item, name in
|
|
142
|
+
zip(items, plain_models)):
|
|
143
|
+
return f"over: steps the parameters of the models {and_list(plain_models)}"
|
|
144
|
+
parts = []
|
|
145
|
+
for item in items:
|
|
146
|
+
if isinstance(item, dict):
|
|
147
|
+
own = ", ".join(f"{key} {value_text(value)}" for key, value in item.items() if key not in ("of", "match"))
|
|
148
|
+
text = f"the parameters of {ref_meaning(item.get('of'))}"
|
|
149
|
+
text += f" matching {item['match']}" if item.get("match") else ""
|
|
150
|
+
parts.append(text + (f" ({own})" if own else ""))
|
|
151
|
+
elif isinstance(item, str) and item.startswith("@"):
|
|
152
|
+
parts.append(f"the parameters of {ref_meaning(item)}")
|
|
153
|
+
else:
|
|
154
|
+
parts.append(value_text(item))
|
|
155
|
+
return f"over: steps {and_list(parts)}"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def optimizer_notes(seen, key, value, keys, path):
|
|
159
|
+
resolved = seen.mapping(*keys)
|
|
160
|
+
notes = call_notes(seen, path, value, SLOT_KEYS["optimizer"])
|
|
161
|
+
if "over" in resolved:
|
|
162
|
+
notes.append(over_note(resolved["over"]))
|
|
163
|
+
loss = ref_name(resolved.get("minimizes"), "losses")
|
|
164
|
+
if loss:
|
|
165
|
+
notes.append(f"minimizes: steps on the loss {loss}")
|
|
166
|
+
schedule = use_of(resolved.get("schedule"))
|
|
167
|
+
if schedule:
|
|
168
|
+
notes.append(f"schedule: multiplies the lr by {schedule} after every update")
|
|
169
|
+
notes.append(lego_note(schedule))
|
|
170
|
+
notes += param_lines(schedule, resolved.get("schedule"), lead="schedule.")
|
|
171
|
+
return notes + patch_notes(seen, path)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def checkpoint_notes(checkpoint):
|
|
175
|
+
if not isinstance(checkpoint, dict):
|
|
176
|
+
return []
|
|
177
|
+
notes = []
|
|
178
|
+
if checkpoint.get("watch"):
|
|
179
|
+
better = "higher" if checkpoint.get("mode") == "max" else "lower"
|
|
180
|
+
notes.append(f"keeps best.pt whenever {checkpoint['watch']} improves ({better} is better)")
|
|
181
|
+
text = "writes no last.pt" if checkpoint.get("last") is False else "writes last.pt every turn"
|
|
182
|
+
if checkpoint.get("every"):
|
|
183
|
+
text += f", turn_<k>.pt every {value_text(checkpoint['every'])} turns"
|
|
184
|
+
return notes + [text + " and final.pt at the end"]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def train_notes(seen, key, value, keys, path):
|
|
188
|
+
resolved = seen.value(*keys)
|
|
189
|
+
if path == "train":
|
|
190
|
+
return []
|
|
191
|
+
if key == "epochs":
|
|
192
|
+
return [f"trains {value_text(resolved)} turns, each one pass over the train set"]
|
|
193
|
+
if key == "updates" and isinstance(resolved, dict):
|
|
194
|
+
turn = f", {value_text(resolved['turn'])} per turn" if resolved.get("turn") else ""
|
|
195
|
+
return [f"trains {value_text(resolved.get('total'))} updates{turn}"]
|
|
196
|
+
if key == "step":
|
|
197
|
+
return call_notes(seen, "train.step", value)
|
|
198
|
+
if key == "predicts":
|
|
199
|
+
if resolved is False:
|
|
200
|
+
return ["false: no model predicts, for a run whose model makes no predictions, such as a DDPM denoiser"]
|
|
201
|
+
model = ref_name(resolved, "models")
|
|
202
|
+
tail = " and they fill the predictions table" if seen.tabled() else ", and this data class writes no table"
|
|
203
|
+
return [f"the predicts model is {model}: the step computes its outputs for every batch{tail}"] if model else []
|
|
204
|
+
if key == "evaluate" and isinstance(resolved, list):
|
|
205
|
+
return [f"evaluates the {and_list(resolved)} {'set' if len(resolved) == 1 else 'sets'} after every turn"]
|
|
206
|
+
if key == "checkpoint":
|
|
207
|
+
return checkpoint_notes(resolved)
|
|
208
|
+
if key == "report":
|
|
209
|
+
weights = "best.pt" if resolved == "best" else "the weights of the last turn"
|
|
210
|
+
return [f"the final predictions, plots and samples use {weights}"]
|
|
211
|
+
if key == "track" and isinstance(resolved, list):
|
|
212
|
+
return [f"writes {and_list(f'track/{item}' for item in resolved)} every turn"]
|
|
213
|
+
if key == "log":
|
|
214
|
+
return [lego_note(name, seen.found(name)) for name in unique(legos_in(value, True))]
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def trigger_text(use, params, target):
|
|
219
|
+
def get(name):
|
|
220
|
+
found = params.get(name)
|
|
221
|
+
return found if found is not None else default_of(target, name)
|
|
222
|
+
|
|
223
|
+
if use == "after_epoch":
|
|
224
|
+
return f"from turn {value_text(get('at'))}"
|
|
225
|
+
if use == "every":
|
|
226
|
+
return f"every {value_text(get('epochs'))} turns"
|
|
227
|
+
if use == "plateau":
|
|
228
|
+
text = f"after {value_text(get('patience'))} turns without {get('watch')} improving"
|
|
229
|
+
text += f" by {value_text(get('min_delta'))}" if get("min_delta") else ""
|
|
230
|
+
return text + (" (higher is better)" if get("mode") == "max" else "")
|
|
231
|
+
if use in ("above", "below"):
|
|
232
|
+
return f"when {get('watch')} is {use} {value_text(get('value'))}"
|
|
233
|
+
if use == "time_budget":
|
|
234
|
+
return f"once {value_text(get('minutes'))} minutes have passed"
|
|
235
|
+
return f"when {use} fires" if use else "when its trigger fires"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def rule_sentence(rule, target):
|
|
239
|
+
when, after = rule.get("when"), rule.get("after")
|
|
240
|
+
use = use_of(when)
|
|
241
|
+
if when is None:
|
|
242
|
+
fires = f"fires on the turn after rule {after} fired" if after else "fires as soon as it is ready"
|
|
243
|
+
else:
|
|
244
|
+
fires = f"fires {trigger_text(use, when if isinstance(when, dict) else {}, target)}"
|
|
245
|
+
fires = f"asked from the turn after rule {after} fired, it {fires}" if after else fires
|
|
246
|
+
effects = [patch_text(path, value) for path, value in (rule.get("set") or {}).items()]
|
|
247
|
+
if rule.get("stop"):
|
|
248
|
+
effects.append("stops the run after that turn")
|
|
249
|
+
if rule.get("run") is not None:
|
|
250
|
+
effects.append(f"runs {use_of(rule['run']) or value_text(rule['run'])}")
|
|
251
|
+
text = f"{fires} and {and_list(effects)}" if effects else f"{fires} but has no set, stop or run"
|
|
252
|
+
if rule.get("once") is False:
|
|
253
|
+
text += "; with once false it may fire again, and its trigger is reset after each firing"
|
|
254
|
+
return text[0].upper() + text[1:] + "."
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def rule_notes(seen, key, value, keys, path):
|
|
258
|
+
resolved = seen.mapping(*keys)
|
|
259
|
+
if not resolved:
|
|
260
|
+
return []
|
|
261
|
+
notes = [rule_sentence(resolved, seen.target(f"{path}.when"))]
|
|
262
|
+
use = use_of(resolved.get("when"))
|
|
263
|
+
if use and use not in TRIGGERS:
|
|
264
|
+
notes += call_notes(seen, f"{path}.when", resolved.get("when"))
|
|
265
|
+
if resolved.get("run") is not None:
|
|
266
|
+
notes += call_notes(seen, f"{path}.run", resolved.get("run"))
|
|
267
|
+
return notes
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def plot_notes(seen, key, value, keys, path):
|
|
271
|
+
notes = call_notes(seen, path, value)
|
|
272
|
+
target = seen.target(path)
|
|
273
|
+
if target is not None and not getattr(target, "__module__", "").startswith("kalfa.std"):
|
|
274
|
+
notes.append(f"a figure it returns is saved as plots/{key}.png")
|
|
275
|
+
return notes
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def generate_notes(seen, key, value, keys, path):
|
|
279
|
+
if path == "generate":
|
|
280
|
+
return call_notes(seen, "generate", value)
|
|
281
|
+
if key == "use":
|
|
282
|
+
return [lego_note(value, seen.target("generate"))]
|
|
283
|
+
entry = aliases.entry_of(use_of(seen.mapping("generate")) or "")
|
|
284
|
+
note = entry.params.get(key) if entry is not None else None
|
|
285
|
+
return [f"{key}: {short(note)}"] if note else []
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def strategy_name(use):
|
|
289
|
+
module = importlib.import_module("kalfa.commands.sweep")
|
|
290
|
+
target = getattr(module, "STRATEGIES", {}).get(use)
|
|
291
|
+
if target is None:
|
|
292
|
+
return use
|
|
293
|
+
path = aliases.target_path(target)
|
|
294
|
+
names = [entry.name for entry in aliases.catalog(slot="strategy") if aliases.target_path(entry.target) == path]
|
|
295
|
+
return names[0] if names else use
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def point_count(seen):
|
|
299
|
+
from kalfa.commands.sweep.strategies import strategy_of
|
|
300
|
+
|
|
301
|
+
strategy = strategy_of(seen.mapping("sweep").get("strategy"))
|
|
302
|
+
if callable(getattr(strategy, "points", None)):
|
|
303
|
+
return len(strategy.points(seen.space(), 0))
|
|
304
|
+
return getattr(strategy, "trials", None)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def sweep_notes(seen, key, value, keys, path):
|
|
308
|
+
resolved = seen.value(*keys)
|
|
309
|
+
if key == "strategy":
|
|
310
|
+
use = use_of(resolved) or "grid"
|
|
311
|
+
name = strategy_name(use)
|
|
312
|
+
notes = [lego_note(name)] + param_lines(name, resolved)
|
|
313
|
+
try:
|
|
314
|
+
count = point_count(seen)
|
|
315
|
+
except Exception:
|
|
316
|
+
count = None
|
|
317
|
+
return notes + ([f"the sweep runs {count} points"] if count else [])
|
|
318
|
+
if key == "space" and isinstance(resolved, dict):
|
|
319
|
+
notes = []
|
|
320
|
+
for name, entry in resolved.items():
|
|
321
|
+
paths = seen.uses.get(str(name), [])
|
|
322
|
+
where = f", read as ${name}$ by {and_list(paths[:3])}" if paths else ""
|
|
323
|
+
notes.append(f"{name}: {range_text(entry, seen.grid)}{where}")
|
|
324
|
+
return notes
|
|
325
|
+
if key == "objective" and isinstance(resolved, dict):
|
|
326
|
+
better = "higher" if resolved.get("mode") == "max" else "lower"
|
|
327
|
+
at = resolved.get("at") or "best"
|
|
328
|
+
return [f"ranks the points by the {at} value of {resolved.get('watch')} over the turns of each point, "
|
|
329
|
+
f"{better} being better"]
|
|
330
|
+
if key == "record":
|
|
331
|
+
return [f"the sweep root: the points are written to {value_text(resolved)}/0000, "
|
|
332
|
+
f"{value_text(resolved)}/0001 and so on"]
|
|
333
|
+
return []
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from ruamel.yaml import YAML
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
WIDTH = 88
|
|
8
|
+
SHORT = 150
|
|
9
|
+
FLOW = set(",[]{}")
|
|
10
|
+
GLOB = set("*?")
|
|
11
|
+
DUMPER = YAML(typ="safe")
|
|
12
|
+
DUMPER.width = 4096
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def flat(text):
|
|
16
|
+
return " ".join(str(text or "").split())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def short(text):
|
|
20
|
+
text = flat(text)
|
|
21
|
+
return text.split("; ")[0] if len(text) > SHORT and "; " in text else text
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def lower_first(text):
|
|
25
|
+
return text[0].lower() + text[1:] if len(text) > 1 and text[0].isupper() and text[1].islower() else text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def and_list(items):
|
|
29
|
+
items = [str(item) for item in items]
|
|
30
|
+
if len(items) < 2:
|
|
31
|
+
return "".join(items)
|
|
32
|
+
return ", ".join(items[:-1]) + " and " + items[-1]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def unique(items):
|
|
36
|
+
return list(dict.fromkeys(items))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def plain(text):
|
|
40
|
+
stream = io.StringIO()
|
|
41
|
+
DUMPER.dump(text, stream)
|
|
42
|
+
out = stream.getvalue()
|
|
43
|
+
for tail in ("\n...\n", "\n"):
|
|
44
|
+
if out.endswith(tail):
|
|
45
|
+
return out[:-len(tail)] == text
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def scalar_text(value, inner=False):
|
|
50
|
+
if value is None:
|
|
51
|
+
return "null"
|
|
52
|
+
if isinstance(value, bool):
|
|
53
|
+
return "true" if value else "false"
|
|
54
|
+
if isinstance(value, float) and value != value:
|
|
55
|
+
return ".nan"
|
|
56
|
+
if isinstance(value, float) and value in (float("inf"), float("-inf")):
|
|
57
|
+
return ".inf" if value > 0 else "-.inf"
|
|
58
|
+
if isinstance(value, (int, float)):
|
|
59
|
+
return repr(value)
|
|
60
|
+
text = str(value)
|
|
61
|
+
if text and plain(text) and not GLOB & set(text) and not (inner and (FLOW & set(text) or ": " in text)):
|
|
62
|
+
return text
|
|
63
|
+
return json.dumps(text, ensure_ascii=False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def flow(value):
|
|
67
|
+
if isinstance(value, dict):
|
|
68
|
+
return "{" + ", ".join(f"{scalar_text(key, True)}: {flow(item)}" for key, item in value.items()) + "}"
|
|
69
|
+
if isinstance(value, (list, tuple)):
|
|
70
|
+
return "[" + ", ".join(flow(item) for item in value) + "]"
|
|
71
|
+
return scalar_text(value, True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def value_text(value):
|
|
75
|
+
return value if isinstance(value, str) else flow(value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def yaml_lines(key, value, indent=""):
|
|
79
|
+
head = f"{indent}{scalar_text(key)}:"
|
|
80
|
+
one = f"{head} {flow(value)}"
|
|
81
|
+
if not isinstance(value, (dict, list)) or not value or len(one) <= WIDTH:
|
|
82
|
+
return [one]
|
|
83
|
+
if isinstance(value, dict):
|
|
84
|
+
return [head] + [line for inner, item in value.items() for line in yaml_lines(inner, item, indent + " ")]
|
|
85
|
+
return [head] + [line for item in value for line in item_lines(item, indent + " ")]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def item_lines(item, indent):
|
|
89
|
+
one = f"{indent}- {flow(item)}"
|
|
90
|
+
if len(one) <= WIDTH or not isinstance(item, dict) or not item:
|
|
91
|
+
return [one]
|
|
92
|
+
lines = [line for key, value in item.items() for line in yaml_lines(key, value, indent + " ")]
|
|
93
|
+
return [f"{indent}- {lines[0][len(indent) + 2:]}"] + lines[1:]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def yaml_text(key, value):
|
|
97
|
+
return "\n".join(yaml_lines(key, value))
|