yuclid 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.
- yuclid/__init__.py +1 -0
- yuclid/cli.py +229 -0
- yuclid/log.py +56 -0
- yuclid/plot.py +1009 -0
- yuclid/run.py +1239 -0
- yuclid/spread.py +152 -0
- yuclid-0.1.0.dist-info/METADATA +15 -0
- yuclid-0.1.0.dist-info/RECORD +11 -0
- yuclid-0.1.0.dist-info/WHEEL +5 -0
- yuclid-0.1.0.dist-info/entry_points.txt +2 -0
- yuclid-0.1.0.dist-info/top_level.txt +1 -0
yuclid/run.py
ADDED
@@ -0,0 +1,1239 @@
|
|
1
|
+
from yuclid.log import LogLevel, report
|
2
|
+
from datetime import datetime
|
3
|
+
import concurrent.futures
|
4
|
+
import pandas as pd
|
5
|
+
import subprocess
|
6
|
+
import itertools
|
7
|
+
import json
|
8
|
+
import re
|
9
|
+
import os
|
10
|
+
|
11
|
+
|
12
|
+
def substitute_point_yvars(x, point_map, point_id):
|
13
|
+
# replace ${yuclid.<name>} and ${yuclid.@} with point values
|
14
|
+
value_pattern = r"\$\{yuclid\.([a-zA-Z0-9_]+)(?:\.value)?\}"
|
15
|
+
name_pattern = r"\$\{yuclid\.([a-zA-Z0-9_]+)\.name\}"
|
16
|
+
y = re.sub(value_pattern, lambda m: str(point_map[m.group(1)]["value"]), x)
|
17
|
+
y = re.sub(name_pattern, lambda m: str(point_map[m.group(1)]["name"]), y)
|
18
|
+
if point_id is not None:
|
19
|
+
value_pattern = r"\$\{yuclid\.\@\}"
|
20
|
+
y = re.sub(value_pattern, lambda m: f"{point_id}.tmp", y)
|
21
|
+
return y
|
22
|
+
|
23
|
+
|
24
|
+
def substitute_global_yvars(x, subspace):
|
25
|
+
# replace ${yuclid.<name>.values} and ${yuclid.<name>.names}
|
26
|
+
subspace_values = {k: [str(x["value"]) for x in v] for k, v in subspace.items()}
|
27
|
+
subspace_names = {k: {x["name"] for x in v} for k, v in subspace.items()}
|
28
|
+
pattern = r"\$\{yuclid\.([a-zA-Z0-9_]+)\.values\}"
|
29
|
+
y = re.sub(pattern, lambda m: " ".join(subspace_values[m.group(1)]), x)
|
30
|
+
pattern = r"\$\{yuclid\.([a-zA-Z0-9_]+)\.names\}"
|
31
|
+
y = re.sub(pattern, lambda m: " ".join(subspace_names[m.group(1)]), y)
|
32
|
+
return y
|
33
|
+
|
34
|
+
|
35
|
+
def get_yvar_pattern():
|
36
|
+
return r"\$\{yuclid\.([a-zA-Z0-9_@]+)\}"
|
37
|
+
|
38
|
+
|
39
|
+
def validate_yvars_in_env(env):
|
40
|
+
for key, value in env.items():
|
41
|
+
if re.search(get_yvar_pattern(), value):
|
42
|
+
hint = (
|
43
|
+
"maybe you meant ${{yuclid.{}.names}} or ${{yuclid.{}.values}}?".format(
|
44
|
+
key, key
|
45
|
+
)
|
46
|
+
)
|
47
|
+
report(
|
48
|
+
LogLevel.FATAL,
|
49
|
+
f"cannot use yuclid point variables in env",
|
50
|
+
value,
|
51
|
+
hint=hint,
|
52
|
+
)
|
53
|
+
|
54
|
+
|
55
|
+
def validate_yvars_in_setup(data):
|
56
|
+
setup = data["setup"]
|
57
|
+
|
58
|
+
# global setup
|
59
|
+
for command in setup["global"]:
|
60
|
+
# match ${yuclid.<name>}
|
61
|
+
# for all matches, check if the name is in on_dims
|
62
|
+
names = re.findall(get_yvar_pattern(), command)
|
63
|
+
for name in names:
|
64
|
+
hint = (
|
65
|
+
"maybe you meant ${{yuclid.{}.names}} or ${{yuclid.{}.values}}?".format(
|
66
|
+
name, name
|
67
|
+
)
|
68
|
+
)
|
69
|
+
report(
|
70
|
+
LogLevel.FATAL,
|
71
|
+
f"cannot use yuclid point variables in global setup",
|
72
|
+
command,
|
73
|
+
hint=hint,
|
74
|
+
)
|
75
|
+
|
76
|
+
# point setup
|
77
|
+
for point_item in setup["point"]:
|
78
|
+
on_dims = point_item["on"] or data["space"].keys()
|
79
|
+
commands = point_item["commands"]
|
80
|
+
for command in commands:
|
81
|
+
# match ${yuclid.(<name>|@)}
|
82
|
+
pattern = r"\$\{yuclid\.([a-zA-Z0-9_@]+)\}"
|
83
|
+
# for all matches, check if the name is in on_dims
|
84
|
+
names = re.findall(pattern, command)
|
85
|
+
for name in names:
|
86
|
+
if name not in on_dims:
|
87
|
+
hint = "available variables: {}".format(
|
88
|
+
", ".join(["${{yuclid.{}}}".format(d) for d in on_dims])
|
89
|
+
)
|
90
|
+
if name == "@":
|
91
|
+
hint = ". ${yuclid.@} is reserved for trial commands"
|
92
|
+
report(
|
93
|
+
LogLevel.FATAL,
|
94
|
+
f"invalid yuclid variable '{name}' in point setup",
|
95
|
+
command,
|
96
|
+
hint=hint,
|
97
|
+
)
|
98
|
+
|
99
|
+
|
100
|
+
def load_json(f):
|
101
|
+
try:
|
102
|
+
return json.load(f)
|
103
|
+
except json.JSONDecodeError as e:
|
104
|
+
report(
|
105
|
+
LogLevel.FATAL,
|
106
|
+
"failed to parse JSON",
|
107
|
+
f.name,
|
108
|
+
f"at line {e.lineno}, column {e.colno}: {e.msg}",
|
109
|
+
)
|
110
|
+
|
111
|
+
|
112
|
+
def aggregate_input_data(settings):
|
113
|
+
data = None
|
114
|
+
|
115
|
+
for file in settings["inputs"]:
|
116
|
+
with open(file, "r") as f:
|
117
|
+
current = normalize_data(load_json(f))
|
118
|
+
if data is None:
|
119
|
+
data = current
|
120
|
+
continue
|
121
|
+
for key, val in current.items():
|
122
|
+
if isinstance(data[key], list):
|
123
|
+
data[key].extend(val)
|
124
|
+
elif isinstance(data[key], dict):
|
125
|
+
if key == "space":
|
126
|
+
for subkey, subval in val.items():
|
127
|
+
if data[key].get(subkey) is None:
|
128
|
+
data[key][subkey] = subval
|
129
|
+
else:
|
130
|
+
data[key].setdefault(subkey, []).extend(subval)
|
131
|
+
else:
|
132
|
+
data[key].update(val)
|
133
|
+
|
134
|
+
order = data.get("order", []) + current.get("order", [])
|
135
|
+
data["order"] = remove_duplicates(order)
|
136
|
+
|
137
|
+
if len(data["trials"]) == 0:
|
138
|
+
report(LogLevel.FATAL, "no valid trials found")
|
139
|
+
|
140
|
+
if len(data["metrics"]) == 0:
|
141
|
+
report(LogLevel.WARNING, "no metrics found. Trials will not be evaluated")
|
142
|
+
|
143
|
+
return data
|
144
|
+
|
145
|
+
|
146
|
+
def remove_duplicates(items):
|
147
|
+
seen = set()
|
148
|
+
result = []
|
149
|
+
for x in items:
|
150
|
+
if x not in seen:
|
151
|
+
seen.add(x)
|
152
|
+
result.append(x)
|
153
|
+
return result
|
154
|
+
|
155
|
+
|
156
|
+
def build_environment(settings, data):
|
157
|
+
if settings["dry_run"]:
|
158
|
+
for key, value in data["env"].items():
|
159
|
+
report(LogLevel.INFO, "dry env", f'{key}="{value}"')
|
160
|
+
env = dict()
|
161
|
+
else:
|
162
|
+
resolved_env = os.environ.copy()
|
163
|
+
for k, v in data["env"].items():
|
164
|
+
expanded = subprocess.run(
|
165
|
+
f'echo "{v}"',
|
166
|
+
env=resolved_env,
|
167
|
+
capture_output=True,
|
168
|
+
text=True,
|
169
|
+
shell=True,
|
170
|
+
).stdout.strip()
|
171
|
+
resolved_env[k] = expanded
|
172
|
+
env = resolved_env
|
173
|
+
return env
|
174
|
+
|
175
|
+
|
176
|
+
def apply_user_selectors(settings, subspace):
|
177
|
+
all_selectors = dict(pair.split("=") for pair in settings["select"])
|
178
|
+
for dim, csv_selection in all_selectors.items():
|
179
|
+
selectors = csv_selection.split(",")
|
180
|
+
if dim not in subspace:
|
181
|
+
report(
|
182
|
+
LogLevel.FATAL,
|
183
|
+
"unknown dimension in selector",
|
184
|
+
dim,
|
185
|
+
hint="available dimensions: {}".format(", ".join(subspace.keys())),
|
186
|
+
)
|
187
|
+
if subspace[dim] is None:
|
188
|
+
selection = [normalize_point(x) for x in selectors]
|
189
|
+
else:
|
190
|
+
selection = []
|
191
|
+
valid = {str(x["name"]) for x in subspace[dim]}
|
192
|
+
for selector in selectors:
|
193
|
+
if selector in valid:
|
194
|
+
for x in subspace[dim]:
|
195
|
+
if x["name"] == selector:
|
196
|
+
selection.append(x)
|
197
|
+
else:
|
198
|
+
report(
|
199
|
+
LogLevel.ERROR,
|
200
|
+
"invalid selector",
|
201
|
+
selector,
|
202
|
+
hint="available: {}".format(", ".join(valid)),
|
203
|
+
)
|
204
|
+
|
205
|
+
if len(selection) == 0:
|
206
|
+
available = (
|
207
|
+
[str(x["name"]) for x in subspace[dim]]
|
208
|
+
if subspace[dim] is not None
|
209
|
+
else []
|
210
|
+
)
|
211
|
+
if len(available) == 0:
|
212
|
+
hint = None
|
213
|
+
else:
|
214
|
+
hint = "pick from the following values: {}".format(
|
215
|
+
", ".join(available)
|
216
|
+
)
|
217
|
+
report(
|
218
|
+
LogLevel.FATAL,
|
219
|
+
"no valid selection for dimension '{}'".format(dim),
|
220
|
+
hint=hint,
|
221
|
+
)
|
222
|
+
subspace[dim] = selection
|
223
|
+
return subspace
|
224
|
+
|
225
|
+
|
226
|
+
def normalize_metrics(metrics):
|
227
|
+
valid = {"name", "command", "condition"}
|
228
|
+
normalized = []
|
229
|
+
if isinstance(metrics, list):
|
230
|
+
for metric in metrics:
|
231
|
+
if not isinstance(metric, dict):
|
232
|
+
report(LogLevel.FATAL, "each metric must be a dict", metric)
|
233
|
+
if "name" not in metric:
|
234
|
+
report(LogLevel.FATAL, "each metric must have a 'name' field", metric)
|
235
|
+
if "command" not in metric:
|
236
|
+
report(
|
237
|
+
LogLevel.FATAL, "each metric must have a 'command' field", metric
|
238
|
+
)
|
239
|
+
if not set(metric.keys()).issubset(valid):
|
240
|
+
report(
|
241
|
+
LogLevel.WARNING,
|
242
|
+
"metric has unexpected fields",
|
243
|
+
", ".join(set(metric.keys()) - valid),
|
244
|
+
hint="valid fields: {}".format(", ".join(valid)),
|
245
|
+
)
|
246
|
+
normalized.append(
|
247
|
+
{
|
248
|
+
"name": metric["name"],
|
249
|
+
"command": normalize_command(metric["command"]),
|
250
|
+
"condition": metric.get("condition", "True"),
|
251
|
+
}
|
252
|
+
)
|
253
|
+
elif isinstance(metrics, dict):
|
254
|
+
for name, command in metrics.items():
|
255
|
+
if not isinstance(command, str):
|
256
|
+
report(LogLevel.FATAL, "metric command must be a string", command)
|
257
|
+
normalized.append(
|
258
|
+
{
|
259
|
+
"name": name,
|
260
|
+
"command": normalize_command(command),
|
261
|
+
"condition": "True",
|
262
|
+
}
|
263
|
+
)
|
264
|
+
return normalized
|
265
|
+
|
266
|
+
|
267
|
+
def normalize_command(cmd):
|
268
|
+
if isinstance(cmd, str):
|
269
|
+
return cmd
|
270
|
+
elif isinstance(cmd, list):
|
271
|
+
return " ".join(cmd)
|
272
|
+
else:
|
273
|
+
report(LogLevel.FATAL, "command must be a string or a list of strings", cmd)
|
274
|
+
|
275
|
+
|
276
|
+
def normalize_command_list(cl):
|
277
|
+
normalized = []
|
278
|
+
if isinstance(cl, str):
|
279
|
+
normalized = [cl]
|
280
|
+
elif isinstance(cl, list):
|
281
|
+
for cmd in cl:
|
282
|
+
normalized.append(normalize_command(cmd))
|
283
|
+
return normalized
|
284
|
+
|
285
|
+
|
286
|
+
def normalize_condition(x):
|
287
|
+
if not isinstance(x, str):
|
288
|
+
report(LogLevel.FATAL, "condition must be a string", x)
|
289
|
+
return x
|
290
|
+
|
291
|
+
|
292
|
+
def normalize_point(x):
|
293
|
+
normalized = None
|
294
|
+
valid_fields = {"name", "value", "condition", "setup"}
|
295
|
+
if isinstance(x, (str, int, float)):
|
296
|
+
normalized = {"name": str(x), "value": x, "condition": "True", "setup": []}
|
297
|
+
elif isinstance(x, dict):
|
298
|
+
if not set(x.keys()).issubset(valid_fields):
|
299
|
+
report(
|
300
|
+
LogLevel.WARNING,
|
301
|
+
"point has unexpected fields",
|
302
|
+
", ".join(set(x.keys()) - valid_fields),
|
303
|
+
hint="valid fields: {}".format(", ".join(valid_fields)),
|
304
|
+
)
|
305
|
+
if "value" in x:
|
306
|
+
normalized = {
|
307
|
+
"name": str(x.get("name", x["value"])),
|
308
|
+
"value": x["value"],
|
309
|
+
"condition": normalize_condition(x.get("condition", "True")),
|
310
|
+
"setup": normalize_command_list(x.get("setup", [])),
|
311
|
+
}
|
312
|
+
else:
|
313
|
+
report(LogLevel.FATAL, "points must have a 'value' field", x)
|
314
|
+
else:
|
315
|
+
report(LogLevel.FATAL, "point must be a string, int, float or a dict", x)
|
316
|
+
return normalized
|
317
|
+
|
318
|
+
|
319
|
+
def normalize_trials(trial):
|
320
|
+
valid = {"command", "condition", "metrics"}
|
321
|
+
if isinstance(trial, str):
|
322
|
+
return [{"command": trial, "condition": "True", "metrics": None}]
|
323
|
+
elif isinstance(trial, list):
|
324
|
+
items = []
|
325
|
+
for item in trial:
|
326
|
+
normalized_item = {"command": None, "condition": "True", "metrics": None}
|
327
|
+
if isinstance(item, str):
|
328
|
+
normalized_item["command"] = normalize_command(item)
|
329
|
+
elif isinstance(item, dict):
|
330
|
+
# check for invalid fields
|
331
|
+
invalid_fields = set(item.keys()) - valid
|
332
|
+
if len(invalid_fields) > 0:
|
333
|
+
report(
|
334
|
+
LogLevel.WARNING,
|
335
|
+
"trial item has unexpected fields",
|
336
|
+
", ".join(invalid_fields),
|
337
|
+
hint="valid fields: {}".format(", ".join(valid)),
|
338
|
+
)
|
339
|
+
if "command" not in item:
|
340
|
+
report(
|
341
|
+
LogLevel.FATAL, "each trial item must have a 'command' field"
|
342
|
+
)
|
343
|
+
return None
|
344
|
+
normalized_item["command"] = normalize_command(item["command"])
|
345
|
+
normalized_item["condition"] = item.get("condition", "True")
|
346
|
+
normalized_item["metrics"] = item.get("metrics", None)
|
347
|
+
items.append(normalized_item)
|
348
|
+
return items
|
349
|
+
else:
|
350
|
+
report(LogLevel.FATAL, "trial must be a string or a list of strings")
|
351
|
+
return None
|
352
|
+
|
353
|
+
|
354
|
+
def normalize_space_values(space):
|
355
|
+
normalized = dict()
|
356
|
+
for key, values in space.items():
|
357
|
+
if key.endswith(":py"):
|
358
|
+
name = key.split(":")[-2]
|
359
|
+
if not isinstance(values, str):
|
360
|
+
report(LogLevel.FATAL, "python command must be a string", key)
|
361
|
+
result = eval(values)
|
362
|
+
if not isinstance(result, list):
|
363
|
+
report(
|
364
|
+
LogLevel.FATAL, "python command generated non-list values", values
|
365
|
+
)
|
366
|
+
normalized[name] = [normalize_point(x) for x in result]
|
367
|
+
elif values is not None:
|
368
|
+
normalized[key] = []
|
369
|
+
for x in values:
|
370
|
+
normalized[key].append(normalize_point(x))
|
371
|
+
else:
|
372
|
+
normalized[key] = None
|
373
|
+
return normalized
|
374
|
+
|
375
|
+
|
376
|
+
def normalize_data(json_data):
|
377
|
+
valid_fields = {
|
378
|
+
"env",
|
379
|
+
"setup",
|
380
|
+
"space",
|
381
|
+
"trials",
|
382
|
+
"metrics",
|
383
|
+
"presets",
|
384
|
+
"order",
|
385
|
+
}
|
386
|
+
|
387
|
+
normalized = dict()
|
388
|
+
for key in json_data.keys():
|
389
|
+
if key in valid_fields:
|
390
|
+
normalized[key] = json_data[key]
|
391
|
+
else:
|
392
|
+
report(
|
393
|
+
LogLevel.WARNING,
|
394
|
+
"unknown field in configuration",
|
395
|
+
key,
|
396
|
+
hint="available fields: {}".format(", ".join(valid_fields)),
|
397
|
+
)
|
398
|
+
|
399
|
+
space = normalize_space_values(json_data.get("space", {}))
|
400
|
+
|
401
|
+
normalized["space"] = space
|
402
|
+
normalized["trials"] = normalize_trials(json_data.get("trials", []))
|
403
|
+
normalized["setup"] = normalize_setup(json_data.get("setup", {}), space)
|
404
|
+
normalized["metrics"] = normalize_metrics(json_data.get("metrics", []))
|
405
|
+
normalized["presets"] = json_data.get("presets", dict())
|
406
|
+
|
407
|
+
for trial in normalized["trials"]:
|
408
|
+
if trial["metrics"] is not None:
|
409
|
+
metric_names = [m["name"] for m in normalized["metrics"]]
|
410
|
+
if not all(m in metric_names for m in trial["metrics"]):
|
411
|
+
report(
|
412
|
+
LogLevel.FATAL,
|
413
|
+
"trial references unknown metrics",
|
414
|
+
", ".join(trial["metrics"]),
|
415
|
+
hint="available metrics: {}".format(
|
416
|
+
", ".join([m["name"] for m in normalized["metrics"]])
|
417
|
+
),
|
418
|
+
)
|
419
|
+
|
420
|
+
return normalized
|
421
|
+
|
422
|
+
|
423
|
+
def reorder_dimensions(dimensions, order):
|
424
|
+
reordered = list(dimensions)
|
425
|
+
for k in order:
|
426
|
+
if k in reordered:
|
427
|
+
reordered.append(reordered.pop(reordered.index(k)))
|
428
|
+
return reordered
|
429
|
+
|
430
|
+
|
431
|
+
def define_order(settings, data):
|
432
|
+
space = data["space"]
|
433
|
+
|
434
|
+
available = space.keys()
|
435
|
+
invalid_order_keys = [
|
436
|
+
k for k in set(settings["order"] + data["order"]) if k not in available
|
437
|
+
]
|
438
|
+
if len(invalid_order_keys) > 0:
|
439
|
+
hint = "available values: {}".format(", ".join(available))
|
440
|
+
report(
|
441
|
+
LogLevel.FATAL,
|
442
|
+
"invalid order values",
|
443
|
+
", ".join(invalid_order_keys),
|
444
|
+
hint=hint,
|
445
|
+
)
|
446
|
+
|
447
|
+
dims = list(space.keys())
|
448
|
+
final_order = reorder_dimensions(dims, data["order"])
|
449
|
+
final_order = reorder_dimensions(final_order, settings["order"])
|
450
|
+
|
451
|
+
return final_order
|
452
|
+
|
453
|
+
|
454
|
+
def apply_preset(data, preset_name):
|
455
|
+
space = data["space"]
|
456
|
+
space_names = {
|
457
|
+
dim: [x["name"] for x in space[dim]] for dim in space if space[dim] is not None
|
458
|
+
}
|
459
|
+
preset_space = data["presets"][preset_name]
|
460
|
+
subspace = space.copy()
|
461
|
+
|
462
|
+
for dim, space_items in preset_space.items():
|
463
|
+
if dim not in space:
|
464
|
+
hint = "available dimensions: {}".format(", ".join(space.keys()))
|
465
|
+
report(LogLevel.FATAL, "preset dimension not in space", dim, hint=hint)
|
466
|
+
new_items = []
|
467
|
+
wrong = []
|
468
|
+
for item in space_items:
|
469
|
+
if not isinstance(item, (str, int, float)):
|
470
|
+
report(
|
471
|
+
LogLevel.FATAL, "preset item must be a string, int or float", item
|
472
|
+
)
|
473
|
+
if space[dim] is None:
|
474
|
+
if isinstance(item, str) and "*" in item:
|
475
|
+
# regex definition
|
476
|
+
report(
|
477
|
+
LogLevel.FATAL,
|
478
|
+
"regex cannot be used on undefined dimensions",
|
479
|
+
dim,
|
480
|
+
)
|
481
|
+
elif isinstance(item, (str, int, float)):
|
482
|
+
new_items.append(item)
|
483
|
+
else:
|
484
|
+
report(
|
485
|
+
LogLevel.FATAL,
|
486
|
+
"preset item for undefined dimensions must be a string, int or float",
|
487
|
+
item,
|
488
|
+
)
|
489
|
+
elif isinstance(item, str) and "*" in item:
|
490
|
+
# definition via regex
|
491
|
+
pattern = "^" + re.escape(item).replace("\\*", ".*") + "$"
|
492
|
+
regex = re.compile(pattern)
|
493
|
+
new_items += [n for n in space_names[dim] if regex.match(n)]
|
494
|
+
elif str(item) not in space_names[dim]:
|
495
|
+
report(
|
496
|
+
LogLevel.ERROR,
|
497
|
+
"unknown name in preset",
|
498
|
+
hint="in order to use '{}' in preset '{}', define it first in the space".format(
|
499
|
+
item, preset_name
|
500
|
+
),
|
501
|
+
)
|
502
|
+
else:
|
503
|
+
# definition via name
|
504
|
+
new_items.append(next(x for x in space[dim] if x["name"] == str(item)))
|
505
|
+
|
506
|
+
if len(wrong) > 0:
|
507
|
+
hint = "available names: {}".format(", ".join(space_names[dim]))
|
508
|
+
report(
|
509
|
+
LogLevel.FATAL,
|
510
|
+
f"unknown name in preset '{preset_name}'",
|
511
|
+
", ".join(wrong),
|
512
|
+
hint=hint,
|
513
|
+
)
|
514
|
+
subspace[dim] = new_items
|
515
|
+
|
516
|
+
for dim, items in subspace.items():
|
517
|
+
if items is not None and len(items) == 0:
|
518
|
+
report(LogLevel.ERROR, f"empty dimension in preset '{preset_name}'", dim)
|
519
|
+
|
520
|
+
return subspace
|
521
|
+
|
522
|
+
|
523
|
+
def run_point_command(command, execution, point, on_dims):
|
524
|
+
gcommand = substitute_global_yvars(command, execution["subspace"])
|
525
|
+
on_dims_0 = [dim.split(".")[0] for dim in on_dims]
|
526
|
+
suborder = [d for d in execution["order"] if d in on_dims_0]
|
527
|
+
point_map = {key: x for key, x in zip(suborder, point)}
|
528
|
+
pcommand = substitute_point_yvars(gcommand, point_map, None)
|
529
|
+
|
530
|
+
if not valid_conditions(point, suborder):
|
531
|
+
return
|
532
|
+
|
533
|
+
if execution["dry_run"]:
|
534
|
+
report(
|
535
|
+
LogLevel.INFO,
|
536
|
+
"dry run",
|
537
|
+
pcommand,
|
538
|
+
)
|
539
|
+
else:
|
540
|
+
result = subprocess.run(
|
541
|
+
pcommand,
|
542
|
+
shell=True,
|
543
|
+
universal_newlines=True,
|
544
|
+
capture_output=False,
|
545
|
+
env=execution["env"],
|
546
|
+
)
|
547
|
+
if result.returncode != 0:
|
548
|
+
report(
|
549
|
+
LogLevel.ERROR,
|
550
|
+
"point setup",
|
551
|
+
f"'{command}'",
|
552
|
+
f"returned {result.returncode}",
|
553
|
+
)
|
554
|
+
|
555
|
+
|
556
|
+
def run_points_sequential(command, item_plan, execution, on_dims, par_config):
|
557
|
+
sequential_space = item_plan["sequential_space"]
|
558
|
+
parallel_dims = item_plan["parallel_dims"]
|
559
|
+
seq_points = list(itertools.product(*sequential_space))
|
560
|
+
sequential_dims = item_plan["sequential_dims"]
|
561
|
+
order = execution["order"]
|
562
|
+
|
563
|
+
named_par_config = [(name, x) for name, x in zip(parallel_dims, par_config)]
|
564
|
+
if len(sequential_dims) == 0:
|
565
|
+
final_config = [x[1] for x in named_par_config]
|
566
|
+
run_point_command(command, execution, final_config, on_dims)
|
567
|
+
return
|
568
|
+
|
569
|
+
for seq_config in seq_points:
|
570
|
+
named_seq_config = [(dim, x) for dim, x in zip(sequential_dims, seq_config)]
|
571
|
+
named_ordered_config = sorted(
|
572
|
+
named_par_config + named_seq_config, key=lambda x: order.index(x[0])
|
573
|
+
)
|
574
|
+
final_config = [x[1] for x in named_ordered_config]
|
575
|
+
run_point_command(command, execution, final_config, on_dims)
|
576
|
+
|
577
|
+
|
578
|
+
def run_point_setup_item(item, execution):
|
579
|
+
commands = item["commands"]
|
580
|
+
on_dims = item["on"]
|
581
|
+
order = execution["order"]
|
582
|
+
subspace = execution["subspace"]
|
583
|
+
item_plan = get_point_item_plan(item, subspace, order)
|
584
|
+
|
585
|
+
parallel_space = item_plan["parallel_space"]
|
586
|
+
parallel_dims = item_plan["parallel_dims"]
|
587
|
+
|
588
|
+
num_parallel_dims = len(parallel_space)
|
589
|
+
if num_parallel_dims == 0:
|
590
|
+
max_workers = 1
|
591
|
+
else:
|
592
|
+
max_workers = min(execution["subspace_size"], os.cpu_count() or 1)
|
593
|
+
report(LogLevel.INFO, f"using {max_workers} workers for point setup")
|
594
|
+
|
595
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
596
|
+
par_points = list(itertools.product(*parallel_space))
|
597
|
+
|
598
|
+
for i, command in enumerate(commands, start=1):
|
599
|
+
if len(parallel_dims) == 0:
|
600
|
+
run_points_sequential(command, item_plan, execution, on_dims, [])
|
601
|
+
else:
|
602
|
+
futures = []
|
603
|
+
for j, par_config in enumerate(par_points, start=1):
|
604
|
+
future = executor.submit(
|
605
|
+
run_points_sequential,
|
606
|
+
command,
|
607
|
+
item_plan,
|
608
|
+
execution,
|
609
|
+
on_dims,
|
610
|
+
par_config,
|
611
|
+
)
|
612
|
+
futures.append(future)
|
613
|
+
for future in concurrent.futures.as_completed(futures):
|
614
|
+
exc = future.exception()
|
615
|
+
if exc is not None:
|
616
|
+
report(LogLevel.ERROR, "point setup failed", command)
|
617
|
+
|
618
|
+
|
619
|
+
def run_point_setup(data, execution):
|
620
|
+
for item in data["setup"]["point"]:
|
621
|
+
if item["on"] is None:
|
622
|
+
item["on"] = execution["subspace"].keys()
|
623
|
+
|
624
|
+
# reordering
|
625
|
+
item["on"] = [x for x in item["on"] if x.split(".")[0] in execution["order"]]
|
626
|
+
if len(item["on"]) == 0:
|
627
|
+
report(
|
628
|
+
LogLevel.WARNING,
|
629
|
+
"point setup item has no valid 'on' dimensions. Skipping",
|
630
|
+
item,
|
631
|
+
)
|
632
|
+
else:
|
633
|
+
if execution["dry_run"]:
|
634
|
+
report(LogLevel.INFO, "starting dry point setup")
|
635
|
+
else:
|
636
|
+
report(LogLevel.INFO, "starting point setup")
|
637
|
+
|
638
|
+
run_point_setup_item(item, execution)
|
639
|
+
|
640
|
+
if execution["dry_run"]:
|
641
|
+
report(LogLevel.INFO, "dry point setup completed")
|
642
|
+
else:
|
643
|
+
report(LogLevel.INFO, "point setup completed")
|
644
|
+
|
645
|
+
|
646
|
+
def run_global_setup(data, execution):
|
647
|
+
subspace = execution["subspace"]
|
648
|
+
setup_commands = data["setup"]["global"]
|
649
|
+
# gather setup commands from space
|
650
|
+
for key, values in subspace.items():
|
651
|
+
for value in values:
|
652
|
+
if len(value["setup"]) > 0:
|
653
|
+
for cmd in value["setup"]:
|
654
|
+
setup_commands.append(cmd)
|
655
|
+
|
656
|
+
if execution["dry_run"]:
|
657
|
+
report(LogLevel.INFO, "starting dry global setup")
|
658
|
+
else:
|
659
|
+
report(LogLevel.INFO, "starting global setup")
|
660
|
+
|
661
|
+
errors = False
|
662
|
+
for command in setup_commands:
|
663
|
+
if execution["dry_run"]:
|
664
|
+
report(LogLevel.INFO, "dry run", command)
|
665
|
+
else:
|
666
|
+
command = substitute_global_yvars(command, subspace)
|
667
|
+
result = subprocess.run(
|
668
|
+
command,
|
669
|
+
shell=True,
|
670
|
+
universal_newlines=True,
|
671
|
+
capture_output=False,
|
672
|
+
env=execution["env"],
|
673
|
+
)
|
674
|
+
if result.returncode != 0:
|
675
|
+
errors = True
|
676
|
+
report(
|
677
|
+
LogLevel.ERROR,
|
678
|
+
"setup",
|
679
|
+
f"'{command}'",
|
680
|
+
f"returned {result.returncode}",
|
681
|
+
)
|
682
|
+
if errors:
|
683
|
+
report(LogLevel.WARNING, "errors have occurred during setup")
|
684
|
+
report(LogLevel.INFO, "setup failed")
|
685
|
+
if execution["dry_run"]:
|
686
|
+
report(LogLevel.INFO, "dry setup completed")
|
687
|
+
else:
|
688
|
+
report(LogLevel.INFO, "setup completed")
|
689
|
+
|
690
|
+
|
691
|
+
def run_setup(settings, data, execution):
|
692
|
+
run_global_setup(data, execution)
|
693
|
+
run_point_setup(data, execution)
|
694
|
+
|
695
|
+
|
696
|
+
def point_to_string(point):
|
697
|
+
return ".".join([str(x["name"]) for x in point])
|
698
|
+
|
699
|
+
|
700
|
+
def metrics_to_string(metric_values):
|
701
|
+
return " ".join([f"{m}={v}" for m, v in metric_values.items()])
|
702
|
+
|
703
|
+
|
704
|
+
def get_progress(i, subspace_size):
|
705
|
+
return "[{}/{}]".format(i, subspace_size)
|
706
|
+
|
707
|
+
|
708
|
+
def run_point_trials(settings, data, execution, f, i, point):
|
709
|
+
os.makedirs(
|
710
|
+
os.path.join(settings["temp_dir"], settings["now"]),
|
711
|
+
exist_ok=True,
|
712
|
+
)
|
713
|
+
|
714
|
+
point_map = {key: x for key, x in zip(execution["order"], point)}
|
715
|
+
report(
|
716
|
+
LogLevel.INFO,
|
717
|
+
get_progress(i, execution["subspace_size"]),
|
718
|
+
point_to_string(point),
|
719
|
+
"started",
|
720
|
+
)
|
721
|
+
|
722
|
+
compatible_trials, compatible_metrics = get_compatible_trials_and_metrics(
|
723
|
+
data, point, execution
|
724
|
+
)
|
725
|
+
|
726
|
+
if len(compatible_metrics) == 0:
|
727
|
+
report(LogLevel.WARNING, point_to_string(point), "no compatible metrics found")
|
728
|
+
|
729
|
+
if len(compatible_trials) == 0 or len(compatible_metrics) == 0:
|
730
|
+
report(
|
731
|
+
LogLevel.WARNING,
|
732
|
+
point_to_string(point),
|
733
|
+
"no compatible trials found",
|
734
|
+
hint="try relaxing your trial conditions or adding more trials.",
|
735
|
+
)
|
736
|
+
|
737
|
+
for i, trial in enumerate(compatible_trials):
|
738
|
+
point_id = os.path.join(
|
739
|
+
settings["temp_dir"], settings["now"], point_to_string(point) + f"_trial{i}"
|
740
|
+
)
|
741
|
+
|
742
|
+
command = substitute_global_yvars(trial["command"], execution["subspace"])
|
743
|
+
command = substitute_point_yvars(command, point_map, point_id)
|
744
|
+
command_output = subprocess.run(
|
745
|
+
command,
|
746
|
+
shell=True,
|
747
|
+
env=execution["env"],
|
748
|
+
universal_newlines=True,
|
749
|
+
capture_output=True,
|
750
|
+
)
|
751
|
+
|
752
|
+
with open(f"{point_id}.out", "w") as output_file:
|
753
|
+
if command_output.stdout:
|
754
|
+
output_file.write(command_output.stdout)
|
755
|
+
|
756
|
+
with open(f"{point_id}.err", "w") as error_file:
|
757
|
+
if command_output.stderr:
|
758
|
+
error_file.write(command_output.stderr)
|
759
|
+
|
760
|
+
if command_output.returncode != 0:
|
761
|
+
hint = "check the following files for more details:\n"
|
762
|
+
hint += f"{point_id}.out\n{point_id}.err\n{point_id}.tmp"
|
763
|
+
report(
|
764
|
+
LogLevel.ERROR,
|
765
|
+
point_to_string(point),
|
766
|
+
f"failed trial command '{command}' (code {command_output.returncode})",
|
767
|
+
hint=hint,
|
768
|
+
)
|
769
|
+
|
770
|
+
collected_metrics = dict()
|
771
|
+
for metric in compatible_metrics:
|
772
|
+
command = substitute_global_yvars(metric["command"], execution["subspace"])
|
773
|
+
command = substitute_point_yvars(command, point_map, point_id)
|
774
|
+
command_output = subprocess.run(
|
775
|
+
command,
|
776
|
+
shell=True,
|
777
|
+
universal_newlines=True,
|
778
|
+
capture_output=True,
|
779
|
+
env=execution["env"],
|
780
|
+
)
|
781
|
+
if command_output.returncode != 0:
|
782
|
+
hint = "the command '{}' produced the following output:\n{}".format(
|
783
|
+
command,
|
784
|
+
command_output.stdout.strip(),
|
785
|
+
)
|
786
|
+
report(
|
787
|
+
LogLevel.ERROR,
|
788
|
+
point_to_string(point),
|
789
|
+
"failed metric '{}' (code {})".format(
|
790
|
+
metric["name"], command_output.returncode
|
791
|
+
),
|
792
|
+
hint=hint,
|
793
|
+
)
|
794
|
+
else:
|
795
|
+
output_lines = command_output.stdout.strip().split("\n")
|
796
|
+
|
797
|
+
def int_or_float(x):
|
798
|
+
try:
|
799
|
+
return int(x)
|
800
|
+
except ValueError:
|
801
|
+
return float(x)
|
802
|
+
|
803
|
+
collected_metrics[metric["name"]] = [
|
804
|
+
int_or_float(line) for line in output_lines
|
805
|
+
]
|
806
|
+
|
807
|
+
metric_values_df = pd.DataFrame.from_dict(
|
808
|
+
collected_metrics, orient="index"
|
809
|
+
).transpose()
|
810
|
+
if not settings["fold"]:
|
811
|
+
NaNs = metric_values_df.columns[metric_values_df.isnull().any()]
|
812
|
+
if len(NaNs) > 0:
|
813
|
+
report(
|
814
|
+
LogLevel.WARNING,
|
815
|
+
"the following metrics generated some NaNs",
|
816
|
+
" ".join(list(NaNs)),
|
817
|
+
)
|
818
|
+
|
819
|
+
result = {k: x["name"] for k, x in point_map.items()}
|
820
|
+
if settings["fold"]:
|
821
|
+
result.update(metric_values_df.to_dict(orient="list"))
|
822
|
+
f.write(json.dumps(result) + "\n")
|
823
|
+
else:
|
824
|
+
for record in metric_values_df.to_dict(orient="records"):
|
825
|
+
result.update(record)
|
826
|
+
f.write(json.dumps(result) + "\n")
|
827
|
+
|
828
|
+
report(LogLevel.INFO, "obtained", metrics_to_string(collected_metrics))
|
829
|
+
for metric_name, values in collected_metrics.items():
|
830
|
+
if len(values) > 1:
|
831
|
+
report(
|
832
|
+
LogLevel.INFO,
|
833
|
+
"median",
|
834
|
+
metric_name,
|
835
|
+
f"{pd.Series(values).median():.3f}",
|
836
|
+
)
|
837
|
+
report(
|
838
|
+
LogLevel.INFO,
|
839
|
+
get_progress(i, execution["subspace_size"]),
|
840
|
+
point_to_string(point),
|
841
|
+
"completed",
|
842
|
+
)
|
843
|
+
f.flush()
|
844
|
+
|
845
|
+
|
846
|
+
def valid_conditions(point, order):
|
847
|
+
point_context = {}
|
848
|
+
yuclid = {name: x["value"] for name, x in zip(order, point)}
|
849
|
+
point_context["yuclid"] = type("Yuclid", (), yuclid)()
|
850
|
+
conditions = [x["condition"] for x in point if "condition" in x]
|
851
|
+
return all(eval(c, point_context) for c in conditions)
|
852
|
+
|
853
|
+
|
854
|
+
def valid_condition(condition, point, order):
|
855
|
+
point_context = {}
|
856
|
+
yuclid = {name: x["value"] for name, x in zip(order, point)}
|
857
|
+
point_context["yuclid"] = type("Yuclid", (), yuclid)()
|
858
|
+
return eval(condition, point_context)
|
859
|
+
|
860
|
+
|
861
|
+
def validate_execution(execution, data):
|
862
|
+
# checking if there's at least of compatible trial command for each point
|
863
|
+
for point in execution["subspace_points"]:
|
864
|
+
compatible_trials, compatible_metrics = get_compatible_trials_and_metrics(
|
865
|
+
data, point, execution
|
866
|
+
)
|
867
|
+
if len(compatible_trials) == 0:
|
868
|
+
report(
|
869
|
+
LogLevel.ERROR,
|
870
|
+
"no compatible trials found for point",
|
871
|
+
point_to_string(point),
|
872
|
+
hint="try relaxing your trial conditions or adding more trials.",
|
873
|
+
)
|
874
|
+
|
875
|
+
|
876
|
+
def get_compatible_trials_and_metrics(data, point, execution):
|
877
|
+
all_metric_names = {m["name"] for m in data["metrics"]}
|
878
|
+
selected_metric_names = execution["metrics"] or all_metric_names
|
879
|
+
valid_metrics = [
|
880
|
+
metric
|
881
|
+
for metric in data["metrics"]
|
882
|
+
if metric["name"] in selected_metric_names
|
883
|
+
and valid_condition(metric["condition"], point, execution["order"])
|
884
|
+
]
|
885
|
+
valid_metric_names = {m["name"] for m in valid_metrics}
|
886
|
+
compatible_trials = [
|
887
|
+
trial
|
888
|
+
for trial in data["trials"]
|
889
|
+
if valid_condition(trial["condition"], point, execution["order"])
|
890
|
+
and (
|
891
|
+
trial["metrics"] is None
|
892
|
+
or any(m in valid_metric_names for m in trial["metrics"])
|
893
|
+
)
|
894
|
+
]
|
895
|
+
compatible_metrics = [
|
896
|
+
metric
|
897
|
+
for metric in valid_metrics
|
898
|
+
if any(
|
899
|
+
trial["metrics"] is None or metric["name"] in trial["metrics"]
|
900
|
+
for trial in compatible_trials
|
901
|
+
)
|
902
|
+
]
|
903
|
+
return compatible_trials, compatible_metrics
|
904
|
+
|
905
|
+
|
906
|
+
def run_subspace_trials(settings, data, execution):
|
907
|
+
if settings["dry_run"]:
|
908
|
+
for i, point in enumerate(execution["subspace_points"], start=1):
|
909
|
+
point_map = {key: x for key, x in zip(execution["order"], point)}
|
910
|
+
if valid_conditions(point, execution["order"]):
|
911
|
+
compatible_trials, compatible_metrics = (
|
912
|
+
get_compatible_trials_and_metrics(data, point, execution)
|
913
|
+
)
|
914
|
+
if len(compatible_trials) == 0:
|
915
|
+
report(
|
916
|
+
LogLevel.ERROR,
|
917
|
+
point_to_string(point),
|
918
|
+
"no compatible trials found",
|
919
|
+
)
|
920
|
+
elif len(compatible_metrics) == 0:
|
921
|
+
report(
|
922
|
+
LogLevel.ERROR,
|
923
|
+
point_to_string(point),
|
924
|
+
"no compatible metrics found",
|
925
|
+
)
|
926
|
+
else:
|
927
|
+
report(
|
928
|
+
LogLevel.INFO,
|
929
|
+
get_progress(i, execution["subspace_size"]),
|
930
|
+
"dry run",
|
931
|
+
point_to_string(point),
|
932
|
+
)
|
933
|
+
else:
|
934
|
+
output_dir = os.path.dirname(settings["output"])
|
935
|
+
if output_dir and not os.path.exists(output_dir):
|
936
|
+
os.makedirs(output_dir, exist_ok=True)
|
937
|
+
with open(settings["output"], "a") as f:
|
938
|
+
for i, point in enumerate(execution["subspace_points"], start=1):
|
939
|
+
run_point_trials(settings, data, execution, f, i, point)
|
940
|
+
f.flush()
|
941
|
+
|
942
|
+
|
943
|
+
def validate_dimensions(subspace):
|
944
|
+
undefined = [k for k, v in subspace.items() if v is None]
|
945
|
+
if len(undefined) > 0:
|
946
|
+
hint = "define dimensions with presets or select them with --select. "
|
947
|
+
hint += "E.g. --select {}=value1,value2".format(undefined[0])
|
948
|
+
report(LogLevel.FATAL, "dimensions undefined", ", ".join(undefined), hint=hint)
|
949
|
+
|
950
|
+
|
951
|
+
def prepare_subspace_execution(subspace, order, env, metrics, dry_run):
|
952
|
+
ordered_subspace = [subspace[x] for x in order]
|
953
|
+
|
954
|
+
execution = dict()
|
955
|
+
execution["subspace_points"] = []
|
956
|
+
for point in itertools.product(*ordered_subspace):
|
957
|
+
if valid_conditions(point, order):
|
958
|
+
execution["subspace_points"].append(point)
|
959
|
+
execution["subspace_size"] = len(execution["subspace_points"])
|
960
|
+
|
961
|
+
execution["subspace_values"] = {
|
962
|
+
key: [x["value"] for x in subspace[key]] for key in subspace
|
963
|
+
}
|
964
|
+
execution["subspace_names"] = {
|
965
|
+
key: [x["name"] for x in subspace[key]] for key in subspace
|
966
|
+
}
|
967
|
+
execution["subspace"] = subspace
|
968
|
+
execution["order"] = order
|
969
|
+
execution["env"] = env
|
970
|
+
execution["dry_run"] = dry_run
|
971
|
+
execution["metrics"] = metrics
|
972
|
+
|
973
|
+
if execution["subspace_size"] == 0:
|
974
|
+
report(LogLevel.WARNING, "empty subspace")
|
975
|
+
else:
|
976
|
+
report(LogLevel.INFO, "subspace size", execution["subspace_size"])
|
977
|
+
return execution
|
978
|
+
|
979
|
+
|
980
|
+
def validate_presets(settings, data):
|
981
|
+
available = data["presets"].keys()
|
982
|
+
for preset_name in settings["presets"]:
|
983
|
+
if preset_name not in available:
|
984
|
+
hint = "available presets: {}".format(", ".join(available))
|
985
|
+
report(
|
986
|
+
LogLevel.FATAL,
|
987
|
+
"invalid preset",
|
988
|
+
preset_name,
|
989
|
+
hint=hint,
|
990
|
+
)
|
991
|
+
|
992
|
+
|
993
|
+
def get_point_item_plan(item, subspace, order):
|
994
|
+
all_dims = item["on"] or [f"{k}.values" for k in subspace.keys()]
|
995
|
+
|
996
|
+
name_dims = [d.split(".")[0] for d in all_dims if d.endswith(".names")]
|
997
|
+
value_dims = [d.split(".")[0] for d in all_dims if d.endswith(".values")]
|
998
|
+
|
999
|
+
if isinstance(item["parallel"], bool):
|
1000
|
+
if item["parallel"]:
|
1001
|
+
item["parallel"] = item["on"]
|
1002
|
+
else:
|
1003
|
+
item["parallel"] = []
|
1004
|
+
|
1005
|
+
# collect parallel and sequential dimensions
|
1006
|
+
parallel_dims = set(item["parallel"])
|
1007
|
+
sequential_dims = set([x.split(".")[0] for x in all_dims]) - parallel_dims
|
1008
|
+
|
1009
|
+
# reordering
|
1010
|
+
parallel_dims = reorder_dimensions(parallel_dims, order)
|
1011
|
+
sequential_dims = reorder_dimensions(sequential_dims, order)
|
1012
|
+
|
1013
|
+
# building the plan_subspace which is just the subspace where `name_dims`
|
1014
|
+
# have items with no conditions and name==value
|
1015
|
+
|
1016
|
+
plan_subspace = dict()
|
1017
|
+
plan_subspace.update({k: subspace[k] for k in value_dims})
|
1018
|
+
|
1019
|
+
subspace_names = {k: {x["name"] for x in subspace[k]} for k in name_dims}
|
1020
|
+
plan_subspace.update(
|
1021
|
+
{d: [normalize_point(x) for x in names] for d, names in subspace_names.items()}
|
1022
|
+
)
|
1023
|
+
|
1024
|
+
# building space
|
1025
|
+
parallel_space = [plan_subspace[k] for k in parallel_dims]
|
1026
|
+
sequential_space = [plan_subspace[k] for k in sequential_dims]
|
1027
|
+
|
1028
|
+
# name_dims don't need to have conditions, and their name is their value
|
1029
|
+
return {
|
1030
|
+
"parallel_dims": parallel_dims,
|
1031
|
+
"sequential_dims": sequential_dims,
|
1032
|
+
"parallel_space": parallel_space,
|
1033
|
+
"sequential_space": sequential_space,
|
1034
|
+
}
|
1035
|
+
|
1036
|
+
|
1037
|
+
def build_settings(args):
|
1038
|
+
settings = dict(vars(args))
|
1039
|
+
|
1040
|
+
# inputs
|
1041
|
+
settings["inputs"] = []
|
1042
|
+
for file in args.inputs:
|
1043
|
+
if not os.path.isfile(file):
|
1044
|
+
report(LogLevel.ERROR, f"'{file}' does not exist")
|
1045
|
+
else:
|
1046
|
+
settings["inputs"].append(file)
|
1047
|
+
os.makedirs(args.temp_dir, exist_ok=True)
|
1048
|
+
|
1049
|
+
# output
|
1050
|
+
settings["now"] = "{:%Y%m%d-%H%M%S}".format(datetime.now())
|
1051
|
+
filename = "results.{}.json".format(settings["now"])
|
1052
|
+
if args.output is None and args.output_dir is None:
|
1053
|
+
settings["output"] = filename
|
1054
|
+
elif args.output is not None and args.output_dir is not None:
|
1055
|
+
report(LogLevel.FATAL, "either --output or --output-dir must be specified")
|
1056
|
+
elif args.output_dir is not None:
|
1057
|
+
os.makedirs(args.output_dir, exist_ok=True)
|
1058
|
+
settings["output"] = os.path.join(args.output_dir, filename)
|
1059
|
+
else:
|
1060
|
+
settings["output"] = args.output
|
1061
|
+
|
1062
|
+
report(LogLevel.INFO, "working directory", os.getcwd())
|
1063
|
+
report(LogLevel.INFO, "input configurations", ", ".join(args.inputs))
|
1064
|
+
report(LogLevel.INFO, "output data", settings["output"])
|
1065
|
+
report(
|
1066
|
+
LogLevel.INFO,
|
1067
|
+
"temp directory",
|
1068
|
+
os.path.join(settings["temp_dir"], settings["now"]),
|
1069
|
+
)
|
1070
|
+
return settings
|
1071
|
+
|
1072
|
+
|
1073
|
+
def normalize_point_setup(point_setup, space):
|
1074
|
+
if isinstance(point_setup, str):
|
1075
|
+
point_setup = [
|
1076
|
+
{"commands": [x], "on": None, "parallel": False}
|
1077
|
+
for x in normalize_command_list(point_setup)
|
1078
|
+
]
|
1079
|
+
elif isinstance(point_setup, list):
|
1080
|
+
normalized_items = []
|
1081
|
+
for item in point_setup:
|
1082
|
+
unexpected = [x for x in item if x not in ["commands", "on", "parallel"]]
|
1083
|
+
if len(unexpected) > 0:
|
1084
|
+
report(
|
1085
|
+
LogLevel.WARNING,
|
1086
|
+
"point setup item has unexpected fields",
|
1087
|
+
", ".join(unexpected),
|
1088
|
+
hint="fields: 'commands', 'on', 'parallel'",
|
1089
|
+
)
|
1090
|
+
if isinstance(item, str):
|
1091
|
+
normalized_items.append(
|
1092
|
+
{"commands": [item], "on": None, "parallel": False}
|
1093
|
+
)
|
1094
|
+
elif isinstance(item, dict):
|
1095
|
+
if "commands" in item:
|
1096
|
+
normalized_item = {
|
1097
|
+
"commands": normalize_command_list(item["commands"]),
|
1098
|
+
"on": item.get("on", None),
|
1099
|
+
"parallel": item.get("parallel", False),
|
1100
|
+
}
|
1101
|
+
if normalized_item["on"] is None:
|
1102
|
+
normalized_item["on"] = list(space.keys())
|
1103
|
+
|
1104
|
+
# adding .values to 'on' dimensions
|
1105
|
+
normalized_item["on"] = [
|
1106
|
+
x if "." in x else x + ".values" for x in normalized_item["on"]
|
1107
|
+
]
|
1108
|
+
|
1109
|
+
if normalized_item["parallel"] == True:
|
1110
|
+
normalized_item["parallel"] = [
|
1111
|
+
x.split(".")[0] for x in normalized_item["on"]
|
1112
|
+
]
|
1113
|
+
normalized_items.append(normalized_item)
|
1114
|
+
else:
|
1115
|
+
report(
|
1116
|
+
LogLevel.FATAL,
|
1117
|
+
"point setup item must have 'commands' field",
|
1118
|
+
)
|
1119
|
+
else:
|
1120
|
+
report(LogLevel.FATAL, "point setup must be a string or a list")
|
1121
|
+
elif isinstance(point_setup, dict):
|
1122
|
+
report(LogLevel.FATAL, "point setup must be a string or a list")
|
1123
|
+
|
1124
|
+
# check validity of 'on' fields
|
1125
|
+
for item in point_setup:
|
1126
|
+
if not isinstance(item["on"], (list, type(None))):
|
1127
|
+
report(LogLevel.FATAL, "point setup 'on' must be a list or None")
|
1128
|
+
for dim in item["on"]:
|
1129
|
+
if not isinstance(dim, str):
|
1130
|
+
report(LogLevel.FATAL, "every 'on' dimension must be a string")
|
1131
|
+
elif dim.split(".")[0] not in space:
|
1132
|
+
available = list(space.keys())
|
1133
|
+
hint = "available dimensions: {}".format(", ".join(available))
|
1134
|
+
report(
|
1135
|
+
LogLevel.FATAL,
|
1136
|
+
"point setup 'on' dimension not in space",
|
1137
|
+
dim,
|
1138
|
+
hint=hint,
|
1139
|
+
)
|
1140
|
+
|
1141
|
+
# check validity of 'parallel' fields
|
1142
|
+
for item in point_setup:
|
1143
|
+
parallel = item["parallel"]
|
1144
|
+
if not isinstance(parallel, (bool, list)):
|
1145
|
+
report(LogLevel.FATAL, "point setup 'parallel' must be a boolean or a list")
|
1146
|
+
if isinstance(parallel, list):
|
1147
|
+
wrong = [
|
1148
|
+
x for x in parallel if not isinstance(x, str) or x not in item["on"]
|
1149
|
+
]
|
1150
|
+
if len(wrong) > 0:
|
1151
|
+
hint = "available dimensions: {}".format(", ".join(item["on"]))
|
1152
|
+
report(
|
1153
|
+
LogLevel.FATAL,
|
1154
|
+
"invalid parallel dimensions",
|
1155
|
+
", ".join(wrong),
|
1156
|
+
hint=hint,
|
1157
|
+
)
|
1158
|
+
|
1159
|
+
return normalized_items
|
1160
|
+
|
1161
|
+
|
1162
|
+
def normalize_setup(setup, space):
|
1163
|
+
normalized = {"global": [], "point": []}
|
1164
|
+
|
1165
|
+
if not isinstance(setup, dict):
|
1166
|
+
report(LogLevel.FATAL, "setup must have fields 'global' and/or 'point'")
|
1167
|
+
|
1168
|
+
if "global" in setup:
|
1169
|
+
normalized["global"] = normalize_command_list(setup["global"])
|
1170
|
+
|
1171
|
+
if "point" in setup:
|
1172
|
+
normalized["point"] = normalize_point_setup(setup["point"], space)
|
1173
|
+
|
1174
|
+
return normalized
|
1175
|
+
|
1176
|
+
|
1177
|
+
def print_subspace(subspace):
|
1178
|
+
for dim, values in subspace.items():
|
1179
|
+
names = remove_duplicates([x["name"] for x in values])
|
1180
|
+
report(LogLevel.INFO, "subspace.{}: {}".format(dim, ", ".join(names)))
|
1181
|
+
|
1182
|
+
|
1183
|
+
def run_experiments(settings, data, order, env, preset_name=None):
|
1184
|
+
if preset_name is None:
|
1185
|
+
subspace = data["space"].copy()
|
1186
|
+
else:
|
1187
|
+
subspace = apply_preset(data, preset_name)
|
1188
|
+
|
1189
|
+
subspace = apply_user_selectors(settings, subspace)
|
1190
|
+
validate_dimensions(subspace)
|
1191
|
+
print_subspace(subspace)
|
1192
|
+
execution = prepare_subspace_execution(
|
1193
|
+
subspace, order, env, metrics=settings["metrics"], dry_run=settings["dry_run"]
|
1194
|
+
)
|
1195
|
+
validate_execution(execution, data)
|
1196
|
+
run_setup(settings, data, execution)
|
1197
|
+
run_subspace_trials(settings, data, execution)
|
1198
|
+
|
1199
|
+
|
1200
|
+
def validate_settings(data, settings):
|
1201
|
+
if settings["metrics"]:
|
1202
|
+
valid = [x["name"] for x in data["metrics"]]
|
1203
|
+
wrong = [m for m in settings["metrics"] if m not in valid]
|
1204
|
+
if len(wrong) > 0:
|
1205
|
+
hint = "available metrics: {}".format(", ".join(valid))
|
1206
|
+
report(
|
1207
|
+
LogLevel.FATAL,
|
1208
|
+
"invalid metrics",
|
1209
|
+
", ".join(wrong),
|
1210
|
+
hint=hint,
|
1211
|
+
)
|
1212
|
+
|
1213
|
+
|
1214
|
+
def launch(args):
|
1215
|
+
settings = build_settings(args)
|
1216
|
+
data = aggregate_input_data(settings)
|
1217
|
+
validate_settings(data, settings)
|
1218
|
+
env = build_environment(settings, data)
|
1219
|
+
order = define_order(settings, data)
|
1220
|
+
validate_yvars_in_env(env)
|
1221
|
+
validate_yvars_in_setup(data)
|
1222
|
+
validate_presets(settings, data)
|
1223
|
+
|
1224
|
+
if len(settings["presets"]) > 0:
|
1225
|
+
for preset_name in settings["presets"]:
|
1226
|
+
report(LogLevel.INFO, "running preset", preset_name)
|
1227
|
+
run_experiments(settings, data, order, env, preset_name)
|
1228
|
+
report(LogLevel.INFO, "completed preset", preset_name)
|
1229
|
+
else:
|
1230
|
+
run_experiments(settings, data, order, env, preset_name=None)
|
1231
|
+
|
1232
|
+
report(LogLevel.INFO, "finished")
|
1233
|
+
|
1234
|
+
if not settings["dry_run"]:
|
1235
|
+
metric_names = {m["name"] for m in data["metrics"]}
|
1236
|
+
hint = "use `yuclid plot {} -y {}` to analyze the results".format(
|
1237
|
+
settings["output"], ",".join(metric_names)
|
1238
|
+
)
|
1239
|
+
report(LogLevel.INFO, "output data written to", settings["output"], hint=hint)
|