ceci 2.0__tar.gz → 2.1__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 → ceci-2.1}/PKG-INFO +2 -2
- {ceci-2.0 → ceci-2.1}/ceci/_version.py +2 -2
- {ceci-2.0 → ceci-2.1}/ceci/minirunner.py +25 -7
- {ceci-2.0 → ceci-2.1}/ceci/pipeline.py +79 -15
- {ceci-2.0 → ceci-2.1}/ceci/stage.py +261 -29
- {ceci-2.0 → ceci-2.1}/ceci/update_pipelines_for_ceci_2.py +7 -2
- {ceci-2.0 → ceci-2.1}/ceci/utils.py +8 -0
- {ceci-2.0 → ceci-2.1}/ceci.egg-info/PKG-INFO +2 -2
- {ceci-2.0 → ceci-2.1}/tests/test_interactive.py +0 -1
- {ceci-2.0 → ceci-2.1}/tests/test_pipeline.py +19 -1
- {ceci-2.0 → ceci-2.1}/tests/test_stage.py +77 -3
- {ceci-2.0 → ceci-2.1}/.coveragerc +0 -0
- {ceci-2.0 → ceci-2.1}/.github/workflows/ci.yml +0 -0
- {ceci-2.0 → ceci-2.1}/.github/workflows/publish.yml +0 -0
- {ceci-2.0 → ceci-2.1}/.gitignore +0 -0
- {ceci-2.0 → ceci-2.1}/.pylintrc +0 -0
- {ceci-2.0 → ceci-2.1}/LICENSE +0 -0
- {ceci-2.0 → ceci-2.1}/README.md +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/__init__.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/__main__.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/config.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/errors.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/main.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/monitor.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/sites/__init__.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/sites/ccin2p3.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/sites/local.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/sites/nersc.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/sites/site.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/tools/__init__.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci/tools/ancestors.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci.egg-info/SOURCES.txt +0 -0
- {ceci-2.0 → ceci-2.1}/ceci.egg-info/dependency_links.txt +0 -0
- {ceci-2.0 → ceci-2.1}/ceci.egg-info/entry_points.txt +0 -0
- {ceci-2.0 → ceci-2.1}/ceci.egg-info/requires.txt +0 -0
- {ceci-2.0 → ceci-2.1}/ceci.egg-info/top_level.txt +0 -0
- {ceci-2.0 → ceci-2.1}/ceci.png +0 -0
- {ceci-2.0 → ceci-2.1}/ceci_example/__init__.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci_example/__main__.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci_example/example_stages.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci_example/types.py +0 -0
- {ceci-2.0 → ceci-2.1}/ceci_example_namespace/example_stages.py +0 -0
- {ceci-2.0 → ceci-2.1}/do_cover.sh +0 -0
- {ceci-2.0 → ceci-2.1}/docs/Makefile +0 -0
- {ceci-2.0 → ceci-2.1}/docs/conf.py +0 -0
- {ceci-2.0 → ceci-2.1}/docs/config.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/config2.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/index.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/installation.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/launchers.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/logo.png +0 -0
- {ceci-2.0 → ceci-2.1}/docs/overview.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/sites.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/stages.rst +0 -0
- {ceci-2.0 → ceci-2.1}/docs/tutorial.rst +0 -0
- {ceci-2.0 → ceci-2.1}/nb/ceci_interactive_example.ipynb +0 -0
- {ceci-2.0 → ceci-2.1}/pyproject.toml +0 -0
- {ceci-2.0 → ceci-2.1}/setup.cfg +0 -0
- {ceci-2.0 → ceci-2.1}/setup.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/__init__.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/config.yml +0 -0
- {ceci-2.0 → ceci-2.1}/tests/config_nm.yml +0 -0
- {ceci-2.0 → ceci-2.1}/tests/create_registry_entries.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/inputs/dm.txt +0 -0
- {ceci-2.0 → ceci-2.1}/tests/inputs/fiducial_cosmology.txt +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test.hdf5 +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test.yml +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_cmd.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_dask.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_dataregistry.yml +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_executables.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_flow_chart.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_helpers.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_main.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_minirunner.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_monitor.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_namespace.yml +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_python_paths.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_site.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_snapshot.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_update_script.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/test_utils.py +0 -0
- {ceci-2.0 → ceci-2.1}/tests/update_test_files/config.yml +0 -0
- {ceci-2.0 → ceci-2.1}/tests/update_test_files/pipeline1.yml +0 -0
- {ceci-2.0 → ceci-2.1}/tests/update_test_files/pipeline2.yml +0 -0
{ceci-2.0 → ceci-2.1}/PKG-INFO
RENAMED
|
@@ -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
|
|
@@ -185,16 +190,7 @@ class StageExecutionConfig:
|
|
|
185
190
|
self.stage_class = PipelineStage.get_stage(
|
|
186
191
|
self.class_name, self.module_name
|
|
187
192
|
)
|
|
188
|
-
|
|
189
|
-
# but that would requiring changing the signature of every sub-class, so we do this
|
|
190
|
-
# instead. At some point we might want to migrate to doing it the better way
|
|
191
|
-
try:
|
|
192
|
-
self.stage_obj = self.stage_class(args, aliases=self.aliases)
|
|
193
|
-
except TypeError:
|
|
194
|
-
self.stage_obj = self.stage_class(args)
|
|
195
|
-
self.stage_obj._aliases.update(**self.aliases)
|
|
196
|
-
self.stage_obj._io_checked = False
|
|
197
|
-
self.stage_obj.check_io()
|
|
193
|
+
self.stage_obj = self.stage_class(args, aliases=self.aliases)
|
|
198
194
|
return self.stage_obj
|
|
199
195
|
|
|
200
196
|
def generate_full_command(self, inputs, outputs, config):
|
|
@@ -373,11 +369,16 @@ class Pipeline:
|
|
|
373
369
|
run_config = {
|
|
374
370
|
"output_dir": pipe_config.get("output_dir", "."),
|
|
375
371
|
"log_dir": pipe_config.get("log_dir", "."),
|
|
376
|
-
"resume": pipe_config.get("resume",
|
|
372
|
+
"resume": pipe_config.get("resume", RESUME_MODE_RESUME),
|
|
377
373
|
"flow_chart": pipe_config.get("flow_chart", ""),
|
|
378
374
|
"registry": pipe_config.get("registry", None),
|
|
379
375
|
}
|
|
380
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
|
+
|
|
381
382
|
launcher_dict = dict(cwl=CWLPipeline, parsl=ParslPipeline, mini=MiniPipeline)
|
|
382
383
|
|
|
383
384
|
if pipe_config.get("flow_chart", False):
|
|
@@ -674,9 +675,11 @@ class Pipeline:
|
|
|
674
675
|
`Pipeline.pipeline_files` data member, so that they are available to later stages
|
|
675
676
|
"""
|
|
676
677
|
kwcopy = kwargs.copy()
|
|
678
|
+
aliases = kwcopy.pop("aliases", {})
|
|
679
|
+
comm = kwcopy.pop("comm", None)
|
|
677
680
|
kwcopy.update(**self.pipeline_files)
|
|
678
|
-
|
|
679
|
-
stage = stage_class(kwcopy, aliases=aliases)
|
|
681
|
+
|
|
682
|
+
stage = stage_class(kwcopy, comm=comm, aliases=aliases)
|
|
680
683
|
return self.add_stage(stage)
|
|
681
684
|
|
|
682
685
|
def remove_stage(self, name):
|
|
@@ -827,7 +830,9 @@ class Pipeline:
|
|
|
827
830
|
# In interactive use the stage object might already be created
|
|
828
831
|
# otherwise we need to create it
|
|
829
832
|
if sec.stage_obj is not None:
|
|
833
|
+
orig_stage_config = stages_config_data.get(stage_name, {}).copy()
|
|
830
834
|
stage = sec.stage_obj
|
|
835
|
+
stage.config.update(**orig_stage_config)
|
|
831
836
|
else:
|
|
832
837
|
# Find the inputs for this stage and set up the arguments
|
|
833
838
|
# to the stage init method
|
|
@@ -956,7 +961,24 @@ class Pipeline:
|
|
|
956
961
|
|
|
957
962
|
def should_skip_stage(self, stage):
|
|
958
963
|
"""Return true if we should skip a stage because it is finished and we are in resume mode"""
|
|
959
|
-
|
|
964
|
+
resume_mode = self.run_config["resume"]
|
|
965
|
+
if resume_mode is True:
|
|
966
|
+
resume_mode = "resume"
|
|
967
|
+
elif resume_mode is False:
|
|
968
|
+
resume_mode = "restart"
|
|
969
|
+
|
|
970
|
+
if resume_mode == "restart":
|
|
971
|
+
return False
|
|
972
|
+
|
|
973
|
+
outputs = stage.find_outputs(self.run_config["output_dir"]).values()
|
|
974
|
+
|
|
975
|
+
if resume_mode == "resume":
|
|
976
|
+
return all(os.path.exists(output) for output in outputs)
|
|
977
|
+
elif resume_mode == "refuse":
|
|
978
|
+
if any(os.path.exists(output) for output in outputs):
|
|
979
|
+
raise RuntimeError(f"Output files already exist for stage {stage} and resume mode is 'refuse'")
|
|
980
|
+
else:
|
|
981
|
+
raise ValueError(f"Unknown resume mode: {resume_mode}")
|
|
960
982
|
|
|
961
983
|
def save(self, pipefile, stagefile=None, reduce_config=False, **kwargs):
|
|
962
984
|
"""Save this pipeline state to a yaml file
|
|
@@ -1046,6 +1068,10 @@ class Pipeline:
|
|
|
1046
1068
|
# Nodes we have already added
|
|
1047
1069
|
seen = set()
|
|
1048
1070
|
|
|
1071
|
+
# Dictionary to track nodes by their inputs and outputs
|
|
1072
|
+
node_groups = {}
|
|
1073
|
+
|
|
1074
|
+
|
|
1049
1075
|
# Add overall pipeline inputs
|
|
1050
1076
|
for inp in self.overall_inputs.keys():
|
|
1051
1077
|
graph.add_node(inp, shape="box", color="gold", style="filled")
|
|
@@ -1071,6 +1097,43 @@ class Pipeline:
|
|
|
1071
1097
|
seen.add(out)
|
|
1072
1098
|
graph.add_edge(stage.instance_name, out, color="black")
|
|
1073
1099
|
|
|
1100
|
+
# We want to group together all the files that all created
|
|
1101
|
+
# by the same stage and also all used by the same stages, to
|
|
1102
|
+
# reduce the number of nodes in the graph and make it more readable.
|
|
1103
|
+
# First we find that grouping.
|
|
1104
|
+
node_groups = collections.defaultdict(list)
|
|
1105
|
+
for node in graph.nodes_iter():
|
|
1106
|
+
# only affect the nodes representing files
|
|
1107
|
+
if node.attr['color'] != "skyblue":
|
|
1108
|
+
continue
|
|
1109
|
+
# Find the stage node that created this file,
|
|
1110
|
+
# and all the stage nodes that make use of it
|
|
1111
|
+
edge_in = graph.in_edges(node)[0]
|
|
1112
|
+
creator = edge_in[0]
|
|
1113
|
+
users = []
|
|
1114
|
+
for edge in graph.out_edges(node):
|
|
1115
|
+
users.append(edge[1])
|
|
1116
|
+
key = (creator, tuple(users))
|
|
1117
|
+
node_groups[key].append(node)
|
|
1118
|
+
|
|
1119
|
+
# Now we remove all the groups of nodes with more than one in
|
|
1120
|
+
# and replace them with a single node
|
|
1121
|
+
for key, nodes in node_groups.items():
|
|
1122
|
+
if len(nodes) > 1:
|
|
1123
|
+
if len(nodes) > 4:
|
|
1124
|
+
# make a string with two nodes per line
|
|
1125
|
+
node_names = []
|
|
1126
|
+
for i in range(0, len(nodes), 2):
|
|
1127
|
+
node_names.append(", ".join(nodes[i:i+2]))
|
|
1128
|
+
new_node = "\n".join(node_names)
|
|
1129
|
+
else:
|
|
1130
|
+
new_node = "\n".join(nodes)
|
|
1131
|
+
graph.remove_nodes_from(nodes)
|
|
1132
|
+
graph.add_node(new_node, shape="box", color="skyblue", style="filled")
|
|
1133
|
+
graph.add_edge(key[0], new_node, color="black")
|
|
1134
|
+
for user in key[1]:
|
|
1135
|
+
graph.add_edge(new_node, user, color="black")
|
|
1136
|
+
|
|
1074
1137
|
# finally, output the stage to file
|
|
1075
1138
|
if filename.endswith(".dot"):
|
|
1076
1139
|
graph.write(filename)
|
|
@@ -1439,6 +1502,7 @@ class MiniPipeline(Pipeline):
|
|
|
1439
1502
|
f"""
|
|
1440
1503
|
*************************************************
|
|
1441
1504
|
Error running pipeline stage {error.job_name}.
|
|
1505
|
+
Failed after {error.run_time}.
|
|
1442
1506
|
|
|
1443
1507
|
Standard output and error streams in {log_dir}/{error.job_name}.out
|
|
1444
1508
|
*************************************************
|
|
@@ -8,11 +8,14 @@ import shutil
|
|
|
8
8
|
import cProfile
|
|
9
9
|
import pdb
|
|
10
10
|
import datetime
|
|
11
|
+
import warnings
|
|
12
|
+
import socket
|
|
11
13
|
|
|
12
14
|
from abc import abstractmethod
|
|
13
15
|
from . import errors
|
|
14
16
|
from .monitor import MemoryMonitor
|
|
15
17
|
from .config import StageParameter, StageConfig, cast_to_streamable
|
|
18
|
+
from .utils import activate_tracing
|
|
16
19
|
|
|
17
20
|
SERIAL = "serial"
|
|
18
21
|
MPI_PARALLEL = "mpi"
|
|
@@ -102,6 +105,8 @@ class PipelineStage:
|
|
|
102
105
|
if comm is not None:
|
|
103
106
|
self.setup_mpi(comm)
|
|
104
107
|
|
|
108
|
+
self.check_io()
|
|
109
|
+
|
|
105
110
|
|
|
106
111
|
@classmethod
|
|
107
112
|
def make_stage(cls, **kwargs):
|
|
@@ -117,13 +122,7 @@ class PipelineStage:
|
|
|
117
122
|
for output_ in cls.outputs: # pylint: disable=no-member
|
|
118
123
|
outtag = output_[0]
|
|
119
124
|
aliases[outtag] = f"{outtag}_{name}"
|
|
120
|
-
|
|
121
|
-
# but that would requiring changing the signature of every sub-class, so we do this
|
|
122
|
-
# instead. At some point we might want to migrate to doing it the better way
|
|
123
|
-
stage = cls(kwcopy, comm=comm)
|
|
124
|
-
stage._aliases.update(**aliases)
|
|
125
|
-
stage._io_checked = False
|
|
126
|
-
stage.check_io()
|
|
125
|
+
stage = cls(kwcopy, comm=comm, aliases=aliases)
|
|
127
126
|
return stage
|
|
128
127
|
|
|
129
128
|
def get_aliases(self):
|
|
@@ -149,8 +148,18 @@ class PipelineStage:
|
|
|
149
148
|
|
|
150
149
|
@abstractmethod
|
|
151
150
|
def run(self): # pragma: no cover
|
|
152
|
-
"""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
|
+
"""
|
|
153
155
|
raise NotImplementedError("run")
|
|
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
|
|
154
163
|
|
|
155
164
|
def load_configs(self, args):
|
|
156
165
|
"""
|
|
@@ -181,7 +190,6 @@ class PipelineStage:
|
|
|
181
190
|
error_class = type(error)
|
|
182
191
|
msg = str(error)
|
|
183
192
|
raise error_class(f"Error configuring {self.instance_name}: {msg}")
|
|
184
|
-
self.check_io(args)
|
|
185
193
|
|
|
186
194
|
def check_io(self, args=None):
|
|
187
195
|
"""
|
|
@@ -344,7 +352,8 @@ class PipelineStage:
|
|
|
344
352
|
path = pathlib.Path(filename).resolve()
|
|
345
353
|
|
|
346
354
|
# Add a description of the parameters to the end of the docstring
|
|
347
|
-
|
|
355
|
+
# If no config options are specified, omit this.
|
|
356
|
+
if stage_is_complete and cls.config_options:
|
|
348
357
|
config_text = cls._describe_configuration_text()
|
|
349
358
|
if cls.__doc__ is None:
|
|
350
359
|
cls.__doc__ = f"Stage {cls.name}\n\nConfiguration Parameters:\n{config_text}"
|
|
@@ -607,6 +616,12 @@ I currently know about these stages:
|
|
|
607
616
|
help="Report memory use. Argument gives interval in seconds between reports",
|
|
608
617
|
)
|
|
609
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
|
+
|
|
610
625
|
# Error message we will return if --mpi used on a non-supported
|
|
611
626
|
# stage.
|
|
612
627
|
mpi_err = (
|
|
@@ -665,6 +680,21 @@ I currently know about these stages:
|
|
|
665
680
|
if args.memmon: # pragma: no cover
|
|
666
681
|
monitor = MemoryMonitor.start_in_thread(interval=args.memmon)
|
|
667
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
|
+
|
|
668
698
|
try:
|
|
669
699
|
stage.run()
|
|
670
700
|
except Exception as error: # pragma: no cover
|
|
@@ -785,13 +815,24 @@ I currently know about these stages:
|
|
|
785
815
|
|
|
786
816
|
def is_mpi(self):
|
|
787
817
|
"""
|
|
788
|
-
|
|
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
|
|
789
824
|
"""
|
|
790
825
|
return self._parallel == MPI_PARALLEL
|
|
791
826
|
|
|
792
827
|
def is_dask(self):
|
|
793
828
|
"""
|
|
794
|
-
|
|
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
|
+
|
|
795
836
|
"""
|
|
796
837
|
return self._parallel == DASK_PARALLEL
|
|
797
838
|
|
|
@@ -942,6 +983,11 @@ I currently know about these stages:
|
|
|
942
983
|
Parallel: bool
|
|
943
984
|
Whether to split data by rank or just give all procs all data.
|
|
944
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
|
|
945
991
|
"""
|
|
946
992
|
n_chunks = n_rows // chunk_rows
|
|
947
993
|
if n_chunks * chunk_rows < n_rows: # pragma: no cover
|
|
@@ -963,6 +1009,17 @@ I currently know about these stages:
|
|
|
963
1009
|
"""
|
|
964
1010
|
Return the path of an input file with the given tag,
|
|
965
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
|
+
|
|
966
1023
|
"""
|
|
967
1024
|
tag = self.get_aliased_tag(tag)
|
|
968
1025
|
return self._inputs[tag]
|
|
@@ -975,7 +1032,21 @@ I currently know about these stages:
|
|
|
975
1032
|
which can be aliased already.
|
|
976
1033
|
|
|
977
1034
|
If final_name is False then use a temporary name - file will
|
|
978
|
-
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
|
|
979
1050
|
"""
|
|
980
1051
|
|
|
981
1052
|
tag = self.get_aliased_tag(tag)
|
|
@@ -998,6 +1069,21 @@ I currently know about these stages:
|
|
|
998
1069
|
For specialized file types like FITS or HDF5 it will return
|
|
999
1070
|
a more specific object - see the types.py file for more info.
|
|
1000
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
|
|
1001
1087
|
"""
|
|
1002
1088
|
path = self.get_input(tag)
|
|
1003
1089
|
input_class = self.get_input_type(tag)
|
|
@@ -1014,7 +1100,7 @@ I currently know about these stages:
|
|
|
1014
1100
|
Find and open an output file with the given tag, in write mode.
|
|
1015
1101
|
|
|
1016
1102
|
If final_name is True then they will be opened using their final
|
|
1017
|
-
target output name. Otherwise we will prepend
|
|
1103
|
+
target output name. Otherwise we will prepend `inprogress_` to their
|
|
1018
1104
|
file name. This means we know that if the final file exists then it
|
|
1019
1105
|
is completed.
|
|
1020
1106
|
|
|
@@ -1025,12 +1111,11 @@ I currently know about these stages:
|
|
|
1025
1111
|
|
|
1026
1112
|
Parameters
|
|
1027
1113
|
----------
|
|
1028
|
-
|
|
1029
1114
|
tag: str
|
|
1030
1115
|
Tag as listed in self.outputs
|
|
1031
1116
|
|
|
1032
1117
|
wrapper: bool
|
|
1033
|
-
|
|
1118
|
+
Whether to return an underlying file object (False) or a data type instance (True)
|
|
1034
1119
|
|
|
1035
1120
|
final_name: bool
|
|
1036
1121
|
Default=False. Whether to save to
|
|
@@ -1038,6 +1123,10 @@ I currently know about these stages:
|
|
|
1038
1123
|
**kwargs:
|
|
1039
1124
|
Extra args are passed on to the file's class constructor.
|
|
1040
1125
|
|
|
1126
|
+
Returns
|
|
1127
|
+
-------
|
|
1128
|
+
obj: file or object
|
|
1129
|
+
The opened file or object
|
|
1041
1130
|
"""
|
|
1042
1131
|
path = self.get_output(tag, final_name=final_name)
|
|
1043
1132
|
output_class = self.get_output_type(tag)
|
|
@@ -1082,33 +1171,63 @@ I currently know about these stages:
|
|
|
1082
1171
|
@classmethod
|
|
1083
1172
|
def inputs_(cls):
|
|
1084
1173
|
"""
|
|
1085
|
-
Return the dict
|
|
1174
|
+
Return the dict mapping input tags to file names.
|
|
1175
|
+
|
|
1176
|
+
Returns
|
|
1177
|
+
-------
|
|
1178
|
+
in_dict : dict[str:str]
|
|
1086
1179
|
"""
|
|
1087
1180
|
return cls.inputs # pylint: disable=no-member
|
|
1088
1181
|
|
|
1089
1182
|
@classmethod
|
|
1090
1183
|
def outputs_(cls):
|
|
1091
1184
|
"""
|
|
1092
|
-
Return the dict
|
|
1185
|
+
Return the dict mapping output tags to file names.
|
|
1186
|
+
|
|
1187
|
+
Returns
|
|
1188
|
+
-------
|
|
1189
|
+
out_dict : dict[str:str]
|
|
1093
1190
|
"""
|
|
1094
1191
|
return cls.outputs # pylint: disable=no-member
|
|
1095
1192
|
|
|
1096
1193
|
@classmethod
|
|
1097
1194
|
def output_tags(cls):
|
|
1098
1195
|
"""
|
|
1099
|
-
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
|
|
1100
1202
|
"""
|
|
1101
1203
|
return [tag for tag, _ in cls.outputs_()]
|
|
1102
1204
|
|
|
1103
1205
|
@classmethod
|
|
1104
1206
|
def input_tags(cls):
|
|
1105
1207
|
"""
|
|
1106
|
-
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
|
|
1107
1214
|
"""
|
|
1108
1215
|
return [tag for tag, _ in cls.inputs_()]
|
|
1109
1216
|
|
|
1110
1217
|
def get_input_type(self, tag):
|
|
1111
|
-
"""
|
|
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
|
+
"""
|
|
1112
1231
|
tag = self.get_aliased_tag(tag)
|
|
1113
1232
|
for t, dt in self.inputs_():
|
|
1114
1233
|
t = self.get_aliased_tag(t)
|
|
@@ -1117,7 +1236,19 @@ I currently know about these stages:
|
|
|
1117
1236
|
raise ValueError(f"Tag {tag} is not a known input") # pragma: no cover
|
|
1118
1237
|
|
|
1119
1238
|
def get_output_type(self, tag):
|
|
1120
|
-
"""
|
|
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
|
+
"""
|
|
1121
1252
|
tag = self.get_aliased_tag(tag)
|
|
1122
1253
|
for t, dt in self.outputs_():
|
|
1123
1254
|
t = self.get_aliased_tag(t)
|
|
@@ -1137,8 +1268,12 @@ I currently know about these stages:
|
|
|
1137
1268
|
@property
|
|
1138
1269
|
def config(self):
|
|
1139
1270
|
"""
|
|
1140
|
-
|
|
1271
|
+
The configuration dictionary for this stage, aggregating command
|
|
1141
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.
|
|
1142
1277
|
"""
|
|
1143
1278
|
return self._configs
|
|
1144
1279
|
|
|
@@ -1256,12 +1391,6 @@ I currently know about these stages:
|
|
|
1256
1391
|
f"{tag:20} : {aliased_tag:20} :{str(ftype):20} : {self._outputs[aliased_tag]}\n"
|
|
1257
1392
|
)
|
|
1258
1393
|
|
|
1259
|
-
def should_skip(self, run_config):
|
|
1260
|
-
"""Return true if we should skip a stage b/c it's outputs already exist and we are in resume mode"""
|
|
1261
|
-
outputs = self.find_outputs(run_config["output_dir"]).values()
|
|
1262
|
-
already_run_stage = all(os.path.exists(output) for output in outputs)
|
|
1263
|
-
return already_run_stage and run_config["resume"]
|
|
1264
|
-
|
|
1265
1394
|
def already_finished(self):
|
|
1266
1395
|
"""Print a warning that a stage is being skipped"""
|
|
1267
1396
|
print(f"Skipping stage {self.instance_name} because its outputs exist already")
|
|
@@ -1273,6 +1402,7 @@ I currently know about these stages:
|
|
|
1273
1402
|
Loop through chunks of the input data from a FITS file with the given tag
|
|
1274
1403
|
|
|
1275
1404
|
TODO: add ceci tests of this functions
|
|
1405
|
+
|
|
1276
1406
|
Parameters
|
|
1277
1407
|
----------
|
|
1278
1408
|
tag: str
|
|
@@ -1361,6 +1491,59 @@ I currently know about these stages:
|
|
|
1361
1491
|
data = {col: group[col][start:end] for col in cols}
|
|
1362
1492
|
yield start, end, data
|
|
1363
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
|
+
|
|
1364
1547
|
################################
|
|
1365
1548
|
# Pipeline-related methods
|
|
1366
1549
|
################################
|
|
@@ -1560,3 +1743,52 @@ I currently know about these stages:
|
|
|
1560
1743
|
# cwl_tool.metadata = cwlgen.Metadata(**metadata)
|
|
1561
1744
|
|
|
1562
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()
|
|
@@ -77,7 +77,8 @@ def update_pipeline_file_group(pipeline_files):
|
|
|
77
77
|
def scan_directory_and_update(base_dir):
|
|
78
78
|
groups = collections.defaultdict(list)
|
|
79
79
|
yaml = ruamel.yaml.YAML()
|
|
80
|
-
|
|
80
|
+
yaml.allow_duplicate_keys = True
|
|
81
|
+
for dirpath, _, filenames in os.walk(base_dir):
|
|
81
82
|
# just process yaml files
|
|
82
83
|
for filename in filenames:
|
|
83
84
|
if not (filename.endswith(".yaml") or filename.endswith(".yml")):
|
|
@@ -85,7 +86,11 @@ def scan_directory_and_update(base_dir):
|
|
|
85
86
|
filepath = os.path.join(dirpath, filename)
|
|
86
87
|
with open(filepath) as f:
|
|
87
88
|
yaml_str = f.read()
|
|
88
|
-
|
|
89
|
+
try:
|
|
90
|
+
info = yaml.load(yaml_str)
|
|
91
|
+
except:
|
|
92
|
+
print("# Could not read yaml file:", filepath)
|
|
93
|
+
continue
|
|
89
94
|
|
|
90
95
|
if is_pipeline_file(info):
|
|
91
96
|
config = info["config"]
|
|
@@ -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
|
|
|
@@ -568,9 +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
|
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
|
|
574
648
|
|
|
575
649
|
if __name__ == "__main__":
|
|
576
650
|
test_construct()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ceci-2.0 → ceci-2.1}/.gitignore
RENAMED
|
File without changes
|
{ceci-2.0 → ceci-2.1}/.pylintrc
RENAMED
|
File without changes
|
{ceci-2.0 → ceci-2.1}/LICENSE
RENAMED
|
File without changes
|
{ceci-2.0 → ceci-2.1}/README.md
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
|
{ceci-2.0 → ceci-2.1}/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
|
{ceci-2.0 → ceci-2.1}/setup.cfg
RENAMED
|
File without changes
|
{ceci-2.0 → ceci-2.1}/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
|