hiplot-plus 0.1.0__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.
hiplot/fetchers.py ADDED
@@ -0,0 +1,322 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # This source code is licensed under the MIT license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ import ast
6
+ import importlib
7
+ import importlib.util
8
+ import itertools
9
+ import json
10
+ import random
11
+ import re
12
+ import typing as tp
13
+ from pathlib import Path
14
+
15
+ from . import experiment as hip
16
+ from .fetchers_demo import README_DEMOS
17
+
18
+
19
+ class NoFetcherFound(Exception):
20
+ def __init__(self, uri: str):
21
+ super().__init__(f"Unable to fetch an HiPlot experiment from '{uri}'")
22
+
23
+
24
+ def load_xp_with_fetchers_partial(fetchers: list[hip.ExperimentFetcher], uri: str) -> tuple[hip.Experiment, int]:
25
+ """
26
+ Attempts to parse an uri.
27
+ Returns a corresponding experiment, and position in uri following the last used character.
28
+
29
+ Example:
30
+ /path1
31
+ /path2
32
+ Returns the Experiment for `/path1` and position the substring `\n/path2`
33
+ """
34
+ eol = uri.find("\n")
35
+ if eol == -1:
36
+ eol = len(uri)
37
+ for f in fetchers:
38
+ try:
39
+ endoffetcher = eol
40
+ get_uri_length: tp.Callable[[str], int] | None = getattr(f, "get_uri_length", None)
41
+ if get_uri_length is not None:
42
+ endoffetcher = get_uri_length(uri)
43
+ return f(uri[:endoffetcher]), endoffetcher
44
+ except hip.ExperimentFetcherDoesntApply:
45
+ continue
46
+ raise NoFetcherFound(uri)
47
+
48
+
49
+ def load_xps_with_fetchers(fetchers: list[hip.ExperimentFetcher], uri: str) -> list[hip.Experiment]:
50
+ uri = uri.lstrip()
51
+ xps: list[hip.Experiment] = []
52
+ while uri:
53
+ xp, endfetcher = load_xp_with_fetchers_partial(fetchers, uri)
54
+ uri = uri[endfetcher:].lstrip()
55
+ xps.append(xp)
56
+ assert xps, uri # If URI is empty, we should raise in `load_xp_with_fetchers_partial`
57
+ return xps
58
+
59
+
60
+ def load_xp_with_fetchers(fetchers: list[hip.ExperimentFetcher], uri: str) -> hip.Experiment:
61
+ xps = load_xps_with_fetchers(fetchers, uri)
62
+ return hip.Experiment.merge({f"{i}": xp for i, xp in enumerate(xps)}) if len(xps) > 1 else xps[0]
63
+
64
+
65
+ class MultipleFetcher:
66
+ MULTI_PREFIX = "multi://"
67
+
68
+ def __init__(self, fetchers: list[hip.ExperimentFetcher]) -> None:
69
+ self.fetchers: list[hip.ExperimentFetcher] = [*fetchers, self]
70
+
71
+ def __call__(self, uri: str) -> hip.Experiment:
72
+ if not uri.startswith(self.MULTI_PREFIX):
73
+ raise hip.ExperimentFetcherDoesntApply()
74
+ defs = json.loads(uri[len(self.MULTI_PREFIX) :])
75
+ if isinstance(defs, list):
76
+ return hip.Experiment.merge({v: load_xp_with_fetchers(self.fetchers, v) for v in defs})
77
+ return hip.Experiment.merge({k: load_xp_with_fetchers(self.fetchers, v) for k, v in defs.items()})
78
+
79
+ def get_uri_length(self, uri: str) -> int:
80
+ """
81
+ Returns the end position of the multi block
82
+ """
83
+ if not uri.startswith(self.MULTI_PREFIX):
84
+ raise hip.ExperimentFetcherDoesntApply()
85
+ decoder = json.JSONDecoder()
86
+ _, current_read_offset = decoder.raw_decode(uri[len(self.MULTI_PREFIX) :])
87
+ return len(self.MULTI_PREFIX) + current_read_offset
88
+
89
+
90
+ class InlineJsonFetcher:
91
+ URI_PREFIX = "json://"
92
+
93
+ def __call__(self, uri: str) -> hip.Experiment:
94
+ if not uri.startswith(self.URI_PREFIX):
95
+ raise hip.ExperimentFetcherDoesntApply()
96
+ exp_rows = json.loads(uri[len(self.URI_PREFIX) :])
97
+ assert isinstance(exp_rows, list)
98
+ return hip.Experiment.from_iterable(exp_rows)
99
+
100
+ def get_uri_length(self, uri: str) -> int:
101
+ """
102
+ Returns the end position of the multi block
103
+ """
104
+ if not uri.startswith(self.URI_PREFIX):
105
+ raise hip.ExperimentFetcherDoesntApply()
106
+ decoder = json.JSONDecoder()
107
+ _, current_read_offset = decoder.raw_decode(uri[len(self.URI_PREFIX) :])
108
+ return len(self.URI_PREFIX) + current_read_offset
109
+
110
+
111
+ def load_demo(uri: str) -> hip.Experiment:
112
+ if uri in README_DEMOS:
113
+ # Keep demo data deterministic across page reloads/tests while preserving
114
+ # the caller's RNG state outside this function.
115
+ random_state = random.getstate()
116
+ try:
117
+ random.seed(f"hiplot-demo:{uri}")
118
+ return README_DEMOS[uri]()
119
+ finally:
120
+ random.setstate(random_state)
121
+ raise hip.ExperimentFetcherDoesntApply()
122
+
123
+
124
+ def load_csv(uri: str) -> hip.Experiment:
125
+ if not uri.endswith(".csv"):
126
+ raise hip.ExperimentFetcherDoesntApply(f"Not a CSV file: {uri}")
127
+ try:
128
+ return hip.Experiment.from_csv(uri)
129
+ except FileNotFoundError as error:
130
+ raise hip.ExperimentFetcherDoesntApply(f"No such file: {uri}") from error
131
+
132
+
133
+ def load_json(uri: str) -> hip.Experiment:
134
+ if not uri.endswith(".json"):
135
+ raise hip.ExperimentFetcherDoesntApply(f"Not a JSON file: {uri}")
136
+ dat = json.loads(Path(uri).read_text(encoding="utf-8"))
137
+ if "job_id" in dat[0] and "kwargs" in dat[0] and "results" in dat[0]:
138
+ # Nevergrad JSON
139
+ return hip.Experiment.from_iterable(
140
+ [
141
+ {
142
+ "id": j["job_id"],
143
+ **{param_name: str(param_val) for param_name, param_val in j["kwargs"].items()},
144
+ **j["results"]["scores"],
145
+ }
146
+ for j in dat
147
+ ]
148
+ )
149
+ return hip.Experiment.from_iterable(dat)
150
+
151
+
152
+ def _load_fairseq_metrics_inline(line: str) -> dict[str, tp.Any]:
153
+ line = line.removeprefix("| epoch ")
154
+ epoch = int(line[:3])
155
+ values: dict[str, tp.Any] = {"epoch": epoch}
156
+ # | epoch 002 | loss 8.413 | ...
157
+ # | epoch 002 | valid on 'valid' subset | loss 7.599 | nll_loss 7.599 | ...
158
+ parts = line.split("|")[1:]
159
+ prefix = ""
160
+ for p in parts:
161
+ p = p.strip()
162
+ match_ds = re.match(r"valid on '([a-zA-Z0-9]*)' subset", p)
163
+ if match_ds is not None:
164
+ prefix = match_ds.group(1) + "_"
165
+ continue
166
+ key = prefix + p[::-1].split(" ", 1)[1][::-1].strip()
167
+ value = p[::-1].split(" ", 1)[0][::-1].strip()
168
+ try:
169
+ values[key] = float(value)
170
+ except ValueError:
171
+ values[key] = value
172
+ return values
173
+
174
+
175
+ def _parse_namespace_keywords(line: str) -> dict[str, tp.Any]:
176
+ call = ast.parse(line, mode="eval").body
177
+ if not isinstance(call, ast.Call):
178
+ raise ValueError(f"Expected a Namespace(...) call, got: {line[:80]}")
179
+ return {kw.arg: ast.literal_eval(kw.value) for kw in call.keywords if kw.arg is not None}
180
+
181
+
182
+ def load_fairseq(uri: str) -> hip.Experiment:
183
+ PREFIX = "fairseq://"
184
+ if not uri.startswith(PREFIX):
185
+ raise hip.ExperimentFetcherDoesntApply()
186
+ uri = uri[len(PREFIX) :]
187
+ train_log = Path(uri)
188
+ if train_log.is_dir():
189
+ found = False
190
+ try_files = [
191
+ *(train_log / f for f in ["train.log", "process.out", "process_0.out"]),
192
+ *train_log.glob("*.log"),
193
+ *(train_log / "slurm_logs").glob("*.log"),
194
+ ]
195
+ for try_log_file in try_files:
196
+ if try_log_file.is_file():
197
+ found = True
198
+ train_log = try_log_file
199
+ break
200
+ if not found:
201
+ raise hip.ExperimentFetcherDoesntApply("No log file found")
202
+ lines = train_log.read_text(encoding="utf-8").split("\n")
203
+
204
+ datapoints: list[dict[str, tp.Any]] = []
205
+ params: dict[str, tp.Any] = {}
206
+ logs_prefix_re = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| [A-Z]* \| )"
207
+ for raw_line in lines:
208
+ # Strip log prefix, eg "2020-03-08 16:48:16 | INFO | "
209
+ m = re.match(logs_prefix_re, raw_line)
210
+ line = raw_line if m is None else raw_line[m.span()[1] :]
211
+ if line.startswith("Namespace("):
212
+ # Format: Namespace(activation_dropout=0.1, activation_fn='relu', ...). Evaluating it as
213
+ # `dict(...)` would run user input, so parse the keywords as literals instead (a too
214
+ # complex string can still exhaust the parser's stack depth).
215
+ params = _parse_namespace_keywords(line)
216
+ continue
217
+ # Results in JSON format
218
+ # valid | {"epoch": 33, "valid_loss": "0.723", "valid_ppl": "1.65", ...}
219
+ if line.startswith("valid | {"):
220
+ json_string = line.split("|", 1)[-1].lstrip()
221
+ valid_metrics = json.loads(json_string)
222
+ datapoints.append(valid_metrics)
223
+ # For older version of fairseq
224
+ if line.startswith("| epoch "):
225
+ values = _load_fairseq_metrics_inline(line)
226
+ if datapoints and datapoints[-1]["epoch"] == values["epoch"]:
227
+ datapoints[-1].update(values)
228
+ else:
229
+ datapoints.append(values)
230
+ datapoints = [
231
+ {
232
+ **params,
233
+ **values, # overrides 'learning rate' for instance
234
+ }
235
+ for values in datapoints
236
+ ]
237
+ datapoints.sort(key=lambda d: float(d["epoch"]))
238
+ xp = hip.Experiment.from_iterable(datapoints)
239
+ for dp, next_dp in itertools.pairwise(xp.datapoints):
240
+ next_dp.from_uid = dp.uid
241
+ return xp
242
+
243
+
244
+ class Wav2letterLoader:
245
+ def _parse_metrics(self, file: Path) -> list[dict[str, tp.Any]]:
246
+ # 001_perf:
247
+ """
248
+ # date\tkey1\tkey2...
249
+ 2019-09-30\tval1\tval2...
250
+ """
251
+ PERF_PREFIX = "perf_"
252
+ lines = file.read_text(encoding="utf-8").split("\n")
253
+ metrics: list[dict[str, tp.Any]] = []
254
+ for line in lines[1:]:
255
+ if line == "":
256
+ continue
257
+ epoch_metrics: dict[str, tp.Any] = {}
258
+ for name, val in zip(lines[0].split()[1:], line.split(), strict=False):
259
+ try:
260
+ epoch_metrics[PERF_PREFIX + name] = float(val)
261
+ except ValueError:
262
+ epoch_metrics[PERF_PREFIX + name] = val
263
+ metrics.append(epoch_metrics)
264
+ return metrics
265
+
266
+ def __call__(self, uri: str) -> hip.Experiment:
267
+ PREFIX = "w2l://"
268
+ if not uri.startswith(PREFIX):
269
+ raise hip.ExperimentFetcherDoesntApply()
270
+ uri = uri[len(PREFIX) :]
271
+ perfs = sorted(Path(uri).glob("*_perf"))
272
+
273
+ prev_ckpt_name: str | None = None
274
+ xp = hip.Experiment()
275
+ for p in perfs:
276
+ mtrics = self._parse_metrics(p)
277
+ for m in mtrics:
278
+ ckpt_name = uri[-5:] + "_" + str(len(xp.datapoints))
279
+ xp.datapoints.append(hip.Datapoint(uid=ckpt_name, from_uid=prev_ckpt_name, values=m))
280
+ prev_ckpt_name = ckpt_name
281
+ return xp
282
+
283
+
284
+ load_wav2letter = Wav2letterLoader()
285
+
286
+
287
+ def _get_module_by_name_in_cwd(name: str) -> tp.Any:
288
+ spec = importlib.util.spec_from_file_location(name, str(Path.cwd() / f"{name}.py"))
289
+ if spec is None or spec.loader is None:
290
+ return None
291
+ module = importlib.util.module_from_spec(spec)
292
+ spec.loader.exec_module(module)
293
+ return module
294
+
295
+
296
+ def get_fetcher(fetcher_spec: str) -> hip.ExperimentFetcher:
297
+ parts = fetcher_spec.split(".")
298
+ try:
299
+ module = importlib.import_module(".".join(parts[:-1]))
300
+ except ModuleNotFoundError:
301
+ if len(parts) != 2:
302
+ raise
303
+ module = _get_module_by_name_in_cwd(parts[0])
304
+ if module is None:
305
+ raise RuntimeError(f"Unable to create fetcher '{fetcher_spec}'")
306
+
307
+ fetcher: hip.ExperimentFetcher = getattr(module, parts[-1])
308
+ return fetcher
309
+
310
+
311
+ def get_fetchers(add_fetchers: list[str]) -> list[hip.ExperimentFetcher]:
312
+ xp_fetchers: list[hip.ExperimentFetcher] = [
313
+ load_demo,
314
+ load_csv,
315
+ load_json,
316
+ load_fairseq,
317
+ load_wav2letter,
318
+ InlineJsonFetcher(),
319
+ ]
320
+ xp_fetchers.extend(get_fetcher(fetcher_spec) for fetcher_spec in add_fetchers)
321
+ xp_fetchers.append(MultipleFetcher(xp_fetchers))
322
+ return xp_fetchers