Skip to content

Commit cdcc32e

Browse files
IMOD processing bypass and file copying (#279)
Updates imod dockerfile to include some missing libraries, then runs using -bypass to avoid the java dependency. Also sets up a mechanism to copy the output files to a higher level. This was requested by b24 as the output folders can be messy and they want an easy way to access the tomograms.
1 parent 5a7beb3 commit cdcc32e

12 files changed

Lines changed: 158 additions & 41 deletions

Dockerfiles/tomo_align

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# This Dockerfile is used for GPU AreTomo2 processing
1+
# This Dockerfile is used for GPU AreTomo3 or IMOD processing
22
FROM rockylinux:8 AS conda-build
33

44
# Set up conda environment
@@ -34,15 +34,16 @@ ENV PATH=/install/venv/bin:${PATH}
3434
ENV LD_LIBRARY_PATH=/install/venv/lib:${LD_LIBRARY_PATH}
3535

3636
# Install IMOD
37+
RUN yum groupinstall "Development Tools" -y
38+
RUN yum install mesa-libGL -y
3739
RUN mkdir imod_install && \
3840
curl https://bio3d.colorado.edu/imod/AMD64-RHEL5/imod_5.1.9_RHEL8-64_CUDA12.0.sh > imod_5.1.9_RHEL8-64_CUDA12.0.sh && \
3941
chmod +x imod_5.1.9_RHEL8-64_CUDA12.0.sh && \
40-
ln -s /install/venv/bin/python /usr/bin/python3 && \
4142
./imod_5.1.9_RHEL8-64_CUDA12.0.sh -dir imod_install -skip -y
4243
ENV PATH=/imod_install/IMOD/bin:${PATH}
4344
ENV IMOD_DIR=/imod_install/IMOD
4445

4546
# Install AreTomo
46-
COPY --chown="${userid}":"${groupid}" packages/AreTomo2 /AreTomo2
47-
ENV PATH=/AreTomo2:${PATH}
48-
RUN chmod +x /AreTomo2
47+
COPY --chown="${userid}":"${groupid}" packages/AreTomo3 /AreTomo3
48+
ENV PATH=/AreTomo3:${PATH}
49+
RUN chmod +x /AreTomo3

recipes/ispyb/sxt-aretomo.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"success": 7
2323
},
2424
"parameters": {
25+
"copy_output": true,
2526
"dark_tol": 0,
2627
"manual_tilt_offset": "{manual_tilt_offset}",
2728
"out_bin": 1,

recipes/ispyb/sxt-imod-beads-wbp.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"success": 7
2323
},
2424
"parameters": {
25+
"copy_output": true,
2526
"manual_tilt_offset": "{manual_tilt_offset}",
2627
"out_bin": 1,
2728
"patch": 0,
@@ -30,7 +31,8 @@
3031
"stack_file": "{stack_file}",
3132
"tilt_axis": 0,
3233
"txrm_file": "{txrm_file}",
33-
"wbp": 1
34+
"wbp": 1,
35+
"xrm_reference": "{xrm_reference}"
3436
},
3537
"queue": "tomo_align_imod",
3638
"service": "ImodTomoAlign"

recipes/ispyb/sxt-imod-patch-wbp.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"success": 7
2323
},
2424
"parameters": {
25+
"copy_output": true,
2526
"manual_tilt_offset": "{manual_tilt_offset}",
2627
"out_bin": 1,
2728
"patch": 1,
@@ -30,7 +31,8 @@
3031
"stack_file": "{stack_file}",
3132
"tilt_axis": 0,
3233
"txrm_file": "{txrm_file}",
33-
"wbp": 1
34+
"wbp": 1,
35+
"xrm_reference": "{xrm_reference}"
3436
},
3537
"queue": "tomo_align_imod",
3638
"service": "ImodTomoAlign"

src/cryoemservices/services/denoise.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import re
4+
import shutil
45
import subprocess
56
from pathlib import Path
67
from typing import List, Optional
@@ -48,6 +49,7 @@ class DenoiseParameters(BaseModel):
4849
patch_padding: Optional[int] = None # 48
4950
device: Optional[int] = None # -2
5051
cleanup_output: bool = True
52+
copy_output: bool = False
5153
visits_for_slurm: Optional[list] = ["bi", "cm", "nr", "nt"]
5254
relion_options: RelionServiceOptions
5355

@@ -305,6 +307,7 @@ def denoise(self, rw, header: dict, message: dict):
305307
"tomogram": str(denoised_full_path),
306308
"output_dir": str(segmentation_dir),
307309
"pixel_size": str(denoise_params.relion_options.pixel_size_downscaled),
310+
"copy_output": denoise_params.copy_output,
308311
"relion_options": dict(denoise_params.relion_options),
309312
}
310313
cryolo_parameters = {
@@ -325,6 +328,18 @@ def denoise(self, rw, header: dict, message: dict):
325328
}
326329
rw.send_to("ispyb_connector", ispyb_parameters)
327330

331+
# Optionally copy output file
332+
if denoise_params.copy_output:
333+
# Take file name for Relion-type projects, or folder name for SXT-style
334+
tomo_name = (
335+
denoised_full_path.name
336+
if re.match(".*/job[0-9]+/.*", str(denoised_full_path))
337+
else f"{denoised_full_path.parent.parent}_denoised.mrc"
338+
)
339+
shutil.copy(
340+
denoised_full_path, denoised_full_path.parent.parent.parent / tomo_name
341+
)
342+
328343
self.log.info(f"Done denoising for {denoise_params.volume}")
329344
rw.transport.ack(header)
330345
return

src/cryoemservices/services/membrain_seg.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import re
4+
import shutil
35
import subprocess
46
from pathlib import Path
57
from typing import Optional
@@ -36,6 +38,7 @@ class MembrainSegParameters(BaseModel):
3638
segmentation_threshold: float = 0.0
3739
cleanup_output: bool = True
3840
submit_to_slurm: bool = False
41+
copy_output: bool = False
3942
relion_options: RelionServiceOptions
4043

4144

@@ -266,6 +269,16 @@ def membrain_seg(self, rw, header: dict, message: dict):
266269
},
267270
)
268271

272+
# Optionally copy output file
273+
if membrain_seg_params.copy_output:
274+
# Take file name for Relion-type projects, or folder name for SXT-style
275+
tomo_name = (
276+
segmented_path.name
277+
if re.match(".*/job[0-9]+/.*", str(segmented_path))
278+
else f"{segmented_path.parent.parent}_segmented.mrc"
279+
)
280+
shutil.copy(segmented_path, segmented_path.parent.parent.parent / tomo_name)
281+
269282
self.log.info(f"Done segmentation for {membrain_seg_params.tomogram}")
270283
rw.transport.ack(header)
271284
return

src/cryoemservices/services/tomo_align_aretomo.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import os.path
66
import re
7+
import shutil
78
import subprocess
89
import time
910
from pathlib import Path
@@ -135,6 +136,7 @@ class AreTomoParameters(BaseModel):
135136
interpolation_correction: int | None = None
136137
dark_tol: float | None = None
137138
manual_tilt_offset: float | None = None
139+
copy_output: bool = False
138140
visits_for_slurm: list | None = ["bi", "cm", "nr", "nt"]
139141
relion_options: RelionServiceOptions
140142

@@ -835,6 +837,7 @@ def _tilt(file_list_for_tilts):
835837
"volume": str(aretomo_output_path),
836838
"output_dir": str(denoise_dir),
837839
"relion_options": dict(tomo_params.relion_options),
840+
"copy_output": tomo_params.copy_output,
838841
},
839842
)
840843

@@ -852,6 +855,22 @@ def _tilt(file_list_for_tilts):
852855
):
853856
tmp_file.unlink()
854857

858+
# Optionally copy output file
859+
if tomo_params.copy_output:
860+
# Take file name for Relion-type projects, or folder name for SXT-style
861+
stack_name = (
862+
Path(tomo_params.stack_file).name
863+
if re.match(".*/job[0-9]+/.*", tomo_params.stack_file)
864+
else f"{Path(tomo_params.stack_file).parent.parent}_stack.mrc"
865+
)
866+
tomo_name = (
867+
aretomo_output_path.name
868+
if re.match(".*/job[0-9]+/.*", str(aretomo_output_path))
869+
else f"{aretomo_output_path.parent.parent}_volume.mrc"
870+
)
871+
shutil.copy(Path(tomo_params.stack_file), project_dir.parent / stack_name)
872+
shutil.copy(aretomo_output_path, project_dir.parent / tomo_name)
873+
855874
# Update success processing status
856875
rw.send_to("success", {})
857876
self.log.info(f"Done tomogram alignment for {tomo_params.stack_file}")

src/cryoemservices/services/tomo_align_imod.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import shutil
12
import subprocess
23
from pathlib import Path
34
from typing import Optional
@@ -32,6 +33,7 @@ class ImodTomoParameters(BaseModel):
3233
local_alignment: int = 0
3334
flip_vol: int = 1
3435
manual_tilt_offset: Optional[float] = None
36+
copy_output: bool = False
3537
cpus: int = 4
3638

3739

@@ -150,6 +152,7 @@ def tomo_align(self, rw, header: dict, message: dict):
150152
str(adoc_file),
151153
"-cpus",
152154
str(tomo_params.cpus),
155+
"-bypass",
153156
],
154157
capture_output=True,
155158
)
@@ -272,6 +275,7 @@ def tomo_align(self, rw, header: dict, message: dict):
272275
{
273276
"volume": str(imod_output_path),
274277
"output_dir": str(imod_output_path.parent.parent / "Denoise"),
278+
"copy_output": tomo_params.copy_output,
275279
"relion_options": {},
276280
},
277281
)
@@ -290,6 +294,14 @@ def tomo_align(self, rw, header: dict, message: dict):
290294
):
291295
tmp_file.unlink()
292296

297+
# Optionally copy output file
298+
if tomo_params.copy_output:
299+
shutil.copy(
300+
imod_output_path,
301+
imod_output_path.parent.parent.parent
302+
/ f"{imod_output_path.parent.parent}_volume.mrc",
303+
)
304+
293305
# Update success processing status
294306
rw.send_to("success", {})
295307
self.log.info(f"Done tomogram alignment for {tomo_params.stack_file}")

tests/services/test_denoise.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import subprocess
45
from unittest import mock
56

67
import pytest
@@ -188,6 +189,7 @@ def test_denoise_local_topaz_service(
188189
"tomogram": f"{tmp_path}/Denoise/job007/denoised/test_stack_aretomo.denoised.mrc",
189190
"output_dir": f"{tmp_path}/Segmentation/job008/tomograms",
190191
"pixel_size": "1.0",
192+
"copy_output": False,
191193
"relion_options": output_relion_options,
192194
},
193195
)
@@ -216,9 +218,18 @@ def test_denoise_local_subprocess_service(
216218
This should call the mock subprocess then send messages on to
217219
the membrain-seg and images services.
218220
"""
219-
mock_subprocess().returncode = 0
220-
mock_subprocess().stdout = "stdout".encode("ascii")
221-
mock_subprocess().stderr = "stderr".encode("ascii")
221+
222+
def touch_denoise_output(*args, **kwargs):
223+
(tmp_path / "Denoise/job007/denoised").mkdir(parents=True, exist_ok=True)
224+
(tmp_path / "Denoise/job007/denoised/test_stack_aretomo.denoised.mrc").touch()
225+
return subprocess.CompletedProcess(
226+
"",
227+
returncode=0,
228+
stdout="stdout".encode("ascii"),
229+
stderr="stderr".encode("ascii"),
230+
)
231+
232+
mock_subprocess.side_effect = touch_denoise_output
222233

223234
header = {
224235
"message-id": mock.sentinel,
@@ -250,6 +261,7 @@ def test_denoise_local_subprocess_service(
250261
"patch_size": 96,
251262
"patch_padding": 48,
252263
"device": "-2",
264+
"copy_output": True,
253265
"relion_options": {"pixel_size_downscaled": 1},
254266
}
255267
output_relion_options = dict(RelionServiceOptions())
@@ -310,7 +322,10 @@ def test_denoise_local_subprocess_service(
310322
"-d",
311323
"-2",
312324
]
313-
mock_subprocess.assert_any_call(denoise_command, capture_output=True)
325+
mock_subprocess.assert_called_once_with(denoise_command, capture_output=True)
326+
327+
# Check output copy
328+
assert (tmp_path / "Denoise/test_stack_aretomo.denoised.mrc").is_file()
314329

315330
# Check the images service request
316331
assert offline_transport.send.call_count == 6
@@ -356,6 +371,7 @@ def test_denoise_local_subprocess_service(
356371
"tomogram": f"{tmp_path}/Denoise/job007/denoised/test_stack_aretomo.denoised.mrc",
357372
"output_dir": f"{tmp_path}/Segmentation/job008/tomograms",
358373
"pixel_size": "1.0",
374+
"copy_output": True,
359375
"relion_options": output_relion_options,
360376
},
361377
)
@@ -568,6 +584,7 @@ def write_denoised_files(**kwargs):
568584
"tomogram": f"{tmp_path}/cm12345-6/Denoise/job007/denoised/test_stack_aretomo.denoised.mrc",
569585
"output_dir": f"{tmp_path}/cm12345-6/Segmentation/job008/tomograms",
570586
"pixel_size": "1.0",
587+
"copy_output": False,
571588
"relion_options": output_relion_options,
572589
},
573590
)
@@ -654,6 +671,7 @@ def test_denoise_local_topaz_service_rerun(
654671
"tomogram": f"{tmp_path}/Denoise/job007/denoised/test_stack_aretomo.denoised.mrc",
655672
"output_dir": f"{tmp_path}/Segmentation/job008/tomograms",
656673
"pixel_size": "1.0",
674+
"copy_output": False,
657675
"relion_options": output_relion_options,
658676
},
659677
)

tests/services/test_membrain_seg.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import subprocess
34
from unittest import mock
45

56
import pytest
@@ -159,9 +160,20 @@ def test_membrain_seg_service_local_subprocess(
159160
Send a test message to membrain-seg for the local version
160161
This should call the mock subprocess then send messages to the images service.
161162
"""
162-
mock_subprocess().returncode = 0
163-
mock_subprocess().stdout = "stdout".encode("ascii")
164-
mock_subprocess().stderr = "stderr".encode("ascii")
163+
164+
def touch_membrain_output(*args, **kwargs):
165+
(
166+
tmp_path
167+
/ "Segmentation/job008/tomograms/test_stack_aretomo.denoised_segmented.mrc"
168+
).touch()
169+
return subprocess.CompletedProcess(
170+
"",
171+
returncode=0,
172+
stdout="stdout".encode("ascii"),
173+
stderr="stderr".encode("ascii"),
174+
)
175+
176+
mock_subprocess.side_effect = touch_membrain_output
165177

166178
header = {
167179
"message-id": mock.sentinel,
@@ -179,6 +191,7 @@ def test_membrain_seg_service_local_subprocess(
179191
"window_size": 100,
180192
"connected_component_threshold": 2,
181193
"segmentation_threshold": 4,
194+
"copy_output": True,
182195
"relion_options": {},
183196
}
184197
output_relion_options = dict(RelionServiceOptions())
@@ -213,8 +226,12 @@ def test_membrain_seg_service_local_subprocess(
213226
"--store-probabilities",
214227
"--store-connected-components",
215228
]
216-
assert mock_subprocess.call_count == 4
217-
mock_subprocess.assert_any_call(membrain_command, capture_output=True)
229+
mock_subprocess.assert_called_once_with(membrain_command, capture_output=True)
230+
231+
# Check output copy
232+
assert (
233+
tmp_path / "Segmentation/test_stack_aretomo.denoised_segmented.mrc"
234+
).is_file()
218235

219236
# Check the images service request
220237
assert offline_transport.send.call_count == 5

0 commit comments

Comments
 (0)