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,98 @@
1
+ from kalfa.commands.sweep.space import SPACE_HINT, space_values
2
+ from kalfa.commands.sweep.strategies import STRATEGY_HINT, Grid, strategy_of
3
+ from kalfa.core.check import SERIES_PREFIXES
4
+ from kalfa.core.context import train_of
5
+ from kalfa.core.errors import BuildError, error
6
+
7
+
8
+ SWEEP_KEYS = ("strategy", "space", "objective", "record")
9
+ OBJECTIVE_KEYS = ("watch", "mode", "at")
10
+ MODES = ("min", "max")
11
+ MOMENTS = ("best", "last")
12
+
13
+
14
+ def series_problem(tree, watch):
15
+ names = set(tree.section("losses")) | set(tree.section("metrics"))
16
+ prefixes = set(SERIES_PREFIXES)
17
+ evaluate = train_of(tree).get("evaluate")
18
+ if isinstance(evaluate, list):
19
+ prefixes.update(str(name) for name in evaluate)
20
+ prefix, _, rest = watch.partition("/")
21
+ if not rest or prefix not in prefixes:
22
+ return f"{watch!r} does not look like a series such as val/<metric>"
23
+ if prefix in ("train", "val", "test") and rest.split("/")[0] not in names:
24
+ known = ", ".join(sorted(names)) or "none"
25
+ return f"{watch!r} names no loss or metric; the losses and metrics are {known}"
26
+ return None
27
+
28
+
29
+ def check_space(tree, space, problems):
30
+ try:
31
+ dimensions = space_values(space)
32
+ except BuildError as exc:
33
+ problems.append(error("sweep_space", str(exc), "sweep.space", hint=SPACE_HINT))
34
+ return None
35
+ params = getattr(getattr(tree, "config", None), "params", None) or {}
36
+ known = ", ".join(sorted(str(name) for name in params))
37
+ for name in dimensions:
38
+ if name not in params:
39
+ problems.append(error("sweep_param", f"{name!r} is not in the params section; a swept name is a param the "
40
+ f"config reads as ${name}$", "sweep.space",
41
+ hint=f"the params are {known}" if known else "define it under params"))
42
+ return dimensions
43
+
44
+
45
+ def check_strategy(raw, space, dimensions, problems):
46
+ try:
47
+ strategy = strategy_of(raw)
48
+ except BuildError as exc:
49
+ problems.append(error("sweep_strategy", str(exc), "sweep.strategy", hint=STRATEGY_HINT))
50
+ return
51
+ if dimensions is not None and isinstance(strategy, Grid):
52
+ try:
53
+ strategy.axes(space)
54
+ except BuildError as exc:
55
+ problems.append(error("sweep_space", str(exc), "sweep.space"))
56
+
57
+
58
+ def check_objective(tree, objective, problems):
59
+ where = "sweep.objective"
60
+ if not isinstance(objective, dict):
61
+ problems.append(error("sweep_objective", "sweep.objective is a mapping {watch, mode, at}", where,
62
+ hint="write objective: {watch: val/<metric>, mode: min}"))
63
+ return
64
+ for key in objective:
65
+ if key not in OBJECTIVE_KEYS:
66
+ problems.append(error("sweep_objective", f"{key!r} is not an objective key; the keys are "
67
+ + ", ".join(OBJECTIVE_KEYS), where))
68
+ watch = objective.get("watch")
69
+ if not isinstance(watch, str) or not watch:
70
+ problems.append(error("sweep_objective", "the objective needs watch, the series that ranks the points", where,
71
+ hint="such as watch: val/rmse"))
72
+ else:
73
+ message = series_problem(tree, watch)
74
+ if message:
75
+ problems.append(error("sweep_objective", message, where))
76
+ mode = objective.get("mode")
77
+ if mode is not None and mode not in MODES:
78
+ problems.append(error("sweep_objective", f"mode is min or max, got {mode!r}", where))
79
+ at = objective.get("at")
80
+ if at is not None and at not in MOMENTS:
81
+ problems.append(error("sweep_objective", f"at is best or last, got {at!r}", where))
82
+
83
+
84
+ def check_sweep(tree, raw, problems):
85
+ if not isinstance(raw, dict):
86
+ problems.append(error("sweep_key", "sweep is a mapping with strategy, space, objective and record", "sweep"))
87
+ return
88
+ for key in raw:
89
+ if key not in SWEEP_KEYS:
90
+ problems.append(error("sweep_key", f"{key!r} is not a sweep key; the keys are " + ", ".join(SWEEP_KEYS),
91
+ f"sweep.{key}"))
92
+ dimensions = check_space(tree, raw.get("space"), problems)
93
+ check_strategy(raw.get("strategy"), raw.get("space"), dimensions, problems)
94
+ check_objective(tree, raw.get("objective"), problems)
95
+ record = raw.get("record")
96
+ if not isinstance(record, str) or not record.strip():
97
+ problems.append(error("sweep_record", "the sweep needs record, the root directory its points go under",
98
+ "sweep.record", hint="write record: runs/<name>, or give --record"))
@@ -0,0 +1,183 @@
1
+ import copy
2
+ import json
3
+ import os
4
+ import shlex
5
+ import shutil
6
+ import sys
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ from ruamel.yaml.error import YAMLError
11
+
12
+ from kalfa.commands.sweep.points import LOGS, POINTS_FILE, SWEEP_FILE
13
+ from kalfa.commands.sweep.space import is_int
14
+ from kalfa.commands.sweep.strategies import strategy_note, strategy_of
15
+ from kalfa.core import run as running
16
+ from kalfa.core.check import check
17
+ from kalfa.core.errors import KalfaError, ProblemsError
18
+ from kalfa.core.record import atomic, now
19
+ from kalfa.core.spec import parse_value
20
+ from kalfa.records import reader
21
+
22
+
23
+ MAPPING_NOTE = "# mapping config: run through the python api"
24
+
25
+
26
+ @dataclass
27
+ class Plan:
28
+ paths: tuple
29
+ sets: tuple
30
+ params: tuple
31
+ root: str
32
+ strategy: object
33
+ space: dict
34
+ objective: dict
35
+ points: list | None = None
36
+ config_name: str = "sweep"
37
+ extra: dict = field(default_factory=dict)
38
+
39
+ @property
40
+ def count(self):
41
+ return None if self.points is None else len(self.points)
42
+
43
+ @property
44
+ def adaptive(self):
45
+ return self.points is None
46
+
47
+
48
+ def objective_note(raw):
49
+ raw = raw if isinstance(raw, dict) else {}
50
+ return {"watch": raw.get("watch"), "mode": raw.get("mode") or "min", "at": raw.get("at") or "best"}
51
+
52
+
53
+ def plan_seed(seed):
54
+ return int(seed) if is_int(seed) else 0
55
+
56
+
57
+ def plan_of(paths=(), sets=(), params=(), mappings=(), record=None):
58
+ paths = tuple(str(path) for path in paths)
59
+ sets = tuple(running.pairs(sets))
60
+ params = tuple(running.pairs(params, "param"))
61
+ mappings = list(mappings)
62
+ checked = check(paths=paths, sets=list(sets), params=list(params), mappings=mappings)
63
+ problems = [problem for problem in checked.problems if record is None or problem.code != "sweep_record"]
64
+ if any(problem.severity == "error" for problem in problems):
65
+ raise ProblemsError(problems)
66
+ raw = checked.tree.settings.get("sweep") if checked.tree is not None else None
67
+ if not isinstance(raw, dict):
68
+ raise KalfaError("the config has no sweep section; write sweep: {strategy, space, objective, record}")
69
+ seed = plan_seed(checked.config.data.get("seed"))
70
+ strategy = strategy_of(raw.get("strategy"))
71
+ if getattr(strategy, "seed", 0) is None:
72
+ strategy.seed = seed
73
+ space = copy.deepcopy(raw["space"])
74
+ plan = Plan(paths, sets, params, str(record) if record is not None else str(raw["record"]), strategy, space,
75
+ objective_note(raw.get("objective")), config_name=Path(paths[0]).stem if paths else "sweep",
76
+ extra={"mappings": mappings, "seed": seed, "strategy": strategy_note(raw.get("strategy"))})
77
+ if callable(getattr(strategy, "points", None)):
78
+ plan.points = list(strategy.points(space, seed))
79
+ return plan
80
+
81
+
82
+ def value_text(value):
83
+ if isinstance(value, str):
84
+ try:
85
+ if parse_value(value) == value:
86
+ return value
87
+ except YAMLError:
88
+ pass
89
+ return json.dumps(value, default=str)
90
+
91
+
92
+ def command_args(plan, index=None, absolute=False, repair=False):
93
+ paths = [os.path.abspath(path) if absolute else path for path in plan.paths] or ["CONFIG"]
94
+ args = ["sweep", *paths]
95
+ for path, value in plan.sets:
96
+ args += ["--set", f"{path}={value_text(value)}"]
97
+ for name, value in plan.params:
98
+ args += ["-p", f"{name}={value_text(value)}"]
99
+ args += ["--record", os.path.abspath(plan.root) if absolute else plan.root]
100
+ if index is not None:
101
+ args += ["--point", str(index)]
102
+ if repair:
103
+ args.append("--repair")
104
+ args.append("--quiet")
105
+ return args
106
+
107
+
108
+ def script_text(plan):
109
+ indices = [None] if plan.adaptive else list(range(plan.count))
110
+ lines = [shlex.join(["kalfa", *command_args(plan, index, absolute=True)]) for index in indices]
111
+ if not plan.paths:
112
+ return "\n".join(["#!/usr/bin/env bash", MAPPING_NOTE, *(f"# {line}" for line in lines)]) + "\n"
113
+ repair = shlex.join(["kalfa", *command_args(plan, absolute=True, repair=True)])
114
+ return "\n".join(["#!/usr/bin/env bash", "set -uo pipefail", "status=0", *(f"{line} || status=1" for line in lines),
115
+ "# a point that fails leaves the others running; this reruns the failed and missing points:",
116
+ f"# {repair}", 'exit "$status"']) + "\n"
117
+
118
+
119
+ def condor_arg(text):
120
+ text = str(text).replace('"', '""')
121
+ if any(character.isspace() or character == "'" for character in text):
122
+ return "'" + text.replace("'", "''") + "'"
123
+ return text
124
+
125
+
126
+ def executable():
127
+ found = shutil.which("kalfa")
128
+ return (found, []) if found else (sys.executable, ["-m", "kalfa.cli"])
129
+
130
+
131
+ def submit_text(plan):
132
+ index = None if plan.adaptive else "$(Process)"
133
+ program, prefix = executable()
134
+ arguments = " ".join(condor_arg(arg) for arg in [*prefix, *command_args(plan, index, absolute=True)])
135
+ logs = os.path.join(os.path.abspath(plan.root), LOGS)
136
+ lines = [f"executable = {program}", f'arguments = "{arguments}"', "getenv = True",
137
+ f"output = {logs}/$(Process).out", f"error = {logs}/$(Process).err", f"log = {logs}/sweep.log",
138
+ f"queue {1 if plan.adaptive else plan.count}"]
139
+ if not plan.paths:
140
+ lines = [MAPPING_NOTE, *(f"# {line}" for line in lines)]
141
+ return "\n".join(lines) + "\n"
142
+
143
+
144
+ def write_text(path, text):
145
+ with atomic(path) as temporary:
146
+ temporary.write_text(text, encoding="utf-8")
147
+
148
+
149
+ def points_text(points):
150
+ return "".join(json.dumps({"id": index, "values": values}, default=str) + "\n"
151
+ for index, values in enumerate(points or []))
152
+
153
+
154
+ def previous_note(root):
155
+ try:
156
+ found = reader.read_json(root, SWEEP_FILE)
157
+ except (OSError, ValueError):
158
+ return {}
159
+ return found if isinstance(found, dict) else {}
160
+
161
+
162
+ def write_plan(plan, log=print):
163
+ root = Path(plan.root)
164
+ root.mkdir(parents=True, exist_ok=True)
165
+ (root / LOGS).mkdir(exist_ok=True)
166
+ previous = previous_note(root)
167
+ note = {"format": 1, "strategy": plan.extra.get("strategy") or {"use": type(plan.strategy).__name__.lower()},
168
+ "space": plan.space, "objective": plan.objective, "points": plan.count}
169
+ if plan.adaptive:
170
+ note["trials"] = getattr(plan.strategy, "trials", None)
171
+ note["started"] = previous.get("started") or now()
172
+ note["config_files"] = [os.path.abspath(path) for path in plan.paths]
173
+ note["record"] = plan.root
174
+ written = [root / SWEEP_FILE, root / POINTS_FILE, root / "sweep.sh", root / "sweep.sub"]
175
+ write_text(written[0], json.dumps(note, indent=1, default=str) + "\n")
176
+ if not plan.adaptive or not written[1].exists():
177
+ write_text(written[1], points_text(plan.points))
178
+ write_text(written[2], script_text(plan))
179
+ written[2].chmod(0o755)
180
+ write_text(written[3], submit_text(plan))
181
+ size = f"{plan.count} points" if not plan.adaptive else f"adaptive, {note['trials']} trials"
182
+ log(f"{plan.root}: {size}; " + ", ".join(path.name for path in written))
183
+ return [str(path) for path in written]
@@ -0,0 +1,123 @@
1
+ import json
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ from kalfa.commands.sweep.space import is_int
6
+ from kalfa.core.errors import KalfaError
7
+ from kalfa.core.record import STOP_FILE
8
+ from kalfa.records import reader
9
+ from kalfa.records.sweep import objective_of, point_dirs
10
+
11
+
12
+ REPAIRED = ("failed", "stopped")
13
+ OUTSIDE_STOPS = ("keyboard", "stop.json")
14
+ SWEEP_FILE = "sweep.json"
15
+ POINTS_FILE = "points.jsonl"
16
+ LOGS = "logs"
17
+
18
+
19
+ def point_dir(root, index):
20
+ return f"{root}/{index:04d}"
21
+
22
+
23
+ def manifest_of(directory):
24
+ try:
25
+ found = reader.read_manifest(directory)
26
+ except (OSError, ValueError):
27
+ return None
28
+ return found if isinstance(found, dict) else None
29
+
30
+
31
+ def stopped_by_rule(directory):
32
+ ends = [line for line in reader.read_lines(Path(directory) / "events.jsonl") if line.get("kind") == "run_end"]
33
+ stopping = ends[-1].get("stopping") if ends else None
34
+ return isinstance(stopping, str) and not stopping.startswith(OUTSIDE_STOPS)
35
+
36
+
37
+ def complete(directory, manifest=None):
38
+ manifest = manifest if manifest is not None else manifest_of(directory)
39
+ if manifest is None:
40
+ return False
41
+ status = manifest.get("status")
42
+ return status == "done" or (status == "stopped" and stopped_by_rule(directory))
43
+
44
+
45
+ def abandoned(directory, manifest):
46
+ return reader.state_of(manifest) == "stale" and (Path(directory) / STOP_FILE).is_file()
47
+
48
+
49
+ def repairable(directory):
50
+ manifest = manifest_of(directory)
51
+ if manifest is None:
52
+ return True
53
+ if manifest.get("status") == "running":
54
+ return abandoned(directory, manifest)
55
+ return manifest.get("status") in REPAIRED and not complete(directory, manifest)
56
+
57
+
58
+ def clear_point(directory, index):
59
+ if not directory.is_dir() or not any(directory.iterdir()):
60
+ return
61
+ manifest = manifest_of(directory)
62
+ if manifest is not None and complete(directory, manifest):
63
+ raise KalfaError(f"point {index} is {manifest.get('status')} ({directory}); --repair runs only the failed "
64
+ f"and missing points")
65
+ if manifest is not None and manifest.get("status") == "running" and not abandoned(directory, manifest):
66
+ if reader.state_of(manifest) == "stale":
67
+ raise KalfaError(f"point {index} was running and its heartbeat is stale ({directory}); if its process is "
68
+ f"dead, kalfa stop {directory} marks it and --repair runs it again, or remove the "
69
+ f"directory")
70
+ raise KalfaError(f"point {index} is running ({directory}); kalfa stop {directory} asks it to stop after its "
71
+ f"turn")
72
+ shutil.rmtree(directory)
73
+
74
+
75
+ def recorded_values(plan):
76
+ found = {}
77
+ for directory in point_dirs(plan.root):
78
+ manifest = manifest_of(directory)
79
+ sweep = manifest.get("sweep") if manifest is not None else None
80
+ if isinstance(sweep, dict) and isinstance(sweep.get("values"), dict):
81
+ found[int(directory.name)] = dict(sweep["values"])
82
+ for line in reader.read_lines(Path(plan.root) / POINTS_FILE):
83
+ if is_int(line.get("id")) and isinstance(line.get("values"), dict):
84
+ found[line["id"]] = dict(line["values"])
85
+ return found
86
+
87
+
88
+ def planned_values(plan, index):
89
+ if plan.points is not None:
90
+ if not 0 <= index < len(plan.points):
91
+ raise KalfaError(f"point {index} is not in the plan of {plan.root}, which has {len(plan.points)} points")
92
+ return plan.points[index]
93
+ found = recorded_values(plan).get(index)
94
+ if found is None:
95
+ raise KalfaError(f"{plan.root}: the strategy is adaptive and point {index} was never asked; run the sweep "
96
+ f"without --point")
97
+ return found
98
+
99
+
100
+ def next_index(plan):
101
+ known = [int(directory.name) for directory in point_dirs(plan.root)]
102
+ known += [line["id"] for line in reader.read_lines(Path(plan.root) / POINTS_FILE) if is_int(line.get("id"))]
103
+ return max(known) + 1 if known else 0
104
+
105
+
106
+ def append_point(root, index, values):
107
+ with open(Path(root) / POINTS_FILE, "a", encoding="utf-8") as handle:
108
+ handle.write(json.dumps({"id": index, "values": values}, default=str) + "\n")
109
+
110
+
111
+ def recorded_points(plan):
112
+ points = []
113
+ for index, values in sorted(recorded_values(plan).items()):
114
+ directory = Path(point_dir(plan.root, index))
115
+ summary = reader.summary_of(directory) if directory.is_dir() else None
116
+ failed = not isinstance(summary, dict) or summary.get("status") == "failed"
117
+ points.append((values, None if failed else objective_of(summary, plan.objective)))
118
+ return points
119
+
120
+
121
+ def failed_points(plan):
122
+ indices = range(len(plan.points)) if plan.points is not None else sorted(recorded_values(plan))
123
+ return [index for index in indices if repairable(Path(point_dir(plan.root, index)))]
@@ -0,0 +1,100 @@
1
+ import logging
2
+ import subprocess
3
+ import sys
4
+ import threading
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from pathlib import Path
7
+
8
+ from kalfa.commands.sweep.plan import command_args, write_plan
9
+ from kalfa.commands.sweep.points import (
10
+ LOGS,
11
+ OUTSIDE_STOPS,
12
+ SWEEP_FILE,
13
+ clear_point,
14
+ manifest_of,
15
+ planned_values,
16
+ point_dir,
17
+ recorded_points,
18
+ stopped_by_rule,
19
+ )
20
+ from kalfa.commands.sweep.space import is_int
21
+ from kalfa.core import run as running
22
+ from kalfa.records import reader
23
+
24
+
25
+ LOGGER = logging.getLogger("kalfa.commands")
26
+
27
+
28
+ def silent(text):
29
+ return None
30
+
31
+
32
+ def say_of(monitor):
33
+ say = getattr(monitor, "say", None)
34
+ return say if callable(say) else silent
35
+
36
+
37
+ def run_point(plan, index, values=None, monitor=None):
38
+ index = int(index)
39
+ if values is None:
40
+ values = planned_values(plan, index)
41
+ if not (Path(plan.root) / SWEEP_FILE).exists():
42
+ write_plan(plan, log=say_of(monitor))
43
+ directory = Path(point_dir(plan.root, index))
44
+ clear_point(directory, index)
45
+ return running.run(paths=plan.paths, sets=[*plan.sets, ("sweep.record", plan.root)],
46
+ params=[*plan.params, *values.items()], mappings=plan.extra.get("mappings", ()),
47
+ record=str(directory), monitor=monitor,
48
+ sweep={"root": plan.root, "id": index, "values": values})
49
+
50
+
51
+ def values_text(values):
52
+ return " ".join(f"{key}={value}" for key, value in values.items())
53
+
54
+
55
+ def interrupted(result):
56
+ stopping = getattr(getattr(result, "context", None), "stopping", None)
57
+ return isinstance(stopping, str) and stopping.startswith(OUTSIDE_STOPS)
58
+
59
+
60
+ def child(plan, index, logs, halt):
61
+ if halt.is_set():
62
+ return index, None
63
+ command = [sys.executable, "-m", "kalfa.cli", *command_args(plan, index)]
64
+ with open(logs / f"{index:04d}.log", "w", encoding="utf-8") as out:
65
+ code = subprocess.run(command, stdout=out, stderr=subprocess.STDOUT, check=False).returncode
66
+ directory = Path(point_dir(plan.root, index))
67
+ manifest = manifest_of(directory)
68
+ if code == 0 and manifest is not None and manifest.get("status") == "stopped" and not stopped_by_rule(directory):
69
+ halt.set()
70
+ return index, code
71
+
72
+
73
+ def in_children(plan, indices, jobs, log):
74
+ logs = Path(plan.root) / LOGS
75
+ logs.mkdir(parents=True, exist_ok=True)
76
+ halt = threading.Event()
77
+ results, skipped = [], 0
78
+ with ThreadPoolExecutor(max_workers=jobs) as pool:
79
+ for index, code in pool.map(lambda index: child(plan, index, logs, halt), indices):
80
+ directory = point_dir(plan.root, index)
81
+ output = logs / f"{index:04d}.log"
82
+ if code is None:
83
+ skipped += 1
84
+ elif code == 0:
85
+ results.append(running.Result(record=directory, history=reader.read_history(directory), context=None))
86
+ else:
87
+ LOGGER.warning("point %04d failed (exit %s); its output is in %s", index, code, output)
88
+ results.append(None)
89
+ if skipped:
90
+ log(f"a point was stopped from outside; {skipped} points did not start and --repair runs them")
91
+ return results
92
+
93
+
94
+ def resumed(plan):
95
+ resume = getattr(plan.strategy, "resume", None)
96
+ if not callable(resume):
97
+ return None
98
+ points = recorded_points(plan)
99
+ told = resume(plan.space, plan.objective, points)
100
+ return told if is_int(told) else len(points)
@@ -0,0 +1,67 @@
1
+ import copy
2
+ import math
3
+
4
+ import numpy
5
+
6
+ from kalfa.core.errors import BuildError
7
+ from kalfa.records import reader
8
+
9
+
10
+ RANGES = ("log", "float", "int")
11
+ SPACE_HINT = "a param maps to a list of values or to {log: [a, b]}, {float: [a, b]} or {int: [a, b]}"
12
+
13
+
14
+ def is_int(value):
15
+ return isinstance(value, int) and not isinstance(value, bool)
16
+
17
+
18
+ def spread(kind, bounds, steps):
19
+ low, high = bounds
20
+ grid = numpy.geomspace(low, high, steps) if kind == "log" else numpy.linspace(low, high, steps)
21
+ if kind == "int":
22
+ return list(dict.fromkeys(round(float(value)) for value in grid))
23
+ return [float(f"{float(value):.12g}") for value in grid]
24
+
25
+
26
+ def draw(generator, kind, values):
27
+ if kind == "list":
28
+ return copy.deepcopy(values[int(generator.integers(len(values)))])
29
+ low, high = values
30
+ if kind == "int":
31
+ return int(generator.integers(low, high + 1))
32
+ if kind == "log":
33
+ return float(min(max(math.exp(generator.uniform(math.log(low), math.log(high))), low), high))
34
+ return float(generator.uniform(low, high))
35
+
36
+
37
+ def space_values(space):
38
+ if not isinstance(space, dict) or not space:
39
+ raise BuildError(f"sweep.space must be a mapping of params to values; {SPACE_HINT}")
40
+ return {str(name): dimension(name, entry) for name, entry in space.items()}
41
+
42
+
43
+ def dimension(name, entry):
44
+ if isinstance(entry, (list, tuple)):
45
+ if not entry:
46
+ raise BuildError(f"sweep.space.{name}: the list of values is empty")
47
+ return "list", list(entry)
48
+ if isinstance(entry, dict) and len(entry) == 1:
49
+ kind, bounds = next(iter(entry.items()))
50
+ if kind in RANGES:
51
+ return kind, bounds_of(name, kind, bounds)
52
+ raise BuildError(f"sweep.space.{name}: {entry!r} is neither a list of values nor a range; {SPACE_HINT}")
53
+
54
+
55
+ def bounds_of(name, kind, bounds):
56
+ if not isinstance(bounds, (list, tuple)) or len(bounds) != 2 or not all(map(reader.is_finite, bounds)):
57
+ raise BuildError(f"sweep.space.{name}: {kind} takes two numbers [low, high], got {bounds!r}")
58
+ low, high = bounds
59
+ if kind == "int" and not (is_int(low) and is_int(high)):
60
+ raise BuildError(f"sweep.space.{name}: int takes two whole numbers [low, high], got {list(bounds)!r}")
61
+ if low > high:
62
+ raise BuildError(f"sweep.space.{name}: {kind} [{low}, {high}] has its low end above its high end")
63
+ if kind == "log" and low <= 0:
64
+ raise BuildError(f"sweep.space.{name}: log [{low}, {high}] needs a low end above zero")
65
+ if kind == "int":
66
+ return [int(low), int(high)]
67
+ return [float(low), float(high)]