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
kalfa/std/common.py ADDED
@@ -0,0 +1,56 @@
1
+ import logging
2
+ import numbers
3
+ from contextlib import contextmanager
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+ from kalfa.core.errors import BuildError
9
+
10
+
11
+ logger = logging.getLogger("kalfa.std")
12
+
13
+
14
+ def positive_int(value, what):
15
+ if isinstance(value, bool) or not isinstance(value, numbers.Integral) or value < 1:
16
+ raise BuildError(f"{what} must be a positive integer, got {value!r}")
17
+ return int(value)
18
+
19
+
20
+ def numeric(value):
21
+ return isinstance(value, numbers.Real) and not isinstance(value, bool)
22
+
23
+
24
+ @contextmanager
25
+ def evaluating(model):
26
+ modules = list(model.modules()) if isinstance(model, nn.Module) else []
27
+ modes = [module.training for module in modules]
28
+ if modules:
29
+ model.eval()
30
+ try:
31
+ with torch.no_grad():
32
+ yield
33
+ finally:
34
+ for module, mode in zip(modules, modes):
35
+ module.training = mode
36
+
37
+
38
+ def classes_last_of(pred, target, choice, owner, label):
39
+ if choice is not None:
40
+ return bool(choice)
41
+ if pred.ndim < 3 or target.ndim != pred.ndim - 1 or pred.shape[0] != target.shape[0]:
42
+ return None
43
+ last = tuple(pred.shape[1:-1]) == tuple(target.shape[1:])
44
+ first = tuple(pred.shape[2:]) == tuple(target.shape[1:])
45
+ if last and first:
46
+ if not getattr(owner, "ambiguous_warned", False):
47
+ owner.ambiguous_warned = True
48
+ logger.warning(f"{label}: logits of shape {tuple(pred.shape)} fit both (batch, tokens, vocabulary) and "
49
+ f"(batch, vocabulary, tokens); the vocabulary is taken in dim 1 as torch does; write "
50
+ f"classes_last: true for (batch, tokens, vocabulary)")
51
+ return False
52
+ if last:
53
+ return True
54
+ if first:
55
+ return False
56
+ return None
@@ -0,0 +1 @@
1
+ from . import images, stream, table, text, transforms, window # noqa: F401
kalfa/std/data/base.py ADDED
@@ -0,0 +1,45 @@
1
+ from torch.utils.data import DataLoader, IterableDataset
2
+
3
+ from kalfa.core.errors import BuildError
4
+
5
+
6
+ def config_directories(ctx):
7
+ return tuple(getattr(getattr(ctx, "config", None), "directories", None) or ())
8
+
9
+
10
+ def seeded_split(split, ctx):
11
+ seed = getattr(ctx, "seed", None)
12
+ if getattr(split, "seed", 0) is None and seed is not None:
13
+ split.seed = seed
14
+ return split
15
+
16
+
17
+ class Loaders:
18
+ def make_loader(self, dataset, train):
19
+ spec = self.batch
20
+ if isinstance(dataset, IterableDataset):
21
+ return DataLoader(dataset, batch_size=spec["size"] if train else (spec["eval_size"] or spec["size"]),
22
+ shuffle=False, drop_last=train and spec["drop_last"], num_workers=spec["workers"])
23
+ if train:
24
+ shuffle = spec["shuffle"]
25
+ generator = self.rng.stream("loader") if shuffle and self.rng is not None else None
26
+ return DataLoader(dataset, batch_size=spec["size"], shuffle=shuffle, drop_last=spec["drop_last"],
27
+ num_workers=spec["workers"], generator=generator)
28
+ return DataLoader(dataset, batch_size=spec["eval_size"] or spec["size"], shuffle=False,
29
+ num_workers=spec["workers"])
30
+
31
+ def loaders(self):
32
+ if not self.datasets:
33
+ raise BuildError("the data is not fitted; call fit(ctx) before loaders()")
34
+ if self.loaders_by_set is None:
35
+ self.loaders_by_set = {name: self.make_loader(dataset, name == "train")
36
+ for name, dataset in self.datasets.items()}
37
+ return dict(self.loaders_by_set)
38
+
39
+ def rows(self, set_name):
40
+ if set_name not in self.datasets:
41
+ raise BuildError(f"there is no {set_name!r} set; the sets are {list(self.datasets)}")
42
+ rows = getattr(self.datasets[set_name], "rows", None)
43
+ if rows is None:
44
+ raise BuildError(f"the {set_name} set is streamed and has no row table")
45
+ return rows
@@ -0,0 +1,190 @@
1
+ import fnmatch
2
+ from dataclasses import dataclass, field
3
+
4
+ import numpy
5
+ import torch
6
+
7
+ from kalfa.core import aliases
8
+ from kalfa.core.coerce import coerced
9
+ from kalfa.core.errors import BuildError, error
10
+ from kalfa.std.data import pre
11
+
12
+
13
+ def is_pattern(text):
14
+ return any(character in text for character in "*?[")
15
+
16
+
17
+ def matches(column, pattern):
18
+ return fnmatch.fnmatchcase(column, pattern) if is_pattern(pattern) else column == pattern
19
+
20
+
21
+ def matches_any(column, patterns):
22
+ return any(matches(column, pattern) for pattern in patterns)
23
+
24
+
25
+ def specificity(pattern):
26
+ return sum(1 for character in pattern if character not in "*?["), pattern.count("?")
27
+
28
+
29
+ def owners_of(columns, patterns):
30
+ owners = {}
31
+ for column in columns:
32
+ claims = [pattern for pattern in patterns if matches(column, pattern)]
33
+ if not claims:
34
+ continue
35
+ literal = [pattern for pattern in claims if not is_pattern(pattern)]
36
+ if literal:
37
+ owners[column] = literal[0]
38
+ continue
39
+ ranked = sorted(claims, key=specificity, reverse=True)
40
+ if len(ranked) > 1 and specificity(ranked[0]) == specificity(ranked[1]):
41
+ raise BuildError(f"column {column!r} matches the features patterns {ranked[0]!r} and {ranked[1]!r} "
42
+ f"with equal specificity; make one of them more specific")
43
+ owners[column] = ranked[0]
44
+ return owners
45
+
46
+
47
+ def hint_of(name):
48
+ return aliases.suggestion(name) if isinstance(name, str) and not aliases.is_import_path(name) else None
49
+
50
+
51
+ def target_of(name, where=None):
52
+ try:
53
+ return aliases.import_target(aliases.resolve(name))
54
+ except BuildError as failure:
55
+ hint = hint_of(name)
56
+ place = f"{where}: " if where else ""
57
+ raise BuildError(f"{place}{failure}" + (f" ({hint})" if hint else "")) from None
58
+
59
+
60
+ def name_problems(spec, where, problems):
61
+ for item in spec if isinstance(spec, (list, tuple)) else [spec]:
62
+ if not isinstance(item, str):
63
+ continue
64
+ try:
65
+ found = aliases.resolve(item)
66
+ except BuildError as failure:
67
+ problems.append(error("unknown_use", str(failure), where, hint_of(item)))
68
+ continue
69
+ try:
70
+ aliases.import_target(found)
71
+ except BuildError as failure:
72
+ problems.append(error("import_failed", str(failure), where))
73
+
74
+
75
+ def step_of(spec, where):
76
+ if isinstance(spec, str):
77
+ return target_of(spec, where)()
78
+ if isinstance(spec, dict):
79
+ if "use" not in spec:
80
+ raise BuildError(f"{where}: a preprocessor call needs use, got the keys {sorted(spec)}")
81
+ params = {key: value for key, value in spec.items() if key != "use"}
82
+ target = target_of(spec["use"], where)
83
+ return target(**coerced(target, params))
84
+ if hasattr(spec, "transform"):
85
+ return spec
86
+ raise BuildError(f"{where}: a preprocessor is a short name, an import path, a call or an object with "
87
+ f"transform, got {type(spec).__name__}")
88
+
89
+
90
+ def chain_of(spec, where):
91
+ if spec is None:
92
+ return []
93
+ items = list(spec) if isinstance(spec, (list, tuple)) else [spec]
94
+ return [step_of(item, where) for item in items]
95
+
96
+
97
+ def as_matrix(values):
98
+ array = numpy.asarray(values)
99
+ if array.ndim == 1:
100
+ return array.reshape(-1, 1)
101
+ if array.ndim > 2:
102
+ return array.reshape(len(array), -1)
103
+ return array
104
+
105
+
106
+ def apply_step(step, values, where, fit):
107
+ try:
108
+ if fit and hasattr(step, "fit"):
109
+ step.fit(values)
110
+ return as_matrix(step.transform(values))
111
+ except (ValueError, TypeError) as error:
112
+ raise BuildError(f"{where}: {type(step).__name__} failed: {error}") from error
113
+
114
+
115
+ def names_after(step, names, width):
116
+ if hasattr(step, "get_feature_names_out"):
117
+ try:
118
+ return [str(name) for name in step.get_feature_names_out(list(names))]
119
+ except (ValueError, TypeError, AttributeError):
120
+ pass
121
+ if width == len(names):
122
+ return list(names)
123
+ return [f"{'_'.join(names)}_{position}" for position in range(width)]
124
+
125
+
126
+ def floats_of(values, where):
127
+ try:
128
+ return numpy.asarray(values, dtype="float32")
129
+ except (ValueError, TypeError) as error:
130
+ raise BuildError(f"{where}: the values are not numeric after the chain ({error}); add one_hot, label or "
131
+ f"cast") from error
132
+
133
+
134
+ def tensor_of(array, dtype):
135
+ out = numpy.ascontiguousarray(array, dtype=dtype)
136
+ if not out.flags.writeable:
137
+ out = out.copy()
138
+ return torch.from_numpy(out)
139
+
140
+
141
+ def step_names(chain):
142
+ return [type(step).__name__ for step in chain]
143
+
144
+
145
+ def inverts(step):
146
+ return hasattr(step, "inverse_transform") and bool(getattr(step, "rescales", True))
147
+
148
+
149
+ def label_step(chain):
150
+ for step in chain:
151
+ if isinstance(step, pre.Label):
152
+ return step
153
+ return None
154
+
155
+
156
+ def mapping_of(spec, what):
157
+ if spec is None:
158
+ return {}
159
+ if not isinstance(spec, dict):
160
+ raise BuildError(f"{what} is a mapping of column names or patterns to preprocessors, got "
161
+ f"{type(spec).__name__}")
162
+ return dict(spec)
163
+
164
+
165
+ def names_of(spec, what):
166
+ if spec is None:
167
+ return []
168
+ if isinstance(spec, str):
169
+ return [spec]
170
+ if isinstance(spec, (list, tuple)) and all(isinstance(item, str) for item in spec):
171
+ return list(spec)
172
+ raise BuildError(f"{what} is a column name or a list of column names, got {spec!r}")
173
+
174
+
175
+ @dataclass
176
+ class Block:
177
+ pattern: str
178
+ columns: list
179
+ chain: list
180
+ names: list = field(default_factory=list)
181
+
182
+
183
+ @dataclass
184
+ class Target:
185
+ name: str
186
+ chain: list
187
+ label: object = None
188
+ width: int = 1
189
+ classes: list | None = None
190
+ class_weights: object = None
kalfa/std/data/flow.py ADDED
@@ -0,0 +1,317 @@
1
+ import math
2
+ from pathlib import Path
3
+
4
+ from kalfa.core import aliases
5
+ from kalfa.std.data import splits
6
+
7
+
8
+ LISTED = 6
9
+ CLIP = 90
10
+ SCALARS = (bool, int, float, str)
11
+ LABELS = ("name", "path")
12
+
13
+
14
+ def alias_of(target, slot=None):
15
+ kind = target if isinstance(target, type) else type(target)
16
+ path = aliases.target_path(kind)
17
+ found = [name for name, entry in aliases.ALIASES.items() if aliases.target_path(entry.target) == path]
18
+ preferred = [name for name in found if slot is None or aliases.ALIASES[name].slot == slot]
19
+ chosen = preferred or found
20
+ return chosen[0] if chosen else None
21
+
22
+
23
+ def kind_of(data):
24
+ return alias_of(data, "data") or aliases.target_path(type(data))
25
+
26
+
27
+ def jsonable(value):
28
+ if isinstance(value, dict):
29
+ return {str(key): jsonable(item) for key, item in value.items()}
30
+ if isinstance(value, (list, tuple, set, frozenset)):
31
+ return [jsonable(item) for item in value]
32
+ if value is None or isinstance(value, (bool, str)):
33
+ return value
34
+ if isinstance(value, int):
35
+ return int(value)
36
+ if isinstance(value, float):
37
+ return float(value) if math.isfinite(value) else None
38
+ if isinstance(value, Path):
39
+ return str(value)
40
+ if callable(getattr(value, "tolist", None)):
41
+ return jsonable(value.tolist())
42
+ return str(value)
43
+
44
+
45
+ def number(value):
46
+ if value is None or isinstance(value, bool):
47
+ return None
48
+ try:
49
+ return int(value)
50
+ except (TypeError, ValueError):
51
+ return None
52
+
53
+
54
+ def amount(value):
55
+ value = number(value)
56
+ if value is None:
57
+ return "?"
58
+ if abs(value) < 10000:
59
+ return str(value)
60
+ if abs(value) < 1000000:
61
+ return f"{value / 1000:.1f}k".replace(".0k", "k")
62
+ return f"{value / 1000000:.1f}M".replace(".0M", "M")
63
+
64
+
65
+ def clip(text, size=CLIP):
66
+ text = "" if text is None else str(text)
67
+ return text if len(text) <= size else text[:size - 1].rstrip() + "…"
68
+
69
+
70
+ def tail(text, size=60):
71
+ text = str(text)
72
+ return text if len(text) <= size else "…" + text[-(size - 1):]
73
+
74
+
75
+ def listed(names, limit=LISTED):
76
+ names = [str(name) for name in names or ()]
77
+ if len(names) <= limit:
78
+ return ", ".join(names)
79
+ return ", ".join(names[:limit]) + f" and {len(names) - limit} more"
80
+
81
+
82
+ def share(value):
83
+ return f"{round(float(value) * 100, 1):g}%"
84
+
85
+
86
+ def simple(value):
87
+ if isinstance(value, SCALARS):
88
+ return True
89
+ return isinstance(value, (list, tuple)) and len(value) <= 8 and all(isinstance(item, SCALARS) for item in value)
90
+
91
+
92
+ def value_text(value):
93
+ if isinstance(value, bool):
94
+ return str(value).lower()
95
+ if isinstance(value, float):
96
+ return f"{value:.4g}"
97
+ if isinstance(value, (list, tuple)):
98
+ return "[" + ", ".join(value_text(item) for item in value) + "]"
99
+ return str(value)
100
+
101
+
102
+ def params_text(params):
103
+ return ", ".join(f"{key} {value_text(value)}" for key, value in params.items() if simple(value))
104
+
105
+
106
+ def params_of(thing):
107
+ fields = getattr(thing, "__dict__", None)
108
+ if not isinstance(fields, dict):
109
+ return ""
110
+ return params_text({key: value for key, value in fields.items()
111
+ if key not in LABELS and not key.startswith("_") and not key.endswith("_")})
112
+
113
+
114
+ def named(text):
115
+ text = str(text)
116
+ if aliases.is_import_path(text):
117
+ return text.rsplit(".", 1)[-1], text
118
+ return text, ""
119
+
120
+
121
+ def step_label(item):
122
+ if isinstance(item, str):
123
+ return named(item)
124
+ if isinstance(item, dict):
125
+ label, path = named(item.get("use"))
126
+ return label, params_text({key: value for key, value in item.items() if key != "use"}) or path
127
+ return alias_of(item, "value") or type(item).__name__, params_of(item)
128
+
129
+
130
+ def steps_of(spec, chain=()):
131
+ items = [] if spec is None else list(spec) if isinstance(spec, (list, tuple)) else [spec]
132
+ if chain and len(items) != len(chain):
133
+ items = list(chain)
134
+ return [step_label(item) for item in items]
135
+
136
+
137
+ def pipeline_steps(pipeline):
138
+ names = list(getattr(pipeline, "names", None) or ())
139
+ steps = list(getattr(pipeline, "steps", None) or ())
140
+ if len(names) != len(steps):
141
+ return [step_label(step) for step in steps]
142
+ found = []
143
+ for name, step in zip(names, steps):
144
+ label, path = named(name)
145
+ found.append((label, params_of(step) or path))
146
+ return found
147
+
148
+
149
+ class Flow:
150
+ def __init__(self):
151
+ self.nodes = []
152
+ self.edges = []
153
+ self.groups = []
154
+ self.ids = set()
155
+
156
+ def unique(self, key):
157
+ found, position = str(key), 1
158
+ while found in self.ids:
159
+ position += 1
160
+ found = f"{key}#{position}"
161
+ self.ids.add(found)
162
+ return found
163
+
164
+ def node(self, key, kind, label, detail="", count=None, group=None):
165
+ key = self.unique(key)
166
+ self.nodes.append({"id": key, "kind": kind, "label": str(label), "detail": clip(detail), "count": number(count),
167
+ "group": group})
168
+ return key
169
+
170
+ def edge(self, start, end, label=""):
171
+ if start is not None and end is not None and start != end:
172
+ self.edges.append({"from": start, "to": end, "label": str(label or "")})
173
+
174
+ def join(self, starts, end, label=""):
175
+ for start in starts:
176
+ self.edge(start, end, label)
177
+
178
+ def group(self, key, label, detail="", parent=None):
179
+ key = self.unique(key)
180
+ self.groups.append({"id": key, "label": str(label), "detail": clip(detail), "parent": parent})
181
+ return key
182
+
183
+ def step(self, start, key, kind, label, detail="", count=None, group=None, edge=""):
184
+ made = self.node(key, kind, label, detail, count, group)
185
+ self.edge(start, made, edge)
186
+ return made
187
+
188
+ def chain(self, starts, key, steps, kind="chain", label=None, detail=""):
189
+ steps = list(steps)
190
+ if not steps:
191
+ return list(starts)
192
+ group = self.group(f"group:{key}", label or key, detail) if label is not None else None
193
+ ends = list(starts)
194
+ for position, (name, about) in enumerate(steps):
195
+ made = self.node(f"{key}:{position}", kind, name, about, group=group)
196
+ self.join(ends, made)
197
+ ends = [made]
198
+ return ends
199
+
200
+ def result(self):
201
+ return {"nodes": self.nodes, "edges": self.edges, "groups": self.groups}
202
+
203
+
204
+ def source_node(graph, path, count=None, extra=()):
205
+ text = str(path)
206
+ name = Path(text).name or text
207
+ about = ([tail(text)] if text != name else []) + [part for part in extra if part]
208
+ return graph.node("source", "source", name, ", ".join(about), count)
209
+
210
+
211
+ def split_label(split):
212
+ name = alias_of(split, "value")
213
+ return f"{name} split" if name else type(split).__name__
214
+
215
+
216
+ def split_detail(split):
217
+ if isinstance(split, splits.KFoldSplit):
218
+ parts = [f"fold {split.fold} of {split.folds}"]
219
+ if split.valid_ratio:
220
+ parts.append(f"valid {share(split.valid_ratio)} of the rest")
221
+ if split.seed is not None:
222
+ parts.append(f"seed {split.seed}")
223
+ return ", ".join(parts)
224
+ if isinstance(split, splits.GivenSplit):
225
+ return f"by the column {split.column}"
226
+ shares = ", ".join(share(value) for value in getattr(split, "ratios", None) or () if value)
227
+ if isinstance(split, splits.SequentialSplit):
228
+ about = "in file order" + (f", within each {split.group}" if split.group is not None else "")
229
+ elif isinstance(split, splits.RandomSplit):
230
+ about = "shuffled" + (f" with seed {split.seed}" if split.seed is not None else "")
231
+ else:
232
+ return params_of(split)
233
+ return f"{shares}, {about}" if shares else about
234
+
235
+
236
+ def shares_of(split):
237
+ if isinstance(split, splits.KFoldSplit):
238
+ found = {"train": "other folds", "test": f"fold {split.fold}"}
239
+ if split.valid_ratio:
240
+ found["valid"] = f"{share(split.valid_ratio)} of the rest"
241
+ return found
242
+ if isinstance(split, splits.GivenSplit):
243
+ labels = {"train": split.train, "valid": split.valid, "test": split.test}
244
+ return {name: f"{split.column} = {value}" for name, value in labels.items() if value is not None}
245
+ ratios = getattr(split, "ratios", None)
246
+ if isinstance(ratios, (list, tuple)) and len(ratios) == len(splits.SET_NAMES):
247
+ try:
248
+ return {name: share(value) for name, value in zip(splits.SET_NAMES, ratios) if float(value) > 0}
249
+ except (TypeError, ValueError):
250
+ return {}
251
+ return {}
252
+
253
+
254
+ def split_node(graph, start, split):
255
+ return graph.step(start, "split", "split", split_label(split), split_detail(split))
256
+
257
+
258
+ def set_nodes(graph, start, counts, details=None, edges=None):
259
+ details = details or {}
260
+ edges = edges or {}
261
+ return {name: graph.step(start, f"set:{name}", "set", name, details.get(name, ""), count, edge=edges.get(name, ""))
262
+ for name, count in (counts or {}).items()}
263
+
264
+
265
+ def batch_text(batch):
266
+ if not isinstance(batch, dict):
267
+ return ""
268
+ parts = [f"size {batch['size']}"] if batch.get("size") else []
269
+ if batch.get("eval_size"):
270
+ parts.append(f"eval {batch['eval_size']}")
271
+ if batch.get("drop_last"):
272
+ parts.append("drop last")
273
+ return ", ".join(parts)
274
+
275
+
276
+ def batch_node(graph, starts, batch):
277
+ made = graph.node("batch", "batch", "batch", batch_text(batch))
278
+ graph.join(starts, made)
279
+ return made
280
+
281
+
282
+ def feed_node(graph, starts, feed, windows=None):
283
+ label = alias_of(feed, "value") or type(feed).__name__
284
+ count = sum(windows.values()) if windows else None
285
+ made = graph.node("feed", "feed", label, params_of(feed), count)
286
+ graph.join(starts, made)
287
+ return made
288
+
289
+
290
+ def tensor_names(data):
291
+ found = {}
292
+ for name in list(getattr(data, "inputs", None) or ()):
293
+ found.setdefault(str(name), "input")
294
+ for name in list(getattr(data, "target_names", None) or ()):
295
+ found.setdefault(str(name), "target")
296
+ return found
297
+
298
+
299
+ def generic(data):
300
+ reporter = getattr(data, "report", None)
301
+ report = reporter() if callable(reporter) else None
302
+ rows = report.get("rows") if isinstance(report, dict) else None
303
+ graph = Flow()
304
+ source = graph.node("source", "source", type(data).__name__, kind_of(data))
305
+ made = set_nodes(graph, source, rows if isinstance(rows, dict) else {})
306
+ tensors = [graph.step(source, f"tensor:{name}", "tensor", name, role) for name, role in tensor_names(data).items()]
307
+ batch_node(graph, (list(made.values()) or [source]) + tensors, getattr(data, "batch", None))
308
+ return graph.result()
309
+
310
+
311
+ def pipeline_of(data):
312
+ method = getattr(data, "pipeline", None)
313
+ found = method() if callable(method) else None
314
+ if not isinstance(found, dict) or not isinstance(found.get("nodes"), list):
315
+ found = generic(data)
316
+ return jsonable({"nodes": found.get("nodes") or [], "edges": found.get("edges") or [],
317
+ "groups": found.get("groups") or []})