cryoemservices 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. cryoemservices/__init__.py +3 -0
  2. cryoemservices/cli/__init__.py +0 -0
  3. cryoemservices/cli/resubmit_wrapper.py +51 -0
  4. cryoemservices/pipeliner_plugins/__init__.py +0 -0
  5. cryoemservices/pipeliner_plugins/combine_star_files.py +251 -0
  6. cryoemservices/pipeliner_plugins/combine_star_job.py +155 -0
  7. cryoemservices/services/__init__.py +0 -0
  8. cryoemservices/services/cluster_submission.py +229 -0
  9. cryoemservices/services/cryolo.py +414 -0
  10. cryoemservices/services/ctffind.py +331 -0
  11. cryoemservices/services/denoise_iris.py +317 -0
  12. cryoemservices/services/extract.py +439 -0
  13. cryoemservices/services/icebreaker.py +344 -0
  14. cryoemservices/services/images.py +102 -0
  15. cryoemservices/services/images_plugins.py +283 -0
  16. cryoemservices/services/ispyb.py +1085 -0
  17. cryoemservices/services/ispyb_buffer.py +102 -0
  18. cryoemservices/services/motioncorr.py +680 -0
  19. cryoemservices/services/motioncorr_slurm.py +323 -0
  20. cryoemservices/services/node_creator.py +373 -0
  21. cryoemservices/services/select_classes.py +528 -0
  22. cryoemservices/services/select_particles.py +303 -0
  23. cryoemservices/services/tomo_align.py +614 -0
  24. cryoemservices/services/tomo_align_iris.py +219 -0
  25. cryoemservices/util/__init__.py +0 -0
  26. cryoemservices/util/dispatcher_tools.py +234 -0
  27. cryoemservices/util/spa_output_files.py +387 -0
  28. cryoemservices/util/spa_relion_service_options.py +279 -0
  29. cryoemservices/wrappers/__init__.py +0 -0
  30. cryoemservices/wrappers/class2d_wrapper.py +373 -0
  31. cryoemservices/wrappers/class3d_wrapper.py +527 -0
  32. cryoemservices-0.1.3.dist-info/LICENSE +28 -0
  33. cryoemservices-0.1.3.dist-info/METADATA +156 -0
  34. cryoemservices-0.1.3.dist-info/RECORD +37 -0
  35. cryoemservices-0.1.3.dist-info/WHEEL +5 -0
  36. cryoemservices-0.1.3.dist-info/entry_points.txt +42 -0
  37. cryoemservices-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.1.3"
File without changes
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import workflows.transport.pika_transport as pt
8
+ from workflows.recipe import RecipeWrapper
9
+
10
+
11
+ def run():
12
+ parser = argparse.ArgumentParser(
13
+ description="Resubmit a failed zocalo wrapper script using the .recipewrap file"
14
+ )
15
+ parser.add_argument(
16
+ "-w",
17
+ "--wrapper",
18
+ help="Location of the .recipewrap wrapper file to resubmit",
19
+ dest="wrapper",
20
+ required=True,
21
+ )
22
+ parser.add_argument(
23
+ "-c",
24
+ "--config",
25
+ help="Transport configuration file for connecting to the message broker",
26
+ dest="config",
27
+ required=True,
28
+ )
29
+ args = parser.parse_args()
30
+
31
+ if not Path(args.wrapper).is_file():
32
+ print(f"{args.wrapper} cannot be found")
33
+ return
34
+ if not Path(args.config).is_file():
35
+ print(f"{args.config} cannot be found")
36
+ return
37
+
38
+ # Connect to the message transport
39
+ transport = pt.PikaTransport()
40
+ transport.load_configuration_file(args.config)
41
+ transport.connect()
42
+
43
+ # Load and submit the wrapper part of the recipe
44
+ with open(args.wrapper, "r") as wrap:
45
+ recipe = json.load(wrap)
46
+ rw = RecipeWrapper(message=recipe, transport=transport)
47
+ rw._send_to_destination(rw.recipe_pointer, None, rw.payload, {})
48
+
49
+
50
+ if __name__ == "__main__":
51
+ run()
File without changes
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from math import ceil
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ import starfile
9
+
10
+
11
+ def write_empty_particles_file(file_to_write, optics_dataframe, particles_dataframe):
12
+ """Write a particles star file with no particles, ready for appending to"""
13
+ with open(file_to_write, "w") as optics_file:
14
+ optics_file.write("data_optics\n\nloop_\n")
15
+ for optics_loop_tag in optics_dataframe.keys():
16
+ optics_file.write(f"_{optics_loop_tag}\n")
17
+ optics_file.write(" ".join(optics_dataframe.to_numpy(dtype=str)[0]))
18
+ optics_file.write("\n\n\n")
19
+
20
+ optics_file.write("data_particles\n\nloop_\n")
21
+ for particles_loop_tag in particles_dataframe.keys():
22
+ optics_file.write(f"_{particles_loop_tag}\n")
23
+
24
+
25
+ def combine_star_files(files_to_process: List[Path], output_dir: Path):
26
+ """Combines any number of particle star files into a single file.
27
+
28
+ Parameters:
29
+ files_to_process: A list of the particle star files to combine
30
+ output_dir: The directory in which to save the combined "particles_all.star" file
31
+ """
32
+ total_particles = 0
33
+
34
+ # Never read particles_all.star first as it will be big
35
+ if files_to_process[0].name == "particles_all.star":
36
+ files_to_process.append(files_to_process[0])
37
+ files_to_process = files_to_process[1:]
38
+
39
+ # Make a temporary star file to get the table headings from
40
+ reference_optics = None
41
+ with open(files_to_process[0], "r") as full_starfile, open(
42
+ output_dir / ".particles_tmp.star", "w"
43
+ ) as tmp_starfile:
44
+ for line_counter in range(50):
45
+ line = full_starfile.readline()
46
+ if line.startswith("opticsGroup"):
47
+ reference_optics = line.split()
48
+ if not line:
49
+ break
50
+ tmp_starfile.write(line)
51
+
52
+ star_dictionary = starfile.read(output_dir / ".particles_tmp.star")
53
+ (output_dir / ".particles_tmp.star").unlink()
54
+
55
+ if not reference_optics:
56
+ raise IndexError(f"Cannot find optics group in {files_to_process[0]}")
57
+
58
+ write_empty_particles_file(
59
+ output_dir / ".particles_all_tmp.star",
60
+ star_dictionary["optics"],
61
+ star_dictionary["particles"],
62
+ )
63
+
64
+ number_of_star_files = 0
65
+ # Add the remaining files using append mode for speed and memory efficiency
66
+ for split_file in files_to_process:
67
+ # Check that the files have the same optics tables
68
+ with open(split_file, "r") as added_starfile:
69
+ while True:
70
+ optics_line = added_starfile.readline()
71
+ if not optics_line:
72
+ raise IndexError(f"Cannot find optics group in {split_file}")
73
+ if optics_line.startswith("opticsGroup"):
74
+ new_optics = optics_line.split()
75
+ break
76
+
77
+ if len(new_optics) != len(reference_optics):
78
+ raise IndexError(
79
+ "Cannot combine star files with different length optics tables."
80
+ )
81
+ for optics_label in range(len(reference_optics)):
82
+ ref_value = reference_optics[optics_label]
83
+ new_value = new_optics[optics_label]
84
+ if ref_value[0].isdigit() and new_value[0].isdigit():
85
+ ref_value = float(ref_value)
86
+ new_value = float(new_value)
87
+ if ref_value != new_value:
88
+ print(ref_value, new_value)
89
+ raise IndexError(
90
+ "Cannot combine star files with different values in optics tables."
91
+ )
92
+
93
+ # Add the particles lines to the final star file
94
+ file_particles_count = 0
95
+ with open(split_file, "r") as added_starfile, open(
96
+ output_dir / ".particles_all_tmp.star", "a"
97
+ ) as particles_file:
98
+ while True:
99
+ particle_line = added_starfile.readline()
100
+ if not particle_line:
101
+ break
102
+ particle_split_line = particle_line.split()
103
+ if len(particle_split_line) > 0 and particle_split_line[0][0].isdigit():
104
+ file_particles_count += 1
105
+ total_particles += 1
106
+ particles_file.write(particle_line)
107
+
108
+ print(f"Adding {split_file} with {file_particles_count} particles")
109
+ number_of_star_files += 1
110
+
111
+ (output_dir / ".particles_all_tmp.star").rename(output_dir / "particles_all.star")
112
+ print(
113
+ f"Combined {number_of_star_files} files into particles_all.star "
114
+ f"with {total_particles} particles"
115
+ )
116
+
117
+
118
+ def split_star_file(
119
+ file_to_process: Path,
120
+ output_dir: Path,
121
+ number_of_splits: int or None = None,
122
+ split_size: int or None = None,
123
+ ):
124
+ """Splits a star file into subfiles.
125
+
126
+ The number of subfiles can be given with number_of_splits
127
+ or is determined by split_size, the number of particles for in each file.
128
+ """
129
+
130
+ # Make a temporary star file to get the table headings from
131
+ with open(file_to_process, "r") as full_starfile, open(
132
+ output_dir / ".particles_tmp.star", "w"
133
+ ) as tmp_starfile:
134
+ for line_counter in range(50):
135
+ line = full_starfile.readline()
136
+ if not line:
137
+ break
138
+ tmp_starfile.write(line)
139
+
140
+ star_dictionary = starfile.read(output_dir / ".particles_tmp.star")
141
+ (output_dir / ".particles_tmp.star").unlink()
142
+
143
+ # Find the number of lines in the full file
144
+ starfile_starter_lines = line_counter - star_dictionary["particles"].shape[0]
145
+ count = 0
146
+ with open(file_to_process, "r") as full_starfile:
147
+ for count, line in enumerate(full_starfile):
148
+ pass
149
+ number_of_particles = count + 1 - starfile_starter_lines
150
+
151
+ # Determine the number of files and size of the splits
152
+ if number_of_splits:
153
+ if split_size:
154
+ print(
155
+ "Warning: "
156
+ "Both number_of_splits and split_size have been given, "
157
+ "using number_of_splits.",
158
+ )
159
+ split_size = ceil(number_of_particles / number_of_splits)
160
+ elif split_size:
161
+ number_of_splits = ceil(number_of_particles / split_size)
162
+ else:
163
+ raise KeyError("Either number_of_splits or split_size must be given.")
164
+
165
+ with open(file_to_process, "r") as full_starfile:
166
+ # Read in the full file line by line, removing the header lines first
167
+ for start_line in range(starfile_starter_lines):
168
+ full_starfile.readline()
169
+
170
+ for split in range(number_of_splits):
171
+ # Give each new file the header information
172
+ write_empty_particles_file(
173
+ output_dir / f".particles_split{split+1}_tmp.star",
174
+ star_dictionary["optics"],
175
+ star_dictionary["particles"],
176
+ )
177
+
178
+ # Write particles to the split files by reading in lines from the full file
179
+ with open(
180
+ output_dir / f".particles_split{split+1}_tmp.star", "a"
181
+ ) as split_file:
182
+ for count in range(split_size):
183
+ particle_line = full_starfile.readline()
184
+ if not particle_line:
185
+ break
186
+ split_file.write(particle_line)
187
+
188
+ (output_dir / f".particles_split{split+1}_tmp.star").rename(
189
+ output_dir / f"particles_split{split+1}.star"
190
+ )
191
+
192
+ print(
193
+ f"Split {number_of_particles} particles into "
194
+ f"{number_of_splits} files with {split_size} particles in each."
195
+ )
196
+
197
+
198
+ def create_parser():
199
+ parser = argparse.ArgumentParser()
200
+
201
+ parser.add_argument(
202
+ "files_to_process",
203
+ type=Path,
204
+ nargs="+",
205
+ help="The star files from which to combine particles.",
206
+ )
207
+ parser.add_argument(
208
+ "--output_dir",
209
+ dest="output_dir",
210
+ type=Path,
211
+ help="Folder in which to save the new star files.",
212
+ )
213
+
214
+ parser.add_argument(
215
+ "--split",
216
+ dest="do_split",
217
+ action="store_true",
218
+ default=False,
219
+ help="Whether to split the particles again into new star files.",
220
+ )
221
+ parser.add_argument(
222
+ "--n_files",
223
+ dest="n_files",
224
+ type=int,
225
+ default=None,
226
+ help="Number of files to split the particles into.",
227
+ )
228
+ parser.add_argument(
229
+ "--split_size",
230
+ dest="split_size",
231
+ type=int,
232
+ default=None,
233
+ help="Number of particles to write per file.",
234
+ )
235
+
236
+ return parser
237
+
238
+
239
+ def main():
240
+ arg_parser = create_parser()
241
+ run_args = vars(arg_parser.parse_args())
242
+
243
+ combine_star_files(run_args["files_to_process"], run_args["output_dir"])
244
+
245
+ if run_args["do_split"]:
246
+ split_star_file(
247
+ run_args["output_dir"] / "particles_all.star",
248
+ run_args["output_dir"],
249
+ run_args["n_files"],
250
+ run_args["split_size"],
251
+ )
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from pipeliner.data_structure import SELECT_DIR
6
+ from pipeliner.display_tools import mini_montage_from_starfile
7
+ from pipeliner.job_options import BooleanJobOption, IntJobOption, StringJobOption
8
+ from pipeliner.nodes import NODE_PARTICLESDATA, Node
9
+ from pipeliner.pipeliner_job import ExternalProgram, PipelinerJob
10
+
11
+ try:
12
+ from pipeliner.pipeliner_job import PipelinerCommand
13
+ except ImportError:
14
+ PipelinerCommand = None
15
+
16
+ COMBINE_STAR_NAME = "combine_star_files"
17
+
18
+
19
+ class ProcessStarFiles(PipelinerJob):
20
+ PROCESS_NAME = "combine_star_files_job"
21
+ OUT_DIR = SELECT_DIR
22
+
23
+ def __init__(self):
24
+ super().__init__()
25
+
26
+ self.jobinfo.display_name = "Particle star file merging"
27
+ self.jobinfo.short_desc = "Combine and split star files of particles"
28
+ self.jobinfo.long_desc = (
29
+ "Combine star files of particles, then optionally split them again."
30
+ )
31
+
32
+ self.jobinfo.programs = [ExternalProgram(command=COMBINE_STAR_NAME)]
33
+
34
+ self.joboptions["files_to_process"] = StringJobOption(
35
+ label="The star files from which to combine particles",
36
+ is_required=True,
37
+ help_text="The names of the star files, separated by spaces.",
38
+ )
39
+ self.joboptions["do_split"] = BooleanJobOption(
40
+ label="Whether to split the combined star file", default_value=False
41
+ )
42
+ self.joboptions["n_files"] = IntJobOption(
43
+ label="Number of files to split the combined file into",
44
+ default_value=-1,
45
+ help_text=(
46
+ "Provide either the number of files to split into,"
47
+ " or the number of particles per file."
48
+ ),
49
+ deactivate_if=[("do_split", "is", "False")],
50
+ )
51
+ self.joboptions["split_size"] = IntJobOption(
52
+ label="Number of particles to put in each split",
53
+ default_value=-1,
54
+ help_text=(
55
+ "Provide either the number of files to split into,"
56
+ " or the number of particles per file."
57
+ ),
58
+ deactivate_if=[("do_split", "is", "False")],
59
+ )
60
+
61
+ self.set_joboption_order(
62
+ ["files_to_process", "do_split", "n_files", "split_size"]
63
+ )
64
+
65
+ def get_commands(self):
66
+ """Construct the command for combining and splitting star files"""
67
+ command = [COMBINE_STAR_NAME]
68
+ file_list = self.joboptions["files_to_process"].get_string().split(" ")
69
+ command.extend(file_list)
70
+ command.extend(["--output_dir", str(self.output_dir)])
71
+
72
+ if self.joboptions["do_split"].get_boolean():
73
+ command.extend(["--split"])
74
+
75
+ if (
76
+ self.joboptions["n_files"].get_number() <= 0
77
+ and self.joboptions["split_size"].get_number() <= 0
78
+ ):
79
+ raise ValueError(
80
+ "ERROR: When splitting the combined STAR file into subsets,"
81
+ " set n_files or split_size to a positive value"
82
+ )
83
+
84
+ if self.joboptions["n_files"].get_number() > 0:
85
+ command.extend(["--n_files", self.joboptions["n_files"].get_string()])
86
+
87
+ if self.joboptions["split_size"].get_number() > 0:
88
+ command.extend(
89
+ ["--split_size", self.joboptions["split_size"].get_string()]
90
+ )
91
+
92
+ # Add files as input nodes, as long as they are not also the output node
93
+ for particle_file in file_list:
94
+ if particle_file != str(Path(self.output_dir) / "particles_all.star"):
95
+ self.input_nodes.append(Node(particle_file, NODE_PARTICLESDATA))
96
+ self.output_nodes.append(
97
+ Node(str(Path(self.output_dir) / "particles_all.star"), NODE_PARTICLESDATA)
98
+ )
99
+
100
+ if PipelinerCommand is None:
101
+ return [command]
102
+ pipeliner_commands = [PipelinerCommand([command], relion_control=False)]
103
+ return pipeliner_commands
104
+
105
+ def create_output_nodes(self):
106
+ self.add_output_node("particles_all.star", NODE_PARTICLESDATA, ["relion"])
107
+
108
+ def post_run_actions(self):
109
+ """Find any output files produced by the splitting"""
110
+ output_files = Path(self.output_dir).glob("particles_split*.star")
111
+ for split in output_files:
112
+ self.output_nodes.append(Node(str(split), NODE_PARTICLESDATA))
113
+
114
+ def create_results_display(self):
115
+ with open(
116
+ Path(self.output_dir) / "class_averages.star", "r"
117
+ ) as all_classes, open(
118
+ Path(self.output_dir) / ".class_display_tmp.star", "w"
119
+ ) as display_classes:
120
+ for line in range(200):
121
+ class_line = all_classes.readline()
122
+ if not class_line:
123
+ break
124
+ display_classes.write(class_line)
125
+ with open(
126
+ Path(self.output_dir) / "particles_all.star", "r"
127
+ ) as all_particles, open(
128
+ Path(self.output_dir) / ".particles_display_tmp.star", "w"
129
+ ) as display_particles:
130
+ for line in range(200):
131
+ particles_line = all_particles.readline()
132
+ if not particles_line:
133
+ break
134
+ display_particles.write(particles_line)
135
+ output_dobs = [
136
+ mini_montage_from_starfile(
137
+ starfile=str(Path(self.output_dir) / ".class_display_tmp.star"),
138
+ block="",
139
+ column="_rlnReferenceImage",
140
+ outputdir=self.output_dir,
141
+ nimg=100,
142
+ title="Selected 2D classes",
143
+ ),
144
+ mini_montage_from_starfile(
145
+ starfile=str(Path(self.output_dir) / ".particles_display_tmp.star"),
146
+ block="particles",
147
+ column="_rlnImageName",
148
+ outputdir=self.output_dir,
149
+ nimg=20,
150
+ title="Examples of selected particles",
151
+ ),
152
+ ]
153
+ (Path(self.output_dir) / ".class_display_tmp.star").unlink()
154
+ (Path(self.output_dir) / ".particles_display_tmp.star").unlink()
155
+ return output_dobs
File without changes
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import json
5
+ import logging
6
+ import math
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import requests
12
+ import workflows.recipe
13
+ import zocalo.configuration
14
+ from importlib_metadata import entry_points
15
+ from pydantic import BaseModel, Field
16
+ from workflows.services.common_service import CommonService
17
+ from zocalo.util import slurm
18
+
19
+
20
+ class JobSubmissionParameters(BaseModel):
21
+ scheduler: str = "slurm"
22
+ cluster: Optional[str]
23
+ partition: Optional[str]
24
+ prefer: Optional[str]
25
+ job_name: Optional[str]
26
+ environment: Optional[dict[str, str]] = None
27
+ cpus_per_task: Optional[int] = None
28
+ tasks: Optional[int] = None
29
+ nodes: Optional[int]
30
+ memory_per_node: Optional[int] = None
31
+ gpus_per_node: Optional[str] = None
32
+ min_memory_per_cpu: Optional[int] = Field(
33
+ None, description="Minimum real memory per cpu (MB)"
34
+ )
35
+ time_limit: Optional[datetime.timedelta] = None
36
+ gpus: Optional[int] = None
37
+ exclusive: bool = False
38
+ account: Optional[str]
39
+ commands: str | list[str]
40
+ qos: Optional[str]
41
+
42
+
43
+ def submit_to_slurm(
44
+ params: JobSubmissionParameters,
45
+ working_directory: Path,
46
+ logger: logging.Logger,
47
+ zc: zocalo.configuration,
48
+ ) -> int | None:
49
+ api = slurm.SlurmRestApi.from_zocalo_configuration(zc)
50
+
51
+ script = params.commands
52
+ if not isinstance(script, str):
53
+ script = "\n".join(script)
54
+ script = f"#!/bin/bash\n. /etc/profile.d/modules.sh\n{script}"
55
+
56
+ logger.debug(f"Submitting script to Slurm:\n{script}")
57
+ if params.time_limit:
58
+ time_limit_minutes = math.ceil(params.time_limit.total_seconds() / 60)
59
+ else:
60
+ time_limit_minutes = None
61
+ job_submission = slurm.models.JobSubmission(
62
+ script=script,
63
+ job=slurm.models.JobProperties(
64
+ partition=params.partition,
65
+ prefer=params.prefer,
66
+ name=params.job_name,
67
+ cpus_per_task=params.cpus_per_task,
68
+ tasks=params.tasks,
69
+ nodes=[params.nodes, params.nodes] if params.nodes else params.nodes,
70
+ gpus_per_node=params.gpus_per_node,
71
+ memory_per_node=params.memory_per_node,
72
+ environment=os.environ
73
+ if params.environment is None
74
+ else params.environment,
75
+ memory_per_cpu=params.min_memory_per_cpu,
76
+ time_limit=time_limit_minutes,
77
+ gpus=params.gpus,
78
+ # exclusive=params.exclusive,
79
+ account=params.account,
80
+ current_working_directory=os.fspath(working_directory),
81
+ qos=params.qos,
82
+ ),
83
+ )
84
+ try:
85
+ response = api.submit_job(job_submission)
86
+ except requests.HTTPError as e:
87
+ logger.error(f"Failed Slurm job submission: {e}\n" f"{e.response.text}")
88
+ return None
89
+ if response.errors:
90
+ error_message = "\n".join(f"{e.errno}: {e.error}" for e in response.errors)
91
+ logger.error(f"Failed Slurm job submission: {error_message}")
92
+ return None
93
+ return response.job_id
94
+
95
+
96
+ class ClusterSubmission(CommonService):
97
+ """A service to start new jobs on a slurm cluster."""
98
+
99
+ # Human readable service name
100
+ _service_name = "EMCluster"
101
+
102
+ # Logger name
103
+ _logger_name = "cryoemservices.services.cluster"
104
+
105
+ def __init__(self, *args, **kwargs):
106
+ super().__init__(*args, **kwargs)
107
+ self.schedulers = {}
108
+
109
+ def initializing(self):
110
+ """Subscribe to the cluster submission queue.
111
+ Received messages must be acknowledged."""
112
+ self.log.info("Cluster submission service starting")
113
+
114
+ self.schedulers = {
115
+ f.name: f.load()
116
+ for f in entry_points(group="cryoemservices.services.cluster.schedulers")
117
+ }
118
+ self.log.debug(f"Supported schedulers: {', '.join(self.schedulers.keys())}")
119
+ workflows.recipe.wrap_subscribe(
120
+ self._transport,
121
+ "cluster.submission",
122
+ self.run_submit_job,
123
+ acknowledgement=True,
124
+ log_extender=self.extend_log,
125
+ )
126
+
127
+ def run_submit_job(self, rw, header, message):
128
+ """Submit cluster job according to message."""
129
+
130
+ parameters = rw.recipe_step["parameters"]
131
+ cluster_params = JobSubmissionParameters(**parameters.get("cluster", {}))
132
+
133
+ if not isinstance(cluster_params.commands, str):
134
+ cluster_params.commands = "\n".join(cluster_params.commands)
135
+
136
+ if "recipefile" in parameters:
137
+ recipefile = parameters["recipefile"]
138
+ try:
139
+ Path(recipefile).parent.mkdir(parents=True, exist_ok=True)
140
+ except OSError:
141
+ self.log.error(f"Cannot make directory for {recipefile}")
142
+ self._transport.nack(header)
143
+ return
144
+ self.log.debug("Writing recipe to %s", recipefile)
145
+ cluster_params.commands = cluster_params.commands.replace(
146
+ "$RECIPEFILE", recipefile
147
+ )
148
+ with open(recipefile, "w") as fh:
149
+ fh.write(rw.recipe.pretty())
150
+ if "recipeenvironment" in parameters:
151
+ recipeenvironment = parameters["recipeenvironment"]
152
+ try:
153
+ Path(recipeenvironment).parent.mkdir(parents=True, exist_ok=True)
154
+ except OSError:
155
+ self.log.error(f"Cannot make directory for {recipeenvironment}")
156
+ self._transport.nack(header)
157
+ return
158
+ self.log.debug("Writing recipe environment to %s", recipeenvironment)
159
+ cluster_params.commands = cluster_params.commands.replace(
160
+ "$RECIPEENV", recipeenvironment
161
+ )
162
+ with open(recipeenvironment, "w") as fh:
163
+ json.dump(
164
+ rw.environment, fh, sort_keys=True, indent=2, separators=(",", ": ")
165
+ )
166
+ if "recipewrapper" in parameters:
167
+ recipewrapper = parameters["recipewrapper"]
168
+ try:
169
+ Path(recipewrapper).parent.mkdir(parents=True, exist_ok=True)
170
+ except OSError:
171
+ self.log.error(f"Cannot make directory for {recipewrapper}")
172
+ self._transport.nack(header)
173
+ return
174
+ self.log.debug("Storing serialized recipe wrapper in %s", recipewrapper)
175
+ cluster_params.commands = cluster_params.commands.replace(
176
+ "$RECIPEWRAP", recipewrapper
177
+ )
178
+ with open(recipewrapper, "w") as fh:
179
+ json.dump(
180
+ {
181
+ "recipe": rw.recipe.recipe,
182
+ "recipe-pointer": rw.recipe_pointer,
183
+ "environment": rw.environment,
184
+ "recipe-path": rw.recipe_path,
185
+ "payload": rw.payload,
186
+ },
187
+ fh,
188
+ indent=2,
189
+ separators=(",", ": "),
190
+ )
191
+
192
+ if "workingdir" not in parameters or not parameters["workingdir"].startswith(
193
+ "/"
194
+ ):
195
+ self.log.error(
196
+ "No absolute working directory specified. Will not run cluster job"
197
+ )
198
+ self._transport.nack(header)
199
+ return
200
+ working_directory = Path(parameters["workingdir"])
201
+ try:
202
+ working_directory.mkdir(parents=True, exist_ok=True)
203
+ except OSError as e:
204
+ self.log.error(
205
+ "Could not create working directory: %s", str(e), exc_info=True
206
+ )
207
+ self._transport.nack(header)
208
+ return
209
+
210
+ submit_to_scheduler = self.schedulers.get(cluster_params.scheduler)
211
+
212
+ jobnumber = submit_to_scheduler(
213
+ cluster_params, working_directory, self.log, zc=self.config
214
+ )
215
+ if not jobnumber:
216
+ self._transport.nack(header)
217
+ return
218
+
219
+ # Conditionally acknowledge receipt of the message
220
+ txn = self._transport.transaction_begin(subscription_id=header["subscription"])
221
+ self._transport.ack(header, transaction=txn)
222
+
223
+ # Send results onwards
224
+ rw.set_default_channel("job_submitted")
225
+ rw.send({"jobid": jobnumber}, transaction=txn)
226
+
227
+ # Commit transaction
228
+ self._transport.transaction_commit(txn)
229
+ self.log.info(f"Submitted job {jobnumber} to {cluster_params.cluster}")