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.
- cryoemservices/__init__.py +3 -0
- cryoemservices/cli/__init__.py +0 -0
- cryoemservices/cli/resubmit_wrapper.py +51 -0
- cryoemservices/pipeliner_plugins/__init__.py +0 -0
- cryoemservices/pipeliner_plugins/combine_star_files.py +251 -0
- cryoemservices/pipeliner_plugins/combine_star_job.py +155 -0
- cryoemservices/services/__init__.py +0 -0
- cryoemservices/services/cluster_submission.py +229 -0
- cryoemservices/services/cryolo.py +414 -0
- cryoemservices/services/ctffind.py +331 -0
- cryoemservices/services/denoise_iris.py +317 -0
- cryoemservices/services/extract.py +439 -0
- cryoemservices/services/icebreaker.py +344 -0
- cryoemservices/services/images.py +102 -0
- cryoemservices/services/images_plugins.py +283 -0
- cryoemservices/services/ispyb.py +1085 -0
- cryoemservices/services/ispyb_buffer.py +102 -0
- cryoemservices/services/motioncorr.py +680 -0
- cryoemservices/services/motioncorr_slurm.py +323 -0
- cryoemservices/services/node_creator.py +373 -0
- cryoemservices/services/select_classes.py +528 -0
- cryoemservices/services/select_particles.py +303 -0
- cryoemservices/services/tomo_align.py +614 -0
- cryoemservices/services/tomo_align_iris.py +219 -0
- cryoemservices/util/__init__.py +0 -0
- cryoemservices/util/dispatcher_tools.py +234 -0
- cryoemservices/util/spa_output_files.py +387 -0
- cryoemservices/util/spa_relion_service_options.py +279 -0
- cryoemservices/wrappers/__init__.py +0 -0
- cryoemservices/wrappers/class2d_wrapper.py +373 -0
- cryoemservices/wrappers/class3d_wrapper.py +527 -0
- cryoemservices-0.1.3.dist-info/LICENSE +28 -0
- cryoemservices-0.1.3.dist-info/METADATA +156 -0
- cryoemservices-0.1.3.dist-info/RECORD +37 -0
- cryoemservices-0.1.3.dist-info/WHEEL +5 -0
- cryoemservices-0.1.3.dist-info/entry_points.txt +42 -0
- cryoemservices-0.1.3.dist-info/top_level.txt +1 -0
|
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}")
|