kalfa 3.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,225 @@
1
+ import numpy
2
+ import pandas
3
+
4
+ from .tables import finite, label_text, labels, numbers_of, numeric, ordered_labels, texts
5
+
6
+
7
+ CODES = (3, 20)
8
+
9
+
10
+ def plan_nodes(plan):
11
+ nodes = (plan or {}).get("nodes")
12
+ return nodes if isinstance(nodes, dict) else {}
13
+
14
+
15
+ def plan_targets(plan):
16
+ node = plan_nodes(plan).get("data")
17
+ params = node.get("params") if isinstance(node, dict) else None
18
+ targets = params.get("targets") if isinstance(params, dict) else None
19
+ if isinstance(targets, dict) and targets:
20
+ return [str(name) for name in targets]
21
+ if isinstance(targets, (list, tuple)) and targets:
22
+ return [str(name) for name in targets]
23
+ if isinstance(targets, str):
24
+ return [targets]
25
+ fields = ((plan or {}).get("data") or {}).get("fields")
26
+ if isinstance(fields, dict):
27
+ return [str(name) for name, field in fields.items() if isinstance(field, dict) and field.get("target")]
28
+ return []
29
+
30
+
31
+ def plan_classes(plan):
32
+ return labels(((plan or {}).get("data") or {}).get("classes"))
33
+
34
+
35
+ def fitted_classes(fields, target=None):
36
+ if not isinstance(fields, dict):
37
+ return None
38
+ if target is not None:
39
+ for item in fields.get("target_fields") or ():
40
+ if isinstance(item, dict) and item.get("name") == target:
41
+ return labels(item.get("classes"))
42
+ return labels(fields.get("classes"))
43
+
44
+
45
+ def fitted_entries(fields):
46
+ if not isinstance(fields, dict):
47
+ return {}
48
+ return {str(item["name"]): labels(item.get("classes")) for item in fields.get("target_fields") or ()
49
+ if isinstance(item, dict) and item.get("name") is not None}
50
+
51
+
52
+ def classes_for(plan, fitted, target=None):
53
+ entries = fitted_entries(fitted)
54
+ if target in entries and (entries[target] is None or sum(1 for found in entries.values() if found) > 1):
55
+ return entries[target]
56
+ return plan_classes(plan) or fitted_classes(fitted, target)
57
+
58
+
59
+ def plan_pairings(plan):
60
+ pairs = {}
61
+ for path, node in plan_nodes(plan).items():
62
+ if not isinstance(node, dict) or node.get("slot") not in ("loss", "metric"):
63
+ continue
64
+ keys = node.get("keys") if isinstance(node.get("keys"), dict) else {}
65
+ pred, target = keys.get("pred"), keys.get("target")
66
+ if isinstance(pred, str) and isinstance(target, str):
67
+ pairs.setdefault(pred, target)
68
+ return pairs
69
+
70
+
71
+ def frame_targets(frame):
72
+ return [column for column in frame.columns
73
+ if column != "row" and not str(column).startswith(("pred_", "raw_", "prob_"))]
74
+
75
+
76
+ def kind_of(frame, target, classes, scores):
77
+ if classes or scores:
78
+ return "classification"
79
+ return "regression" if pandas.api.types.is_numeric_dtype(frame[target]) else "classification"
80
+
81
+
82
+ def spread_member(column, columns):
83
+ stem, _, position = str(column).rpartition("_")
84
+ if stem and position.isdigit() and f"{stem}_0" in columns and f"{stem}_1" in columns:
85
+ return stem, int(position)
86
+ return None
87
+
88
+
89
+ def declared_targets(plan, fitted, frame):
90
+ targets = plan_targets(plan)
91
+ if targets:
92
+ return targets
93
+ if isinstance(fitted, dict) and isinstance(fitted.get("targets"), list):
94
+ return [str(name) for name in fitted["targets"]]
95
+ found = frame_targets(frame)
96
+ return found if len(found) == 1 else []
97
+
98
+
99
+ def matches(frame, plan, fitted=None):
100
+ columns = [str(column) for column in frame.columns]
101
+ pairings = plan_pairings(plan)
102
+ targets = declared_targets(plan, fitted, frame)
103
+ fallback = targets[0] if len(targets) == 1 else None
104
+ found = []
105
+ for column in columns:
106
+ if not column.startswith("pred_"):
107
+ continue
108
+ output = column[len("pred_"):]
109
+ target, base = pairings.get(output), None
110
+ member = None if target is not None else spread_member(column, columns)
111
+ if member is not None:
112
+ base = pairings.get(member[0][len("pred_"):], fallback)
113
+ target = None if base is None else f"{base}_{member[1]}"
114
+ elif target is None:
115
+ target = fallback
116
+ if target is None or target not in columns or target == column:
117
+ target = None
118
+ found.append((column, output, target, base))
119
+ return found
120
+
121
+
122
+ def raw_of(columns, output):
123
+ return f"raw_{output}" if f"raw_{output}" in columns else None
124
+
125
+
126
+ def scores_of(columns, output):
127
+ return [name for name in columns if name.startswith(f"prob_{output}_")]
128
+
129
+
130
+ def suffix_label(suffix, known):
131
+ if suffix in known:
132
+ return suffix
133
+ try:
134
+ number = float(suffix)
135
+ except ValueError:
136
+ return suffix
137
+ return label_text(number) or suffix
138
+
139
+
140
+ def score_labels(scores, output, known=()):
141
+ prefix = f"prob_{output}_"
142
+ known = set(known or ())
143
+ return {column: suffix_label(column[len(prefix):], known) for column in scores if column.startswith(prefix)}
144
+
145
+
146
+ def label_columns(scores, output, known=()):
147
+ found = {}
148
+ for column, label in score_labels(scores, output, known).items():
149
+ found.setdefault(label, column)
150
+ return found
151
+
152
+
153
+ def prediction_pairs(frame, plan, fitted=None):
154
+ if frame is None:
155
+ return []
156
+ columns = [str(column) for column in frame.columns]
157
+ pairs = []
158
+ for column, output, target, base in matches(frame, plan, fitted):
159
+ if target is None:
160
+ continue
161
+ raw, scores = raw_of(columns, output), scores_of(columns, output)
162
+ kind = kind_of(frame, target, classes_for(plan, fitted, base or target), scores)
163
+ pairs.append({"output": output, "pred": column, "target": target, "kind": kind, "raw": raw, "scores": scores})
164
+ return pairs
165
+
166
+
167
+ def observed(frame, pred, target):
168
+ return ordered_labels(set(texts(frame[target]).tolist()) | set(texts(frame[pred]).tolist()))
169
+
170
+
171
+ def detected(frame, pred, target, classes, scores):
172
+ truth = frame[target]
173
+ if classes or scores or not numeric(truth):
174
+ found = classes or observed(frame, pred, target)
175
+ if len(found) == 2:
176
+ return "binary", found, ["binary", "multiclass"]
177
+ return "multiclass", found, ["multiclass"]
178
+ distinct = numpy.unique(finite(numbers_of(truth)))
179
+ if len(distinct) == 2:
180
+ return "binary", [label_text(value) for value in distinct], ["binary", "regression"]
181
+ low, high = CODES
182
+ if numeric(frame[pred]) and low <= len(distinct) <= high and bool(numpy.all(distinct == numpy.round(distinct))):
183
+ guessed = numpy.unique(finite(numbers_of(frame[pred])))
184
+ if bool(numpy.isin(guessed, distinct).all()):
185
+ return "multiclass", [label_text(value) for value in distinct], ["multiclass", "regression"]
186
+ return "regression", None, ["regression"]
187
+
188
+
189
+ def target_pair(frame, column, output, target, classes):
190
+ columns = [str(name) for name in frame.columns]
191
+ scores = scores_of(columns, output)
192
+ auto, found, tasks = detected(frame, column, target, classes, scores)
193
+ return {"output": output, "pred": column, "columns": [column], "target": target, "auto": auto, "tasks": tasks,
194
+ "raw": raw_of(columns, output), "scores": scores, "classes": found}
195
+
196
+
197
+ def loose_pair(output, pred, task, columns, raw=None):
198
+ return {"output": output, "pred": pred, "columns": columns, "target": None, "auto": task, "tasks": [task],
199
+ "raw": raw, "scores": [], "classes": None}
200
+
201
+
202
+ def task_pairs(frame, plan, fitted=None):
203
+ columns = [str(column) for column in frame.columns]
204
+ found, spreads = [], {}
205
+ for column, output, target, base in matches(frame, plan, fitted):
206
+ if target is not None:
207
+ found.append(target_pair(frame, column, output, target, classes_for(plan, fitted, base or target)))
208
+ continue
209
+ member = spread_member(column, columns)
210
+ if member is None:
211
+ found.append(loose_pair(output, column, "values", [column], raw_of(columns, output)))
212
+ continue
213
+ stem = member[0]
214
+ if stem not in spreads:
215
+ spreads[stem] = loose_pair(stem[len("pred_"):], stem, "embedding", [])
216
+ found.append(spreads[stem])
217
+ spreads[stem]["columns"].append(column)
218
+ return [single(pair, columns) if pair["auto"] == "embedding" and len(pair["columns"]) < 2 else pair
219
+ for pair in found]
220
+
221
+
222
+ def single(pair, columns):
223
+ column = pair["columns"][0]
224
+ output = column[len("pred_"):]
225
+ return loose_pair(output, column, "values", [column], raw_of(columns, output))
@@ -0,0 +1,153 @@
1
+ import math
2
+ import numbers
3
+ from pathlib import Path
4
+
5
+ import numpy
6
+ import pandas
7
+
8
+ from ...core.errors import KalfaError
9
+ from ..reader import fields_of
10
+
11
+
12
+ PREDICTIONS = "predictions.parquet"
13
+ IMAGE_TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
14
+ ".bmp": "image/bmp", ".webp": "image/webp"}
15
+ IMAGE_SUFFIXES = tuple(IMAGE_TYPES)
16
+
17
+
18
+ def load_table(directory, name=PREDICTIONS):
19
+ path = Path(directory) / name
20
+ if not path.is_file():
21
+ return None
22
+ try:
23
+ return pandas.read_parquet(path)
24
+ except (OSError, ValueError):
25
+ return None
26
+
27
+
28
+ def filtered(frame, where, name=PREDICTIONS):
29
+ if not where:
30
+ return frame
31
+ try:
32
+ return frame.query(where)
33
+ except Exception as exc:
34
+ raise KalfaError(f"bad where for {name}: {exc}") from exc
35
+
36
+
37
+ def read_predictions(directory, name=PREDICTIONS, where=None):
38
+ frame = load_table(directory, name)
39
+ return None if frame is None else filtered(frame, where, name)
40
+
41
+
42
+ def prediction_tables(directory):
43
+ root = Path(directory)
44
+ if not root.is_dir():
45
+ return []
46
+ return sorted(path.name for path in root.glob("predictions*.parquet") if path.is_file())
47
+
48
+
49
+ def read_fitted_fields(directory):
50
+ return fields_of(directory)
51
+
52
+
53
+ def image_column(frame):
54
+ if "path" not in [str(column) for column in frame.columns]:
55
+ return None
56
+ values = [value for value in frame["path"].tolist() if not missing(value)]
57
+ if values and all(isinstance(value, str) and value.lower().endswith(IMAGE_SUFFIXES) for value in values):
58
+ return "path"
59
+ return None
60
+
61
+
62
+ def missing(value):
63
+ if value is None:
64
+ return True
65
+ try:
66
+ return bool(pandas.isna(value))
67
+ except (TypeError, ValueError):
68
+ return False
69
+
70
+
71
+ def label_text(value):
72
+ if isinstance(value, numpy.generic):
73
+ value = value.item()
74
+ if missing(value):
75
+ return None
76
+ if isinstance(value, bool):
77
+ return str(value)
78
+ if isinstance(value, numbers.Integral):
79
+ return str(int(value))
80
+ if isinstance(value, numbers.Real):
81
+ number = float(value)
82
+ if math.isfinite(number) and number.is_integer():
83
+ return str(int(number))
84
+ return format(number, ".12g")
85
+ return str(value)
86
+
87
+
88
+ def labels(classes):
89
+ if not isinstance(classes, (list, tuple)) or not classes:
90
+ return None
91
+ return [label_text(label) or str(label) for label in classes]
92
+
93
+
94
+ def label_key(text):
95
+ try:
96
+ number = float(text)
97
+ except (TypeError, ValueError):
98
+ return 1, 0.0, str(text)
99
+ return (0, number, "") if math.isfinite(number) else (1, 0.0, str(text))
100
+
101
+
102
+ def ordered_labels(values):
103
+ return sorted({value for value in values if value is not None}, key=label_key)
104
+
105
+
106
+ def cached_text(known, value):
107
+ try:
108
+ return known[(type(value), value)]
109
+ except KeyError:
110
+ text = known[(type(value), value)] = label_text(value)
111
+ return text
112
+ except TypeError:
113
+ return label_text(value)
114
+
115
+
116
+ def texts(series):
117
+ known = {}
118
+ return numpy.asarray([cached_text(known, value) for value in series.tolist()], dtype=object)
119
+
120
+
121
+ def present(values):
122
+ return numpy.fromiter((value is not None for value in values), dtype=bool, count=len(values))
123
+
124
+
125
+ def numeric(series):
126
+ return pandas.api.types.is_numeric_dtype(series) and not pandas.api.types.is_bool_dtype(series)
127
+
128
+
129
+ def numbers_of(series):
130
+ return pandas.to_numeric(series, errors="coerce").to_numpy(dtype=numpy.float64)
131
+
132
+
133
+ def finite(values):
134
+ array = numpy.asarray(values, dtype=numpy.float64).reshape(-1)
135
+ return array[numpy.isfinite(array)]
136
+
137
+
138
+ def plain(values):
139
+ return [None if isinstance(value, float) and not math.isfinite(value) else value for value in values.tolist()]
140
+
141
+
142
+ def plain_value(value):
143
+ if isinstance(value, numpy.generic):
144
+ value = value.item()
145
+ if isinstance(value, float) and not math.isfinite(value):
146
+ return None
147
+ return None if missing(value) else value
148
+
149
+
150
+ def rows_in(frame):
151
+ if "row" in frame.columns:
152
+ return frame["row"].to_numpy()
153
+ return frame.index.to_numpy()
kalfa/records/sweep.py ADDED
@@ -0,0 +1,244 @@
1
+ import json
2
+ import math
3
+ import re
4
+ from pathlib import Path
5
+
6
+ import numpy
7
+
8
+ from .reader import (
9
+ MANIFEST,
10
+ SWEEP,
11
+ best_in,
12
+ is_finite,
13
+ is_number,
14
+ is_sweep_root,
15
+ normal_mode,
16
+ read_history,
17
+ read_json,
18
+ record_stamp,
19
+ safe_json,
20
+ summary_of,
21
+ )
22
+
23
+
24
+ POINT_NAME = re.compile(r"^\d{4}$")
25
+ FAILED = "failed"
26
+
27
+
28
+ is_sweep = is_sweep_root
29
+
30
+
31
+ def read_sweep(root):
32
+ return read_json(root, SWEEP)
33
+
34
+
35
+ def point_dirs(root):
36
+ root = Path(root)
37
+ try:
38
+ found = [entry for entry in root.iterdir() if entry.is_dir() and POINT_NAME.match(entry.name)]
39
+ except OSError:
40
+ return []
41
+ return sorted(found, key=lambda path: int(path.name))
42
+
43
+
44
+ def objective_of(summary, objective):
45
+ if not isinstance(summary, dict) or not isinstance(objective, dict):
46
+ return None
47
+ watch = objective.get("watch")
48
+ if watch is None:
49
+ return None
50
+ if str(objective.get("at") or "best") == "last":
51
+ last = summary.get("last")
52
+ value = last.get(watch) if isinstance(last, dict) else None
53
+ else:
54
+ value = best_value(summary, watch, mode_of(objective))
55
+ return float(value) if is_finite(value) else None
56
+
57
+
58
+ def best_value(summary, watch, mode):
59
+ best = summary.get("best")
60
+ if isinstance(best, dict) and best.get("series") == watch and normal_mode(best.get("mode")) == mode:
61
+ return best.get("value")
62
+ path = summary.get("path")
63
+ if not isinstance(path, (str, Path)):
64
+ return None
65
+ return best_in(read_history(path), watch, mode)[0]
66
+
67
+
68
+ def point_id(summary, directory):
69
+ sweep = summary.get("sweep")
70
+ if isinstance(sweep, dict) and is_number(sweep.get("id")):
71
+ return int(sweep["id"])
72
+ return int(directory.name)
73
+
74
+
75
+ def point_values(summary):
76
+ sweep = summary.get("sweep")
77
+ if isinstance(sweep, dict) and isinstance(sweep.get("values"), dict):
78
+ return dict(sweep["values"])
79
+ params = summary.get("params")
80
+ return dict(params) if isinstance(params, dict) else {}
81
+
82
+
83
+ def point_of(directory, root, objective):
84
+ summary = summary_of(directory)
85
+ if not isinstance(summary, dict):
86
+ return None
87
+ status = summary.get("status")
88
+ return {
89
+ "id": point_id(summary, directory),
90
+ "path": directory.relative_to(root).as_posix(),
91
+ "values": point_values(summary),
92
+ "status": status,
93
+ "turn": summary.get("turn"),
94
+ "total": summary.get("total"),
95
+ "objective": None if status == FAILED else objective_of(summary, objective),
96
+ "summary": summary,
97
+ }
98
+
99
+
100
+ def sweep_points(root, cache=None):
101
+ root = Path(root)
102
+ sweep = read_sweep(root)
103
+ objective = sweep.get("objective") if isinstance(sweep, dict) else None
104
+ asked = json.dumps(objective, sort_keys=True, default=str)
105
+ points = []
106
+ for directory in point_dirs(root):
107
+ if cache is None:
108
+ point = point_of(directory, root, objective)
109
+ else:
110
+ stamp = record_stamp(directory, safe_json(directory, MANIFEST))
111
+ point = cache.get(("point", str(directory), str(root), asked), stamp,
112
+ lambda directory=directory: point_of(directory, root, objective))
113
+ if point is not None:
114
+ points.append(dict(point))
115
+ return points
116
+
117
+
118
+ def is_valued(point):
119
+ return is_finite(point.get("objective")) and point.get("status") != FAILED
120
+
121
+
122
+ def id_key(point):
123
+ identity = point.get("id")
124
+ return (0, identity, "") if is_number(identity) else (1, 0, str(identity))
125
+
126
+
127
+ def mode_of(objective):
128
+ return normal_mode(objective.get("mode") if isinstance(objective, dict) else objective)
129
+
130
+
131
+ def rank_points(points, objective):
132
+ maximize = mode_of(objective) == "max"
133
+ valued = [point for point in points if is_valued(point)]
134
+ unvalued = [point for point in points if not is_valued(point)]
135
+ valued.sort(key=lambda point: ((-point["objective"] if maximize else point["objective"]), id_key(point)))
136
+ unvalued.sort(key=id_key)
137
+ return valued + unvalued
138
+
139
+
140
+ def best_point(points, objective):
141
+ ranked = rank_points(points, objective)
142
+ if ranked and is_valued(ranked[0]):
143
+ return ranked[0]
144
+ return None
145
+
146
+
147
+ def running_best(points, objective):
148
+ maximize = mode_of(objective) == "max"
149
+ ordered = sorted(points, key=id_key)
150
+ best = None
151
+ found = {"ids": [], "objective": [], "best": []}
152
+ for point in ordered:
153
+ value = point["objective"] if is_valued(point) else None
154
+ if value is not None and (best is None or (value > best if maximize else value < best)):
155
+ best = value
156
+ found["ids"].append(point.get("id"))
157
+ found["objective"].append(value)
158
+ found["best"].append(best)
159
+ return found
160
+
161
+
162
+ def is_log_space(spec):
163
+ return isinstance(spec, dict) and isinstance(spec.get("log"), (list, tuple))
164
+
165
+
166
+ def label_of(value):
167
+ if isinstance(value, float):
168
+ return f"{value:.4g}"
169
+ return value if isinstance(value, str) else json.dumps(value, default=str)
170
+
171
+
172
+ def axis_of(key, values, spec):
173
+ numeric = all(is_finite(value) for value in values)
174
+ if numeric and is_log_space(spec) and all(value > 0 for value in values):
175
+ shown = sorted(set(values))
176
+ return {"key": key, "label": f"{key} (log)", "values": [math.log10(value) for value in values],
177
+ "ticks": [math.log10(value) for value in shown] if len(shown) <= 12 else None,
178
+ "text": [label_of(float(value)) for value in shown] if len(shown) <= 12 else None}
179
+ if numeric:
180
+ return {"key": key, "label": key, "values": [float(value) for value in values], "ticks": None, "text": None}
181
+ names = []
182
+ for value in values:
183
+ name = label_of(value)
184
+ if name not in names:
185
+ names.append(name)
186
+ return {"key": key, "label": key, "values": [float(names.index(label_of(value))) for value in values],
187
+ "ticks": [float(position) for position in range(len(names))], "text": names}
188
+
189
+
190
+ def axes_of(points, sweep):
191
+ sweep = sweep if isinstance(sweep, dict) else {}
192
+ objective = sweep.get("objective") if isinstance(sweep.get("objective"), dict) else {}
193
+ space = sweep.get("space") if isinstance(sweep.get("space"), dict) else {}
194
+ valued = sorted((point for point in points if is_valued(point)), key=id_key)
195
+ keys = []
196
+ for point in valued:
197
+ keys.extend(key for key in point["values"] if key not in keys)
198
+ keys = [key for key in keys if all(key in point["values"] for point in valued)]
199
+ axes = [axis_of(key, [point["values"][key] for point in valued], space.get(key)) for key in keys]
200
+ watch = str(objective.get("watch") or "objective")
201
+ axes.append({"key": "objective", "label": watch, "values": [float(point["objective"]) for point in valued],
202
+ "ticks": None, "text": None})
203
+ return {"ids": [point.get("id") for point in valued], "axes": axes if valued else [],
204
+ "left_out": len(points) - len(valued), "mode": mode_of(objective)}
205
+
206
+
207
+ def frozen(value):
208
+ if isinstance(value, dict):
209
+ return tuple(sorted((str(key), frozen(item)) for key, item in value.items()))
210
+ if isinstance(value, (list, tuple, set)):
211
+ return tuple(frozen(item) for item in value)
212
+ return value
213
+
214
+
215
+ def numeric_last(summary):
216
+ last = summary.get("last")
217
+ if not isinstance(last, dict):
218
+ return {}
219
+ return {key: float(value) for key, value in last.items() if is_finite(value)}
220
+
221
+
222
+ def group_folds(summaries, key="fold"):
223
+ groups = {}
224
+ for summary in summaries:
225
+ if not isinstance(summary, dict):
226
+ continue
227
+ params = summary.get("params")
228
+ if not isinstance(params, dict) or key not in params:
229
+ continue
230
+ rest = {name: value for name, value in params.items() if name != key}
231
+ group_key = frozen(rest)
232
+ if group_key not in groups:
233
+ groups[group_key] = {"group": rest, "members": [], "mean": {}, "std": {}}
234
+ groups[group_key]["members"].append(summary)
235
+ for group in groups.values():
236
+ lasts = [numeric_last(member) for member in group["members"]]
237
+ common = set(lasts[0])
238
+ for last in lasts[1:]:
239
+ common &= set(last)
240
+ for series in sorted(common):
241
+ values = numpy.asarray([last[series] for last in lasts], dtype=float)
242
+ group["mean"][series] = float(values.mean())
243
+ group["std"][series] = float(numpy.std(values))
244
+ return list(groups.values())
kalfa/std/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ from . import ( # noqa: F401
2
+ actions,
3
+ data,
4
+ init,
5
+ loggers,
6
+ losses,
7
+ metrics,
8
+ models,
9
+ objectives,
10
+ plots,
11
+ samplers,
12
+ samples,
13
+ schedules,
14
+ steps,
15
+ triggers,
16
+ )
17
+ from . import notes # noqa: E402,F401
kalfa/std/actions.py ADDED
@@ -0,0 +1,35 @@
1
+ import torch
2
+
3
+ from kalfa.core.aliases import register
4
+ from kalfa.core.errors import BuildError, RunError
5
+ from kalfa.core.protocols import Action
6
+ from kalfa.std.samples import save_samples
7
+
8
+
9
+ class WriteSamples(Action):
10
+ requires_generate = True
11
+
12
+ def __init__(self, sampler=None, n=16):
13
+ self.sampler = sampler
14
+ self.n = int(n)
15
+
16
+ def bind(self, ctx):
17
+ if self.sampler is None and getattr(ctx, "generate", None) is None:
18
+ raise BuildError(f"{self.path or 'write_samples'}: write_samples needs a sampler: write sampler: "
19
+ "{use: gan_sampler, ...} or a generate section")
20
+
21
+ def __call__(self, ctx):
22
+ sampler = self.sampler if self.sampler is not None else getattr(ctx, "generate", None)
23
+ if sampler is None:
24
+ raise RunError(f"{self.path or 'write_samples'} needs a sampler: write sampler: <a sampler call> or a "
25
+ "generate section")
26
+ with torch.no_grad():
27
+ samples = sampler.sample(ctx, self.n)
28
+ if getattr(ctx, "record", None) is None:
29
+ return []
30
+ return save_samples(samples, ctx.record.path("samples"), f"turn_{ctx.turn}")
31
+
32
+
33
+ register("write_samples", "kalfa.std.actions.WriteSamples", slot="action",
34
+ doc="draws n samples from its sampler (or the run's generate) and writes samples/turn_<k>.pt, a png grid "
35
+ "for images or a txt for text")