ceci 2.0.1__tar.gz → 2.2__tar.gz
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.
- {ceci-2.0.1 → ceci-2.2}/PKG-INFO +2 -2
- {ceci-2.0.1 → ceci-2.2}/ceci/_version.py +2 -2
- {ceci-2.0.1 → ceci-2.2}/ceci/config.py +41 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/minirunner.py +25 -7
- {ceci-2.0.1 → ceci-2.2}/ceci/pipeline.py +84 -8
- {ceci-2.0.1 → ceci-2.2}/ceci/stage.py +274 -38
- {ceci-2.0.1 → ceci-2.2}/ceci/utils.py +8 -0
- {ceci-2.0.1 → ceci-2.2}/ceci.egg-info/PKG-INFO +2 -2
- {ceci-2.0.1 → ceci-2.2}/tests/test_pipeline.py +19 -1
- {ceci-2.0.1 → ceci-2.2}/tests/test_stage.py +78 -2
- {ceci-2.0.1 → ceci-2.2}/.coveragerc +0 -0
- {ceci-2.0.1 → ceci-2.2}/.github/workflows/ci.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/.github/workflows/publish.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/.gitignore +0 -0
- {ceci-2.0.1 → ceci-2.2}/.pylintrc +0 -0
- {ceci-2.0.1 → ceci-2.2}/LICENSE +0 -0
- {ceci-2.0.1 → ceci-2.2}/README.md +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/__init__.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/__main__.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/errors.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/main.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/monitor.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/sites/__init__.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/sites/ccin2p3.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/sites/local.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/sites/nersc.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/sites/site.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/tools/__init__.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/tools/ancestors.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci/update_pipelines_for_ceci_2.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci.egg-info/SOURCES.txt +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci.egg-info/dependency_links.txt +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci.egg-info/entry_points.txt +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci.egg-info/requires.txt +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci.egg-info/top_level.txt +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci.png +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci_example/__init__.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci_example/__main__.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci_example/example_stages.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci_example/types.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/ceci_example_namespace/example_stages.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/do_cover.sh +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/Makefile +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/conf.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/config.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/config2.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/index.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/installation.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/launchers.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/logo.png +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/overview.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/sites.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/stages.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/docs/tutorial.rst +0 -0
- {ceci-2.0.1 → ceci-2.2}/nb/ceci_interactive_example.ipynb +0 -0
- {ceci-2.0.1 → ceci-2.2}/pyproject.toml +0 -0
- {ceci-2.0.1 → ceci-2.2}/setup.cfg +0 -0
- {ceci-2.0.1 → ceci-2.2}/setup.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/__init__.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/config.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/config_nm.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/create_registry_entries.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/inputs/dm.txt +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/inputs/fiducial_cosmology.txt +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test.hdf5 +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_cmd.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_dask.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_dataregistry.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_executables.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_flow_chart.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_helpers.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_interactive.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_main.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_minirunner.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_monitor.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_namespace.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_python_paths.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_site.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_snapshot.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_update_script.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/test_utils.py +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/update_test_files/config.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/update_test_files/pipeline1.yml +0 -0
- {ceci-2.0.1 → ceci-2.2}/tests/update_test_files/pipeline2.yml +0 -0
{ceci-2.0.1 → ceci-2.2}/PKG-INFO
RENAMED
|
@@ -88,6 +88,10 @@ class StageParameter:
|
|
|
88
88
|
self._required = self._default is None
|
|
89
89
|
self._value = cast_value(self._dtype, self._default)
|
|
90
90
|
|
|
91
|
+
def __repr__(self):
|
|
92
|
+
req_str = 'required' if self.required else 'optional'
|
|
93
|
+
return f"Parameter({self.msg}, type: {self.dtype}, default: {self.default} [{req_str}])"
|
|
94
|
+
|
|
91
95
|
@property
|
|
92
96
|
def value(self):
|
|
93
97
|
"""Return the value"""
|
|
@@ -138,6 +142,27 @@ class StageParameter:
|
|
|
138
142
|
self._default = default
|
|
139
143
|
self.set_to_default()
|
|
140
144
|
|
|
145
|
+
def numpy_style_help_text(self):
|
|
146
|
+
"""Create a docstring followwing numpy style guidelines"""
|
|
147
|
+
s = ""
|
|
148
|
+
if self._dtype is None:
|
|
149
|
+
s += "[type not specified] "
|
|
150
|
+
else:
|
|
151
|
+
s += f"[{self._dtype.__name__}] "
|
|
152
|
+
if self._required:
|
|
153
|
+
s += "(required)"
|
|
154
|
+
else:
|
|
155
|
+
# truncate long dicts
|
|
156
|
+
if isinstance(self._default, dict) and len(self._default) > 10:
|
|
157
|
+
s += "(default={...})"
|
|
158
|
+
# truncate long lists
|
|
159
|
+
elif isinstance(self._default, list) and len(self._default) > 10:
|
|
160
|
+
s += "(default=[...])"
|
|
161
|
+
else:
|
|
162
|
+
s += f"default={self._default}"
|
|
163
|
+
s += f"\n {self._help}"
|
|
164
|
+
return s
|
|
165
|
+
|
|
141
166
|
|
|
142
167
|
class StageConfig(dict):
|
|
143
168
|
"""A small class to manage a dictionary of configuration parameters with basic type checking"""
|
|
@@ -279,3 +304,19 @@ class StageConfig(dict):
|
|
|
279
304
|
if isinstance(attr, StageParameter):
|
|
280
305
|
return attr.dtype
|
|
281
306
|
return type(attr)
|
|
307
|
+
|
|
308
|
+
def numpy_style_help_text(self):
|
|
309
|
+
"""Create a docstring followwing numpy style guidelines"""
|
|
310
|
+
if not self:
|
|
311
|
+
return "<This class has no configuration options>"
|
|
312
|
+
s = []
|
|
313
|
+
for key, val in dict.items(self):
|
|
314
|
+
if isinstance(val, StageConfig):
|
|
315
|
+
val = val[key]
|
|
316
|
+
if isinstance(val, StageParameter):
|
|
317
|
+
s.append(f"{key}: {val.numpy_style_help_text()}")
|
|
318
|
+
elif isinstance(val, type):
|
|
319
|
+
s.append(f"{key}: {val.__name__}] (required)")
|
|
320
|
+
else:
|
|
321
|
+
s.append(f"{key}: {type(val).__name__}] (default={val})")
|
|
322
|
+
return '\n\n'.join(s)
|
|
@@ -36,9 +36,10 @@ class TimeOut(RunnerError):
|
|
|
36
36
|
class FailedJob(RunnerError):
|
|
37
37
|
"""Error for when a job has failed."""
|
|
38
38
|
|
|
39
|
-
def __init__(self, msg, job_name):
|
|
39
|
+
def __init__(self, msg, job_name, run_time):
|
|
40
40
|
super().__init__(msg)
|
|
41
41
|
self.job_name = job_name
|
|
42
|
+
self.run_time = run_time
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class Node:
|
|
@@ -145,6 +146,21 @@ def null_callback(
|
|
|
145
146
|
): # pylint: disable=unused-argument,missing-function-docstring
|
|
146
147
|
pass
|
|
147
148
|
|
|
149
|
+
def make_run_time_string(seconds):
|
|
150
|
+
"""Convert the time in seconds to a string of the form
|
|
151
|
+
"X days, Y hours, Z minutes, W seconds"
|
|
152
|
+
"""
|
|
153
|
+
if seconds < 60:
|
|
154
|
+
return f"{seconds:.1f} seconds"
|
|
155
|
+
minutes, seconds = divmod(seconds, 60)
|
|
156
|
+
if minutes < 60:
|
|
157
|
+
return f"{minutes:.0f} minutes, {seconds:.1f} seconds"
|
|
158
|
+
hours, minutes = divmod(minutes, 60)
|
|
159
|
+
if hours < 24:
|
|
160
|
+
return f"{hours:.0f} hours, {minutes:.0f} minutes, {seconds:.1f} seconds"
|
|
161
|
+
days, hours = divmod(hours, 24)
|
|
162
|
+
return f"{days:.0f} days, {hours:.0f} hours, {minutes:.0f} minutes, {seconds:.1f} seconds"
|
|
163
|
+
|
|
148
164
|
|
|
149
165
|
class Runner:
|
|
150
166
|
"""The main pipeline runner class.
|
|
@@ -245,7 +261,7 @@ class Runner:
|
|
|
245
261
|
|
|
246
262
|
def abort(self):
|
|
247
263
|
"""End the pipeline and kill all running jobs."""
|
|
248
|
-
for process, _, _ in self.running:
|
|
264
|
+
for process, _, _, _ in self.running:
|
|
249
265
|
process.kill()
|
|
250
266
|
|
|
251
267
|
for node in self.nodes:
|
|
@@ -275,7 +291,7 @@ class Runner:
|
|
|
275
291
|
p = subprocess.Popen(
|
|
276
292
|
cmd, shell=True, stdout=stdout, stderr=subprocess.STDOUT
|
|
277
293
|
) # pylint: disable=consider-using-with
|
|
278
|
-
self.running.append((p, job, alloc))
|
|
294
|
+
self.running.append((p, job, alloc, default_timer()))
|
|
279
295
|
self.callback(
|
|
280
296
|
EVENT_LAUNCH,
|
|
281
297
|
{"job": job, "stdout": stdout_file, "process": p, "nodes": alloc},
|
|
@@ -337,12 +353,12 @@ class Runner:
|
|
|
337
353
|
completed_jobs = []
|
|
338
354
|
continuing_jobs = []
|
|
339
355
|
# loop through all known running ones
|
|
340
|
-
for process, job, alloc in self.running:
|
|
356
|
+
for process, job, alloc, start_time in self.running:
|
|
341
357
|
# check status
|
|
342
358
|
status = process.poll()
|
|
343
359
|
# None indicates job is still running
|
|
344
360
|
if status is None: # pragma: no cover
|
|
345
|
-
continuing_jobs.append((process, job, alloc))
|
|
361
|
+
continuing_jobs.append((process, job, alloc, start_time))
|
|
346
362
|
# status !=0 indicates error in job.
|
|
347
363
|
# kill everything
|
|
348
364
|
elif status:
|
|
@@ -355,11 +371,13 @@ class Runner:
|
|
|
355
371
|
EVENT_FAIL,
|
|
356
372
|
{"job": job, "status": status, "process": process, "nodes": alloc},
|
|
357
373
|
)
|
|
374
|
+
fail_time = make_run_time_string(default_timer() - start_time)
|
|
358
375
|
self.abort()
|
|
359
|
-
raise FailedJob(job.cmd, job.name)
|
|
376
|
+
raise FailedJob(job.cmd, job.name, fail_time)
|
|
360
377
|
# status==0 indicates sucess in job, so free resources
|
|
361
378
|
else:
|
|
362
|
-
|
|
379
|
+
run_time = make_run_time_string(default_timer() - start_time)
|
|
380
|
+
print(f"Job {job.name} has completed successfully in {run_time} seconds !")
|
|
363
381
|
# Call back with info about the successful job and how it ran
|
|
364
382
|
self.callback(
|
|
365
383
|
EVENT_COMPLETED,
|
|
@@ -6,13 +6,18 @@ import graphlib
|
|
|
6
6
|
import yaml
|
|
7
7
|
import shutil
|
|
8
8
|
from abc import abstractmethod
|
|
9
|
-
import
|
|
9
|
+
import collections
|
|
10
10
|
|
|
11
11
|
from .stage import PipelineStage
|
|
12
12
|
from . import minirunner
|
|
13
13
|
from .sites import load, get_default_site
|
|
14
14
|
from .utils import embolden, extra_paths
|
|
15
15
|
|
|
16
|
+
RESUME_MODE_RESUME = "resume"
|
|
17
|
+
RESUME_MODE_RESTART = "restart"
|
|
18
|
+
RESUME_MODE_REFUSE = "refuse"
|
|
19
|
+
|
|
20
|
+
|
|
16
21
|
|
|
17
22
|
def override_config(config, extra):
|
|
18
23
|
"""Override configuration parameters with extra parameters provided on command line
|
|
@@ -364,11 +369,16 @@ class Pipeline:
|
|
|
364
369
|
run_config = {
|
|
365
370
|
"output_dir": pipe_config.get("output_dir", "."),
|
|
366
371
|
"log_dir": pipe_config.get("log_dir", "."),
|
|
367
|
-
"resume": pipe_config.get("resume",
|
|
372
|
+
"resume": pipe_config.get("resume", RESUME_MODE_RESUME),
|
|
368
373
|
"flow_chart": pipe_config.get("flow_chart", ""),
|
|
369
374
|
"registry": pipe_config.get("registry", None),
|
|
370
375
|
}
|
|
371
376
|
|
|
377
|
+
if run_config["resume"] is True:
|
|
378
|
+
run_config["resume"] = RESUME_MODE_RESUME
|
|
379
|
+
elif run_config["resume"] is False:
|
|
380
|
+
run_config["resume"] = RESUME_MODE_RESTART
|
|
381
|
+
|
|
372
382
|
launcher_dict = dict(cwl=CWLPipeline, parsl=ParslPipeline, mini=MiniPipeline)
|
|
373
383
|
|
|
374
384
|
if pipe_config.get("flow_chart", False):
|
|
@@ -514,18 +524,23 @@ class Pipeline:
|
|
|
514
524
|
else:
|
|
515
525
|
raise ValueError("Must specify either id or name in registry lookup")
|
|
516
526
|
|
|
527
|
+
# Whatever the lookup, we always require a dataset which has not beed deleted or replaced
|
|
528
|
+
status_filter = registry.Query.gen_filter("dataset.status", "==", 1)
|
|
529
|
+
|
|
517
530
|
# Main finder method
|
|
518
|
-
results = registry.Query.find_datasets(["dataset.dataset_id"], [filter])
|
|
531
|
+
results = registry.Query.find_datasets(["dataset.dataset_id"], [filter, status_filter])
|
|
519
532
|
|
|
520
533
|
# Check that we find exactly one dataset matching the query
|
|
521
|
-
|
|
522
|
-
if len(results) == 0:
|
|
534
|
+
if not results:
|
|
523
535
|
raise ValueError(f"Could not find any dataset matching {info} in registry")
|
|
524
|
-
|
|
536
|
+
|
|
537
|
+
results = results['dataset.dataset_id']
|
|
538
|
+
|
|
539
|
+
if len(results) > 1:
|
|
525
540
|
raise ValueError(f"Found multiple datasets matching {info} in registry")
|
|
526
541
|
|
|
527
542
|
# Get the absolute path
|
|
528
|
-
return registry.Query.get_dataset_absolute_path(results[0]
|
|
543
|
+
return registry.Query.get_dataset_absolute_path(results[0])
|
|
529
544
|
|
|
530
545
|
|
|
531
546
|
def process_overall_inputs(self, inputs):
|
|
@@ -820,7 +835,9 @@ class Pipeline:
|
|
|
820
835
|
# In interactive use the stage object might already be created
|
|
821
836
|
# otherwise we need to create it
|
|
822
837
|
if sec.stage_obj is not None:
|
|
838
|
+
orig_stage_config = stages_config_data.get(stage_name, {}).copy()
|
|
823
839
|
stage = sec.stage_obj
|
|
840
|
+
stage.config.update(**orig_stage_config)
|
|
824
841
|
else:
|
|
825
842
|
# Find the inputs for this stage and set up the arguments
|
|
826
843
|
# to the stage init method
|
|
@@ -949,7 +966,24 @@ class Pipeline:
|
|
|
949
966
|
|
|
950
967
|
def should_skip_stage(self, stage):
|
|
951
968
|
"""Return true if we should skip a stage because it is finished and we are in resume mode"""
|
|
952
|
-
|
|
969
|
+
resume_mode = self.run_config["resume"]
|
|
970
|
+
if resume_mode is True:
|
|
971
|
+
resume_mode = "resume"
|
|
972
|
+
elif resume_mode is False:
|
|
973
|
+
resume_mode = "restart"
|
|
974
|
+
|
|
975
|
+
if resume_mode == "restart":
|
|
976
|
+
return False
|
|
977
|
+
|
|
978
|
+
outputs = stage.find_outputs(self.run_config["output_dir"]).values()
|
|
979
|
+
|
|
980
|
+
if resume_mode == "resume":
|
|
981
|
+
return all(os.path.exists(output) for output in outputs)
|
|
982
|
+
elif resume_mode == "refuse":
|
|
983
|
+
if any(os.path.exists(output) for output in outputs):
|
|
984
|
+
raise RuntimeError(f"Output files already exist for stage {stage} and resume mode is 'refuse'")
|
|
985
|
+
else:
|
|
986
|
+
raise ValueError(f"Unknown resume mode: {resume_mode}")
|
|
953
987
|
|
|
954
988
|
def save(self, pipefile, stagefile=None, reduce_config=False, **kwargs):
|
|
955
989
|
"""Save this pipeline state to a yaml file
|
|
@@ -1039,6 +1073,10 @@ class Pipeline:
|
|
|
1039
1073
|
# Nodes we have already added
|
|
1040
1074
|
seen = set()
|
|
1041
1075
|
|
|
1076
|
+
# Dictionary to track nodes by their inputs and outputs
|
|
1077
|
+
node_groups = {}
|
|
1078
|
+
|
|
1079
|
+
|
|
1042
1080
|
# Add overall pipeline inputs
|
|
1043
1081
|
for inp in self.overall_inputs.keys():
|
|
1044
1082
|
graph.add_node(inp, shape="box", color="gold", style="filled")
|
|
@@ -1064,6 +1102,43 @@ class Pipeline:
|
|
|
1064
1102
|
seen.add(out)
|
|
1065
1103
|
graph.add_edge(stage.instance_name, out, color="black")
|
|
1066
1104
|
|
|
1105
|
+
# We want to group together all the files that all created
|
|
1106
|
+
# by the same stage and also all used by the same stages, to
|
|
1107
|
+
# reduce the number of nodes in the graph and make it more readable.
|
|
1108
|
+
# First we find that grouping.
|
|
1109
|
+
node_groups = collections.defaultdict(list)
|
|
1110
|
+
for node in graph.nodes_iter():
|
|
1111
|
+
# only affect the nodes representing files
|
|
1112
|
+
if node.attr['color'] != "skyblue":
|
|
1113
|
+
continue
|
|
1114
|
+
# Find the stage node that created this file,
|
|
1115
|
+
# and all the stage nodes that make use of it
|
|
1116
|
+
edge_in = graph.in_edges(node)[0]
|
|
1117
|
+
creator = edge_in[0]
|
|
1118
|
+
users = []
|
|
1119
|
+
for edge in graph.out_edges(node):
|
|
1120
|
+
users.append(edge[1])
|
|
1121
|
+
key = (creator, tuple(users))
|
|
1122
|
+
node_groups[key].append(node)
|
|
1123
|
+
|
|
1124
|
+
# Now we remove all the groups of nodes with more than one in
|
|
1125
|
+
# and replace them with a single node
|
|
1126
|
+
for key, nodes in node_groups.items():
|
|
1127
|
+
if len(nodes) > 1:
|
|
1128
|
+
if len(nodes) > 4:
|
|
1129
|
+
# make a string with two nodes per line
|
|
1130
|
+
node_names = []
|
|
1131
|
+
for i in range(0, len(nodes), 2):
|
|
1132
|
+
node_names.append(", ".join(nodes[i:i+2]))
|
|
1133
|
+
new_node = "\n".join(node_names)
|
|
1134
|
+
else:
|
|
1135
|
+
new_node = "\n".join(nodes)
|
|
1136
|
+
graph.remove_nodes_from(nodes)
|
|
1137
|
+
graph.add_node(new_node, shape="box", color="skyblue", style="filled")
|
|
1138
|
+
graph.add_edge(key[0], new_node, color="black")
|
|
1139
|
+
for user in key[1]:
|
|
1140
|
+
graph.add_edge(new_node, user, color="black")
|
|
1141
|
+
|
|
1067
1142
|
# finally, output the stage to file
|
|
1068
1143
|
if filename.endswith(".dot"):
|
|
1069
1144
|
graph.write(filename)
|
|
@@ -1432,6 +1507,7 @@ class MiniPipeline(Pipeline):
|
|
|
1432
1507
|
f"""
|
|
1433
1508
|
*************************************************
|
|
1434
1509
|
Error running pipeline stage {error.job_name}.
|
|
1510
|
+
Failed after {error.run_time}.
|
|
1435
1511
|
|
|
1436
1512
|
Standard output and error streams in {log_dir}/{error.job_name}.out
|
|
1437
1513
|
*************************************************
|
|
@@ -9,11 +9,13 @@ import cProfile
|
|
|
9
9
|
import pdb
|
|
10
10
|
import datetime
|
|
11
11
|
import warnings
|
|
12
|
+
import socket
|
|
12
13
|
|
|
13
14
|
from abc import abstractmethod
|
|
14
15
|
from . import errors
|
|
15
16
|
from .monitor import MemoryMonitor
|
|
16
17
|
from .config import StageParameter, StageConfig, cast_to_streamable
|
|
18
|
+
from .utils import activate_tracing
|
|
17
19
|
|
|
18
20
|
SERIAL = "serial"
|
|
19
21
|
MPI_PARALLEL = "mpi"
|
|
@@ -146,9 +148,19 @@ class PipelineStage:
|
|
|
146
148
|
|
|
147
149
|
@abstractmethod
|
|
148
150
|
def run(self): # pragma: no cover
|
|
149
|
-
"""Run the stage and return the execution status
|
|
151
|
+
"""Run the stage and return the execution status.
|
|
152
|
+
|
|
153
|
+
Subclasses must implemented this method.
|
|
154
|
+
"""
|
|
150
155
|
raise NotImplementedError("run")
|
|
151
156
|
|
|
157
|
+
def validate(self):
|
|
158
|
+
"""Check that the inputs actually have the data needed for execution,
|
|
159
|
+
This is called before the run method. It is an optional stage, meant
|
|
160
|
+
for checking that the input to the stage is actual in the form and
|
|
161
|
+
shape needed before an expensive run is executed."""
|
|
162
|
+
pass
|
|
163
|
+
|
|
152
164
|
def load_configs(self, args):
|
|
153
165
|
"""
|
|
154
166
|
Load the configuraiton
|
|
@@ -340,14 +352,15 @@ class PipelineStage:
|
|
|
340
352
|
path = pathlib.Path(filename).resolve()
|
|
341
353
|
|
|
342
354
|
# Add a description of the parameters to the end of the docstring
|
|
343
|
-
|
|
355
|
+
# If no config options are specified, omit this.
|
|
356
|
+
if stage_is_complete and cls.config_options:
|
|
344
357
|
config_text = cls._describe_configuration_text()
|
|
345
358
|
if cls.__doc__ is None:
|
|
346
|
-
cls.__doc__ = f"Stage {cls.name}\n\
|
|
359
|
+
cls.__doc__ = f"Stage {cls.name}\n\nParameters\n----------\n{config_text}"
|
|
347
360
|
else:
|
|
348
361
|
# strip any existing configuration text from parent classes that is at the end of the doctring
|
|
349
|
-
cls.__doc__ = cls.__doc__.split("
|
|
350
|
-
cls.__doc__ += f"\n\
|
|
362
|
+
cls.__doc__ = cls.__doc__.split("Parameters")[0]
|
|
363
|
+
cls.__doc__ += f"\n\nParameters\n----------\n{config_text}"
|
|
351
364
|
|
|
352
365
|
# Register the class
|
|
353
366
|
if stage_is_complete:
|
|
@@ -423,24 +436,24 @@ class PipelineStage:
|
|
|
423
436
|
s = []
|
|
424
437
|
if cls.config_options is None:
|
|
425
438
|
return "<This class has no configuration options>"
|
|
439
|
+
|
|
426
440
|
for name, val in cls.config_options.items():
|
|
441
|
+
if isinstance(val, StageConfig):
|
|
442
|
+
val = val[name]
|
|
427
443
|
if isinstance(val, StageParameter):
|
|
428
|
-
|
|
429
|
-
if val.dtype is None:
|
|
430
|
-
txt = f"[type not specified]: {val._help} (required)"
|
|
431
|
-
else:
|
|
432
|
-
txt = f"[{val.dtype.__name__}]: {val._help} (required)"
|
|
433
|
-
else:
|
|
434
|
-
if val.dtype is None:
|
|
435
|
-
txt = f"[type not specified]: {val._help} (default={val.default})"
|
|
436
|
-
else:
|
|
437
|
-
txt = f"[{val.dtype.__name__}]: {val._help} (default={val.default})"
|
|
444
|
+
s.append(f"{name}: {val.numpy_style_help_text()}")
|
|
438
445
|
elif isinstance(val, type):
|
|
439
|
-
|
|
446
|
+
s.append(f"{name}: {val.__name__}] (required)")
|
|
440
447
|
else:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
448
|
+
s.append(f"{name}: {type(val).__name__}] (default={val})")
|
|
449
|
+
|
|
450
|
+
for input_ in cls.inputs:
|
|
451
|
+
s.append(f"{input_[0]}: {input_[1].__name__} (INPUT)")
|
|
452
|
+
for output_ in cls.outputs:
|
|
453
|
+
s.append(f"{output_[0]}: {output_[1].__name__} (OUTPUT)")
|
|
454
|
+
|
|
455
|
+
return '\n\n'.join(s)
|
|
456
|
+
|
|
444
457
|
|
|
445
458
|
@classmethod
|
|
446
459
|
def usage(cls): # pragma: no cover
|
|
@@ -603,6 +616,12 @@ I currently know about these stages:
|
|
|
603
616
|
help="Report memory use. Argument gives interval in seconds between reports",
|
|
604
617
|
)
|
|
605
618
|
|
|
619
|
+
parser.add_argument(
|
|
620
|
+
"--trace",
|
|
621
|
+
action="store_true",
|
|
622
|
+
help="Enable sending a signal to the process that prints a trace wherever it is",
|
|
623
|
+
)
|
|
624
|
+
|
|
606
625
|
# Error message we will return if --mpi used on a non-supported
|
|
607
626
|
# stage.
|
|
608
627
|
mpi_err = (
|
|
@@ -661,6 +680,21 @@ I currently know about these stages:
|
|
|
661
680
|
if args.memmon: # pragma: no cover
|
|
662
681
|
monitor = MemoryMonitor.start_in_thread(interval=args.memmon)
|
|
663
682
|
|
|
683
|
+
if args.trace:
|
|
684
|
+
activate_tracing(stage._rank)
|
|
685
|
+
|
|
686
|
+
# Now we try to see if the validation step has been changed,
|
|
687
|
+
# if it has then we will run the validation step, and raise any errors
|
|
688
|
+
try:
|
|
689
|
+
stage.validate()
|
|
690
|
+
except Exception as error:
|
|
691
|
+
if stage.rank==0:
|
|
692
|
+
print(f"Looks like there is an validation error in: {cls.name}",
|
|
693
|
+
"the input data for this stage did not pass the checks implemented on it.")
|
|
694
|
+
print(error)
|
|
695
|
+
raise
|
|
696
|
+
|
|
697
|
+
|
|
664
698
|
try:
|
|
665
699
|
stage.run()
|
|
666
700
|
except Exception as error: # pragma: no cover
|
|
@@ -781,13 +815,24 @@ I currently know about these stages:
|
|
|
781
815
|
|
|
782
816
|
def is_mpi(self):
|
|
783
817
|
"""
|
|
784
|
-
|
|
818
|
+
Check if the stage is being run under MPI.
|
|
819
|
+
|
|
820
|
+
Returns
|
|
821
|
+
-------
|
|
822
|
+
bool
|
|
823
|
+
True if the stage is being run under MPI
|
|
785
824
|
"""
|
|
786
825
|
return self._parallel == MPI_PARALLEL
|
|
787
826
|
|
|
788
827
|
def is_dask(self):
|
|
789
828
|
"""
|
|
790
|
-
|
|
829
|
+
Check if the stage is being run in parallel with Dask.
|
|
830
|
+
|
|
831
|
+
Returns
|
|
832
|
+
-------
|
|
833
|
+
bool
|
|
834
|
+
True if the stage is being run under MPI
|
|
835
|
+
|
|
791
836
|
"""
|
|
792
837
|
return self._parallel == DASK_PARALLEL
|
|
793
838
|
|
|
@@ -938,6 +983,11 @@ I currently know about these stages:
|
|
|
938
983
|
Parallel: bool
|
|
939
984
|
Whether to split data by rank or just give all procs all data.
|
|
940
985
|
Default=True
|
|
986
|
+
|
|
987
|
+
Returns
|
|
988
|
+
-------
|
|
989
|
+
start, end: tuple
|
|
990
|
+
The start and end of the range of rows to be read by this process
|
|
941
991
|
"""
|
|
942
992
|
n_chunks = n_rows // chunk_rows
|
|
943
993
|
if n_chunks * chunk_rows < n_rows: # pragma: no cover
|
|
@@ -959,6 +1009,17 @@ I currently know about these stages:
|
|
|
959
1009
|
"""
|
|
960
1010
|
Return the path of an input file with the given tag,
|
|
961
1011
|
which can be aliased.
|
|
1012
|
+
|
|
1013
|
+
Parameters
|
|
1014
|
+
----------
|
|
1015
|
+
tag: str
|
|
1016
|
+
Tag as listed in self.outputs
|
|
1017
|
+
|
|
1018
|
+
Returns
|
|
1019
|
+
-------
|
|
1020
|
+
path: str
|
|
1021
|
+
The path to the output file
|
|
1022
|
+
|
|
962
1023
|
"""
|
|
963
1024
|
tag = self.get_aliased_tag(tag)
|
|
964
1025
|
return self._inputs[tag]
|
|
@@ -971,7 +1032,21 @@ I currently know about these stages:
|
|
|
971
1032
|
which can be aliased already.
|
|
972
1033
|
|
|
973
1034
|
If final_name is False then use a temporary name - file will
|
|
974
|
-
be moved to its final name at the end
|
|
1035
|
+
be moved to its final name at the end. The temporary name
|
|
1036
|
+
is prefixed with `inprogress_`.
|
|
1037
|
+
|
|
1038
|
+
Parameters
|
|
1039
|
+
----------
|
|
1040
|
+
tag: str
|
|
1041
|
+
Tag as listed in self.outputs
|
|
1042
|
+
|
|
1043
|
+
final_name: bool
|
|
1044
|
+
Default=False. Whether to save to the final name.
|
|
1045
|
+
|
|
1046
|
+
Returns
|
|
1047
|
+
-------
|
|
1048
|
+
path: str
|
|
1049
|
+
The path to the output file
|
|
975
1050
|
"""
|
|
976
1051
|
|
|
977
1052
|
tag = self.get_aliased_tag(tag)
|
|
@@ -994,6 +1069,21 @@ I currently know about these stages:
|
|
|
994
1069
|
For specialized file types like FITS or HDF5 it will return
|
|
995
1070
|
a more specific object - see the types.py file for more info.
|
|
996
1071
|
|
|
1072
|
+
Parameters
|
|
1073
|
+
----------
|
|
1074
|
+
tag: str
|
|
1075
|
+
Tag as listed in self.inputs
|
|
1076
|
+
|
|
1077
|
+
wrapper: bool
|
|
1078
|
+
Whether to return an underlying file object (False) or a data type instance (True)
|
|
1079
|
+
|
|
1080
|
+
**kwargs: dict
|
|
1081
|
+
Extra arguments to pass to the file class constructor
|
|
1082
|
+
|
|
1083
|
+
Returns
|
|
1084
|
+
-------
|
|
1085
|
+
obj: file or object
|
|
1086
|
+
The opened file or object
|
|
997
1087
|
"""
|
|
998
1088
|
path = self.get_input(tag)
|
|
999
1089
|
input_class = self.get_input_type(tag)
|
|
@@ -1010,7 +1100,7 @@ I currently know about these stages:
|
|
|
1010
1100
|
Find and open an output file with the given tag, in write mode.
|
|
1011
1101
|
|
|
1012
1102
|
If final_name is True then they will be opened using their final
|
|
1013
|
-
target output name. Otherwise we will prepend
|
|
1103
|
+
target output name. Otherwise we will prepend `inprogress_` to their
|
|
1014
1104
|
file name. This means we know that if the final file exists then it
|
|
1015
1105
|
is completed.
|
|
1016
1106
|
|
|
@@ -1021,12 +1111,11 @@ I currently know about these stages:
|
|
|
1021
1111
|
|
|
1022
1112
|
Parameters
|
|
1023
1113
|
----------
|
|
1024
|
-
|
|
1025
1114
|
tag: str
|
|
1026
1115
|
Tag as listed in self.outputs
|
|
1027
1116
|
|
|
1028
1117
|
wrapper: bool
|
|
1029
|
-
|
|
1118
|
+
Whether to return an underlying file object (False) or a data type instance (True)
|
|
1030
1119
|
|
|
1031
1120
|
final_name: bool
|
|
1032
1121
|
Default=False. Whether to save to
|
|
@@ -1034,6 +1123,10 @@ I currently know about these stages:
|
|
|
1034
1123
|
**kwargs:
|
|
1035
1124
|
Extra args are passed on to the file's class constructor.
|
|
1036
1125
|
|
|
1126
|
+
Returns
|
|
1127
|
+
-------
|
|
1128
|
+
obj: file or object
|
|
1129
|
+
The opened file or object
|
|
1037
1130
|
"""
|
|
1038
1131
|
path = self.get_output(tag, final_name=final_name)
|
|
1039
1132
|
output_class = self.get_output_type(tag)
|
|
@@ -1078,33 +1171,63 @@ I currently know about these stages:
|
|
|
1078
1171
|
@classmethod
|
|
1079
1172
|
def inputs_(cls):
|
|
1080
1173
|
"""
|
|
1081
|
-
Return the dict
|
|
1174
|
+
Return the dict mapping input tags to file names.
|
|
1175
|
+
|
|
1176
|
+
Returns
|
|
1177
|
+
-------
|
|
1178
|
+
in_dict : dict[str:str]
|
|
1082
1179
|
"""
|
|
1083
1180
|
return cls.inputs # pylint: disable=no-member
|
|
1084
1181
|
|
|
1085
1182
|
@classmethod
|
|
1086
1183
|
def outputs_(cls):
|
|
1087
1184
|
"""
|
|
1088
|
-
Return the dict
|
|
1185
|
+
Return the dict mapping output tags to file names.
|
|
1186
|
+
|
|
1187
|
+
Returns
|
|
1188
|
+
-------
|
|
1189
|
+
out_dict : dict[str:str]
|
|
1089
1190
|
"""
|
|
1090
1191
|
return cls.outputs # pylint: disable=no-member
|
|
1091
1192
|
|
|
1092
1193
|
@classmethod
|
|
1093
1194
|
def output_tags(cls):
|
|
1094
1195
|
"""
|
|
1095
|
-
Return the list of output tags required by this stage
|
|
1196
|
+
Return the list of output tags required by this stage.
|
|
1197
|
+
|
|
1198
|
+
Returns
|
|
1199
|
+
-------
|
|
1200
|
+
out_tags : list[str]
|
|
1201
|
+
The list of output tags
|
|
1096
1202
|
"""
|
|
1097
1203
|
return [tag for tag, _ in cls.outputs_()]
|
|
1098
1204
|
|
|
1099
1205
|
@classmethod
|
|
1100
1206
|
def input_tags(cls):
|
|
1101
1207
|
"""
|
|
1102
|
-
Return the list of input tags required by this stage
|
|
1208
|
+
Return the list of input tags required by this stage.
|
|
1209
|
+
|
|
1210
|
+
Returns
|
|
1211
|
+
-------
|
|
1212
|
+
in_tags : list[str]
|
|
1213
|
+
The list of input tags
|
|
1103
1214
|
"""
|
|
1104
1215
|
return [tag for tag, _ in cls.inputs_()]
|
|
1105
1216
|
|
|
1106
1217
|
def get_input_type(self, tag):
|
|
1107
|
-
"""
|
|
1218
|
+
"""
|
|
1219
|
+
Return the file type class of an input file with the given tag.
|
|
1220
|
+
|
|
1221
|
+
Parameters
|
|
1222
|
+
----------
|
|
1223
|
+
tag : str
|
|
1224
|
+
The tag of the input file
|
|
1225
|
+
|
|
1226
|
+
Returns
|
|
1227
|
+
-------
|
|
1228
|
+
ftype : FileType
|
|
1229
|
+
The file type class
|
|
1230
|
+
"""
|
|
1108
1231
|
tag = self.get_aliased_tag(tag)
|
|
1109
1232
|
for t, dt in self.inputs_():
|
|
1110
1233
|
t = self.get_aliased_tag(t)
|
|
@@ -1113,7 +1236,19 @@ I currently know about these stages:
|
|
|
1113
1236
|
raise ValueError(f"Tag {tag} is not a known input") # pragma: no cover
|
|
1114
1237
|
|
|
1115
1238
|
def get_output_type(self, tag):
|
|
1116
|
-
"""
|
|
1239
|
+
"""
|
|
1240
|
+
Return the file type class of an output file with the given tag.
|
|
1241
|
+
|
|
1242
|
+
Parameters
|
|
1243
|
+
----------
|
|
1244
|
+
tag : str
|
|
1245
|
+
The tag of the output file
|
|
1246
|
+
|
|
1247
|
+
Returns
|
|
1248
|
+
-------
|
|
1249
|
+
ftype : FileType
|
|
1250
|
+
The file type class
|
|
1251
|
+
"""
|
|
1117
1252
|
tag = self.get_aliased_tag(tag)
|
|
1118
1253
|
for t, dt in self.outputs_():
|
|
1119
1254
|
t = self.get_aliased_tag(t)
|
|
@@ -1133,8 +1268,12 @@ I currently know about these stages:
|
|
|
1133
1268
|
@property
|
|
1134
1269
|
def config(self):
|
|
1135
1270
|
"""
|
|
1136
|
-
|
|
1271
|
+
The configuration dictionary for this stage, aggregating command
|
|
1137
1272
|
line options and optional configuration file.
|
|
1273
|
+
|
|
1274
|
+
Options specified in the subclass variable `config_options` are
|
|
1275
|
+
read from the configuration file, command line, or `make_stage` choices,
|
|
1276
|
+
and stored in this dictionary.
|
|
1138
1277
|
"""
|
|
1139
1278
|
return self._configs
|
|
1140
1279
|
|
|
@@ -1252,12 +1391,6 @@ I currently know about these stages:
|
|
|
1252
1391
|
f"{tag:20} : {aliased_tag:20} :{str(ftype):20} : {self._outputs[aliased_tag]}\n"
|
|
1253
1392
|
)
|
|
1254
1393
|
|
|
1255
|
-
def should_skip(self, run_config):
|
|
1256
|
-
"""Return true if we should skip a stage b/c it's outputs already exist and we are in resume mode"""
|
|
1257
|
-
outputs = self.find_outputs(run_config["output_dir"]).values()
|
|
1258
|
-
already_run_stage = all(os.path.exists(output) for output in outputs)
|
|
1259
|
-
return already_run_stage and run_config["resume"]
|
|
1260
|
-
|
|
1261
1394
|
def already_finished(self):
|
|
1262
1395
|
"""Print a warning that a stage is being skipped"""
|
|
1263
1396
|
print(f"Skipping stage {self.instance_name} because its outputs exist already")
|
|
@@ -1269,6 +1402,7 @@ I currently know about these stages:
|
|
|
1269
1402
|
Loop through chunks of the input data from a FITS file with the given tag
|
|
1270
1403
|
|
|
1271
1404
|
TODO: add ceci tests of this functions
|
|
1405
|
+
|
|
1272
1406
|
Parameters
|
|
1273
1407
|
----------
|
|
1274
1408
|
tag: str
|
|
@@ -1357,6 +1491,59 @@ I currently know about these stages:
|
|
|
1357
1491
|
data = {col: group[col][start:end] for col in cols}
|
|
1358
1492
|
yield start, end, data
|
|
1359
1493
|
|
|
1494
|
+
def combined_iterators(self, rows, *inputs, parallel=True):
|
|
1495
|
+
"""
|
|
1496
|
+
Iterate through multiple files at the same time.
|
|
1497
|
+
|
|
1498
|
+
If you have more several HDF files with the some
|
|
1499
|
+
columns of the same length then you can use this method to
|
|
1500
|
+
iterate through them all at once, and combine the data from
|
|
1501
|
+
all of them into a single dictionary.
|
|
1502
|
+
|
|
1503
|
+
Parameters
|
|
1504
|
+
----------
|
|
1505
|
+
rows: int
|
|
1506
|
+
The number of rows to read in each chunk
|
|
1507
|
+
|
|
1508
|
+
*inputs: list
|
|
1509
|
+
A list of (tag, group, cols) triples for each file to read.
|
|
1510
|
+
In each case tag is the input file name tag, group is the
|
|
1511
|
+
group within the HDF5 file to read, and cols is a list of
|
|
1512
|
+
columns to read from that group. Specify multiple triplets
|
|
1513
|
+
to read from multiple files
|
|
1514
|
+
|
|
1515
|
+
parallel: bool
|
|
1516
|
+
Whether to split up data among processes (parallel=True) or give
|
|
1517
|
+
all processes all data (parallel=False). Default = True.
|
|
1518
|
+
|
|
1519
|
+
Returns
|
|
1520
|
+
-------
|
|
1521
|
+
it: iterator
|
|
1522
|
+
Iterator yielding (int, int, dict) tuples of (start, end, data)
|
|
1523
|
+
"""
|
|
1524
|
+
if not len(inputs) % 3 == 0:
|
|
1525
|
+
raise ValueError(
|
|
1526
|
+
"Arguments to combined_iterators should be in threes: "
|
|
1527
|
+
"tag, group, value"
|
|
1528
|
+
)
|
|
1529
|
+
n = len(inputs) // 3
|
|
1530
|
+
|
|
1531
|
+
iterators = []
|
|
1532
|
+
for i in range(n):
|
|
1533
|
+
tag = inputs[3 * i]
|
|
1534
|
+
section = inputs[3 * i + 1]
|
|
1535
|
+
cols = inputs[3 * i + 2]
|
|
1536
|
+
iterators.append(
|
|
1537
|
+
self.iterate_hdf(tag, section, cols, rows, parallel=parallel)
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
for it in zip(*iterators):
|
|
1541
|
+
data = {}
|
|
1542
|
+
for (s, e, d) in it:
|
|
1543
|
+
data.update(d)
|
|
1544
|
+
yield s, e, data
|
|
1545
|
+
|
|
1546
|
+
|
|
1360
1547
|
################################
|
|
1361
1548
|
# Pipeline-related methods
|
|
1362
1549
|
################################
|
|
@@ -1556,3 +1743,52 @@ I currently know about these stages:
|
|
|
1556
1743
|
# cwl_tool.metadata = cwlgen.Metadata(**metadata)
|
|
1557
1744
|
|
|
1558
1745
|
return cwl_tool
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
def time_stamp(self, tag):
|
|
1749
|
+
"""
|
|
1750
|
+
Print a time stamp with an optional tag.
|
|
1751
|
+
|
|
1752
|
+
Parameters
|
|
1753
|
+
----------
|
|
1754
|
+
tag: str
|
|
1755
|
+
Additional info to print in the output line. Default is empty.
|
|
1756
|
+
"""
|
|
1757
|
+
t = datetime.datetime.now()
|
|
1758
|
+
print(f"Process {self.rank}: {tag} {t}")
|
|
1759
|
+
sys.stdout.flush()
|
|
1760
|
+
|
|
1761
|
+
def memory_report(self, tag=None):
|
|
1762
|
+
"""
|
|
1763
|
+
Print a report about memory currently available
|
|
1764
|
+
on the node the process is running on.
|
|
1765
|
+
|
|
1766
|
+
Parameters
|
|
1767
|
+
----------
|
|
1768
|
+
tag: str
|
|
1769
|
+
Additional info to print in the output line. Default is empty.
|
|
1770
|
+
"""
|
|
1771
|
+
import psutil
|
|
1772
|
+
|
|
1773
|
+
t = datetime.datetime.now()
|
|
1774
|
+
|
|
1775
|
+
# The different types of memory are really fiddly and don't
|
|
1776
|
+
# correspond to how you usually imagine. The simplest thing
|
|
1777
|
+
# to report here is just how much memory is left on the machine.
|
|
1778
|
+
mem = psutil.virtual_memory()
|
|
1779
|
+
avail = mem.available / 1024**3
|
|
1780
|
+
total = mem.total / 1024**3
|
|
1781
|
+
|
|
1782
|
+
if tag is None:
|
|
1783
|
+
tag = ""
|
|
1784
|
+
else:
|
|
1785
|
+
tag = f" {tag}:"
|
|
1786
|
+
|
|
1787
|
+
# This gives you the name of the host. At NERSC that is the node name
|
|
1788
|
+
host = socket.gethostname()
|
|
1789
|
+
|
|
1790
|
+
# Print messsage
|
|
1791
|
+
print(
|
|
1792
|
+
f"{t}: Process {self.rank}:{tag} Remaining memory on {host} {avail:.1f} GB / {total:.1f} GB"
|
|
1793
|
+
)
|
|
1794
|
+
sys.stdout.flush()
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
4
|
import sys
|
|
5
5
|
import os
|
|
6
|
+
import signal
|
|
7
|
+
import faulthandler
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def add_python_path(path, start):
|
|
@@ -107,3 +109,9 @@ def embolden(text):
|
|
|
107
109
|
Emboldened text
|
|
108
110
|
"""
|
|
109
111
|
return "\033[1m" + text + "\033[0m"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def activate_tracing(rank):
|
|
115
|
+
pid = os.getpid()
|
|
116
|
+
print(f"Activating trace mode for process {rank}. Trigger trace using command: kill -s {signal.SIGUSR1} {pid}")
|
|
117
|
+
faulthandler.register(signal.SIGUSR1)
|
|
@@ -130,7 +130,7 @@ class FailingStage(PipelineStage):
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def _return_value_test_(resume):
|
|
133
|
-
expected_status = 0 if resume else 1
|
|
133
|
+
expected_status = 0 if ((resume == "resume") or (resume is True)) else 1
|
|
134
134
|
# Mini pipeline should not run
|
|
135
135
|
launcher_config = {"interval": 0.5, "name": "mini"}
|
|
136
136
|
run_config = {
|
|
@@ -159,10 +159,28 @@ def _return_value_test_(resume):
|
|
|
159
159
|
|
|
160
160
|
def test_resume():
|
|
161
161
|
_return_value_test_(True)
|
|
162
|
+
_return_value_test_("resume")
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
def test_fail():
|
|
165
166
|
_return_value_test_(False)
|
|
167
|
+
_return_value_test_("restart")
|
|
168
|
+
|
|
169
|
+
def test_refuse():
|
|
170
|
+
launcher_config = {"interval": 0.5, "name": "mini"}
|
|
171
|
+
run_config = {
|
|
172
|
+
"log_dir": "./tests/logs",
|
|
173
|
+
"output_dir": "./tests/inputs",
|
|
174
|
+
"resume": "refuse",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pipeline = MiniPipeline([{"name": "FailingStage"}], launcher_config)
|
|
178
|
+
|
|
179
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
180
|
+
pipeline.initialize({}, run_config, "tests/config.yml")
|
|
181
|
+
pipeline.run()
|
|
182
|
+
|
|
183
|
+
assert "Output files already exist for stage" in str(excinfo.value)
|
|
166
184
|
|
|
167
185
|
|
|
168
186
|
def test_dry_run(mocker):
|
|
@@ -5,8 +5,12 @@ import numpy as np
|
|
|
5
5
|
from ceci.errors import *
|
|
6
6
|
import pytest
|
|
7
7
|
import h5py
|
|
8
|
+
import sys
|
|
8
9
|
import os
|
|
9
|
-
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
import subprocess
|
|
13
|
+
import signal
|
|
10
14
|
# TODO: test MPI facilities properly with:
|
|
11
15
|
# https://github.com/rmjarvis/TreeCorr/blob/releases/4.1/tests/mock_mpi.py
|
|
12
16
|
|
|
@@ -145,7 +149,7 @@ def test_parameter():
|
|
|
145
149
|
assert stage_1.config.b == 'puffins are not extinct?'
|
|
146
150
|
assert 1 in stage_1.config.c
|
|
147
151
|
assert len(stage_1.config.d) == 0
|
|
148
|
-
assert "
|
|
152
|
+
assert "[list] default=[1, 2, 3]" in stage_1.__doc__
|
|
149
153
|
stage_1.describe_configuration()
|
|
150
154
|
|
|
151
155
|
cmd = "TestStage", "--a", "6", "--b", "puffins are not extinct?", "--inp", "dummy"
|
|
@@ -568,7 +572,79 @@ def test_wrong_mpi_flag():
|
|
|
568
572
|
with pytest.raises(ValueError):
|
|
569
573
|
assert LimaSerial.parse_command_line(["LimaSerial", "--mpi"]).mpi
|
|
570
574
|
|
|
575
|
+
@pytest.mark.skipif(os.environ.get("GITHUB_ACTIONS") == "true", reason="Doesn't work on github actions")
|
|
576
|
+
def test_tracing():
|
|
577
|
+
with open("mike_stage.py", "w") as f:
|
|
578
|
+
f.write("""
|
|
579
|
+
import ceci
|
|
580
|
+
import time
|
|
581
|
+
class Mike(ceci.PipelineStage):
|
|
582
|
+
name = "Mike"
|
|
583
|
+
inputs = []
|
|
584
|
+
outputs = []
|
|
585
|
+
config_options = {}
|
|
586
|
+
|
|
587
|
+
def run(self):
|
|
588
|
+
time.sleep(6)
|
|
589
|
+
print("Mike complete")
|
|
571
590
|
|
|
591
|
+
if __name__ == "__main__":
|
|
592
|
+
ceci.PipelineStage.main()
|
|
593
|
+
""")
|
|
594
|
+
with open("config.yml", "w") as f:
|
|
595
|
+
f.write("{}")
|
|
596
|
+
|
|
597
|
+
cmd = f"{sys.executable} mike_stage.py Mike --config config.yml --trace"
|
|
598
|
+
p1 = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
599
|
+
time.sleep(1)
|
|
600
|
+
p1.send_signal(signal.SIGUSR1)
|
|
601
|
+
try:
|
|
602
|
+
outs, _ = p1.communicate(timeout=15)
|
|
603
|
+
except subprocess.TimeoutExpired:
|
|
604
|
+
p1.kill()
|
|
605
|
+
outs, _ = p1.communicate()
|
|
606
|
+
raise ValueError("Timeout expired in Mike test with outs =" + outs.decode())
|
|
607
|
+
assert 'mike_stage.py", line 15' in outs.decode()
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def test_combined_iterators():
|
|
611
|
+
class Oscar(PipelineStage):
|
|
612
|
+
inputs = [("inp1", HDFFile), ("inp2", HDFFile)]
|
|
613
|
+
outputs = []
|
|
614
|
+
config_options = {}
|
|
615
|
+
def run(self):
|
|
616
|
+
it = self.combined_iterators(
|
|
617
|
+
10,
|
|
618
|
+
"inp1", "group1", ["x"],
|
|
619
|
+
"inp2", "group1", ["y", "z"])
|
|
620
|
+
for (s, e, data) in it:
|
|
621
|
+
pass
|
|
622
|
+
|
|
623
|
+
oo = Oscar.make_stage(inp1="tests/test.hdf5", inp2="tests/test.hdf5")
|
|
624
|
+
oo.run()
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def test_memory_and_time_reports(capsys):
|
|
628
|
+
class November(PipelineStage):
|
|
629
|
+
name = f"November"
|
|
630
|
+
parallel = False
|
|
631
|
+
inputs = []
|
|
632
|
+
outputs = []
|
|
633
|
+
config_options = {}
|
|
634
|
+
|
|
635
|
+
def run(self):
|
|
636
|
+
self.memory_report()
|
|
637
|
+
self.memory_report("TAGTAG")
|
|
638
|
+
self.time_stamp("Hello")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
nn = November.make_stage()
|
|
642
|
+
nn.run()
|
|
643
|
+
captured = capsys.readouterr()
|
|
644
|
+
assert "Remaining memory on" in captured.out
|
|
645
|
+
assert "TAGTAG" in captured.out
|
|
646
|
+
assert "Process 0: Hello" in captured.out
|
|
647
|
+
|
|
572
648
|
|
|
573
649
|
if __name__ == "__main__":
|
|
574
650
|
test_construct()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ceci-2.0.1 → ceci-2.2}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ceci-2.0.1 → ceci-2.2}/ceci.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ceci-2.0.1 → ceci-2.2}/setup.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|