Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4c91101
Way to run parallel denoising for tilts
stephen-riggs Oct 16, 2025
b212481
Put denoising and quality into recipe
stephen-riggs Oct 16, 2025
bcd7e27
Test that denoiser gets called
stephen-riggs Oct 16, 2025
425dc2b
Put in spool and ensure exists
stephen-riggs Oct 17, 2025
ba95bf0
Add some logs
stephen-riggs Oct 17, 2025
d1f4604
Merge branch 'main' into denoise-tilts
stephen-riggs Oct 28, 2025
1b5dd52
Method for denoising using IRIS
stephen-riggs Oct 28, 2025
3e4aae1
Need to run in singularity with full path
stephen-riggs Oct 28, 2025
3619abb
Need to set the home
stephen-riggs Oct 29, 2025
fb40a80
Need proper config not file name
stephen-riggs Oct 29, 2025
eb489f8
Fix output file names
stephen-riggs Nov 4, 2025
4a48ee7
More logs to assess job status
stephen-riggs Nov 14, 2025
0ccccc5
Merge branch 'main' into denoise-tilts
stephen-riggs Nov 18, 2025
44f4134
Merge branch 'main' into denoise-tilts
stephen-riggs Nov 20, 2025
b78e556
Merge branch 'main' into denoise-tilts
stephen-riggs Nov 25, 2025
ffb2f94
Merge branch 'main' into denoise-tilts
stephen-riggs Dec 1, 2025
5b45a9d
Merge branch 'main' into denoise-tilts
stephen-riggs Jan 14, 2026
430faf8
Merge branch 'main' into denoise-tilts
stephen-riggs Jan 14, 2026
6a3fdfa
Merge branch 'main' into denoise-tilts
stephen-riggs Jan 26, 2026
d61d3e8
Fix numbering in recipe
stephen-riggs Jan 26, 2026
b57f1c0
Merge branch 'main' into denoise-tilts
stephen-riggs Jan 28, 2026
de30ee3
Merge branch 'main' into denoise-tilts
stephen-riggs Feb 11, 2026
92f7101
Merge branch 'main' into denoise-tilts
stephen-riggs Feb 18, 2026
d0adc9c
Merge branch 'main' into denoise-tilts
stephen-riggs Feb 19, 2026
54bb745
Merge branch 'main' into denoise-tilts
stephen-riggs Feb 24, 2026
df623a0
Merge branch 'main' into denoise-tilts
stephen-riggs Feb 24, 2026
5487335
Merge branch 'main' into denoise-tilts
stephen-riggs Mar 11, 2026
21a8a4e
Merge branch 'main' into denoise-tilts
stephen-riggs Mar 11, 2026
56cdde7
Merge branch 'main' into denoise-tilts
stephen-riggs Mar 17, 2026
08b0336
Merge branch 'main' into denoise-tilts
stephen-riggs Mar 18, 2026
1117a83
Merge branch 'main' into denoise-tilts
stephen-riggs Apr 10, 2026
ae03e50
Skip denoising on iris
stephen-riggs Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Helm/charts/tomo_align/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ spec:
- -c
- >-
{{ .Values.command }}
env:
- name: HOME
value: "/tmp"
volumeMounts:
- name: config-file
mountPath: /cryoemservices/config
Expand Down
2 changes: 2 additions & 0 deletions Helm/charts/tomo_align_slurm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ spec:
- >-
{{ .Values.command }}
env:
- name: TILT_DENOISING_SIF
value: "{{ .Values.tiltDenoisingSIF }}"
- name: ARETOMO2_EXECUTABLE
value: "{{ .Values.aretomo2Executable }}"
- name: ARETOMO3_EXECUTABLE
Expand Down
21 changes: 20 additions & 1 deletion recipes/ispyb/em-tomo-align.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
"node_creator": 12,
"projxy": 6,
"projxz": 6,
"success": 7
"success": 7,
"tomo_align_denoise": 13
},
"parameters": {
"denoise_tilts": 1,
"dose_per_frame": "{dose_per_frame}",
"frame_count": "{frame_count}",
"input_file_list": "{input_file_list}",
Expand Down Expand Up @@ -127,5 +129,22 @@
"queue": "node_creator",
"service": "NodeCreator"
},
"13": {
"parameters": {
"denoise_tilts": 2,
"dose_per_frame": "{dose_per_frame}",
"frame_count": "{frame_count}",
"input_file_list": "{input_file_list}",
"kv": "{kv}",
"manual_tilt_offset": "{manual_tilt_offset}",
"path_pattern": "{path_pattern}",
"pixel_size": "{pixel_size}",
"relion_options": {},
"stack_file": "{stack_file}",
"tilt_axis": "{tilt_axis}"
},
"queue": "tomo_align",
"service": "TomoAlign"
},
"start": [[1, []]]
}
61 changes: 61 additions & 0 deletions src/cryoemservices/services/tomo_align.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class TomoParameters(BaseModel):
interpolation_correction: Optional[int] = None
dark_tol: Optional[float] = None
manual_tilt_offset: Optional[float] = None
denoise_tilts: int = 0
visits_for_slurm: Optional[list] = ["bi", "cm", "nr", "nt"]
relion_options: RelionServiceOptions

Expand Down Expand Up @@ -222,6 +223,38 @@ def parse_tomo_output(self, tomo_stdout: str):
if line.startswith("Best tilt axis"):
self.alignment_quality = float(line.split()[5])

def get_denoised_tilt_name(self, tilt: str) -> str:
denoised_tilt = "/" + "/".join(
"spool" if p == "processed" else p for p in Path(tilt).parts[1:]
)
denoised_tilt = str(
Path(denoised_tilt).parent / (Path(denoised_tilt).stem + "_denoised.mrc")
)
Path(denoised_tilt).parent.mkdir(parents=True, exist_ok=True)
return denoised_tilt

def run_tilt_denoising(self, tilt_list: list[str]) -> bool:
for tilt in tilt_list:
denoised_tilt = self.get_denoised_tilt_name(tilt)
denoise_result = subprocess.run(
[
"python",
"run_denoiser.py",
"--nimage",
str(tilt),
"--dimage",
str(denoised_tilt),
],
capture_output=True,
)
if denoise_result.returncode:
self.log.error(f"Failed to denoise tilt {tilt}")
self.log.error(
f"Denoise reason: {denoise_result.stdout.decode('utf8')} {denoise_result.stderr.decode('utf8')}"
)
return False
return True

def extract_from_aln(self, tomo_parameters, alignment_output_dir, plot_path):
tomo_aln_file = None
self.thickness_pixels = None
Expand Down Expand Up @@ -385,6 +418,30 @@ def _tilt(file_list_for_tilts):
for index in sorted(tilts_to_remove, reverse=True):
self.input_file_list_of_lists.remove(self.input_file_list_of_lists[index])

# Decide whether to denoise
if tomo_params.denoise_tilts == 1:
self.log.info("Sending to tilt denoising and alignment re-run")
rw.send_to("tomo_align_denoise", {"denoise_tilts": 2})
elif tomo_params.denoise_tilts == 2:
self.log.info("Running tilt denoising")
new_input_list_of_lists = []
tilts_to_denoise = []
for tname, tangle in self.input_file_list_of_lists:
denoised_tilt = self.get_denoised_tilt_name(tname)
tilts_to_denoise.append(tname)
new_input_list_of_lists.append([denoised_tilt, tangle])
denoise_success = self.run_tilt_denoising(tilts_to_denoise)
if not denoise_success:
self.log.error("Failed to denoise tilts")
rw.transport.nack(header)
return
self.input_file_list_of_lists = new_input_list_of_lists
tomo_params.stack_file = "/" + "/".join(
"spool" if p == "processed" else p
for p in Path(tomo_params.stack_file).parts[1:]
)
Path(tomo_params.stack_file).parent.mkdir(parents=True, exist_ok=True)

# Find the input image dimensions
with mrcfile.open(self.input_file_list_of_lists[0][0]) as mrc:
mrc_header = mrc.header
Expand Down Expand Up @@ -589,6 +646,10 @@ def _tilt(file_list_for_tilts):
}
]

# Write the score somewhere
with open(aretomo_output_path.with_suffix(".com"), "a") as comfile:
comfile.write(f"\n\nAlignment quality {self.alignment_quality}")

# Find the indexes of the dark images removed by AreTomo
missing_indices = []
dark_images_file = Path(stack_name + "_DarkImgs.txt")
Expand Down
75 changes: 74 additions & 1 deletion src/cryoemservices/services/tomo_align_slurm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
import requests

from cryoemservices.services.tomo_align import TomoAlign, TomoParameters
from cryoemservices.util.slurm_submission import slurm_submission_for_services
from cryoemservices.util.slurm_submission import (
config_from_file,
slurm_submission_for_services,
wait_for_job_completion,
)


def retrieve_files(
Expand Down Expand Up @@ -104,6 +108,8 @@ def check_visit(tomo_params: TomoParameters):
visit_search = re.search(
"/[a-z]{2}[0-9]{5}-[0-9]{1,3}/", tomo_params.stack_file
)
if tomo_params.denoise_tilts == 2:
return False
if visit_search:
visit_name = visit_search[0][1:-1]
visit_code = visit_name[:2]
Expand All @@ -126,6 +132,73 @@ def parse_tomo_output_file(self, tomo_output_file: Path):
self.alignment_quality = float(line.split()[5])
tomo_file.close()

def run_tilt_denoising(self, tilt_list: list[str]) -> bool:
transfer_status = transfer_files([Path(tilt) for tilt in tilt_list])
if len(transfer_status) != len(tilt_list):
self.log.error(
f"Unable to transfer files: desired {tilt_list}, done {transfer_status}"
)
return False
self.log.info("All files transferred")

job_ids = []
final_tilts = []
for tilt in tilt_list:
denoised_tilt = self.get_denoised_tilt_name(tilt)
command = [
"python",
"/install/denoiser/run_denoiser.py",
"--nimage",
str(tilt),
"--dimage",
str(denoised_tilt),
]
tilt_job_id = slurm_submission_for_services(
log=self.log,
service_config_file=self._environment["config"],
slurm_cluster=self._environment["slurm_cluster"],
job_name="TiltDenoise",
command=command,
project_dir=Path(denoised_tilt).parent,
output_file=Path(denoised_tilt),
cpus=1,
use_gpu=True,
use_singularity=True,
cif_name=os.environ["TILT_DENOISING_SIF"],
external_filesystem=True,
wait_for_completion=False,
)
job_ids.append(tilt_job_id.returncode)
final_tilts.append(denoised_tilt)

service_config = config_from_file(self._environment["config"])
self.log.info("Waiting for completion and retrieval of output files...")
for tid, job_id in enumerate(job_ids):
job_state = wait_for_job_completion(
job_id=job_id,
logger=self.log,
service_config=service_config,
cluster_name=self._environment["slurm_cluster"],
)
retrieve_files(
job_directory=Path(final_tilts[tid]).parent,
files_to_skip=[Path(tilt_list[tid])],
basepath=str(Path(tilt_list[tid]).stem),
)
if job_state != "COMPLETED":
self.log.error(f"Job {job_id} failed with {job_state}")
self.log.info("All denoising jobs finished and output files retrieved")

for out_tilt in final_tilts:
if not Path(out_tilt).is_file():
self.log.info(f"Tilt denoising failed for {out_tilt}")
if Path(out_tilt).with_suffix(".err").is_file():
with open(Path(out_tilt).with_suffix(".err"), "r") as slurm_stderr:
stderr = slurm_stderr.read()
self.log.error(stderr)
return False
return True

def aretomo(
self,
tomo_parameters: TomoParameters,
Expand Down
3 changes: 3 additions & 0 deletions src/cryoemservices/util/slurm_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ def slurm_submission_for_services(
memory_request: int = 12000,
external_filesystem: bool = False,
extra_singularity_directories: Optional[list[str]] = None,
wait_for_completion: bool = True,
) -> subprocess.CompletedProcess:
"""Submit jobs to a slurm cluster via the RestAPI"""
# Load the service config with slurm credentials
Expand Down Expand Up @@ -371,6 +372,8 @@ def slurm_submission_for_services(

# Get the status of the submitted job from the restAPI
log.info(f"Submitted job {job_id} for {job_name} to slurm. Waiting...")
if not wait_for_completion:
return subprocess.CompletedProcess(args="", returncode=job_id)
slurm_job_state = wait_for_job_completion(
job_id=job_id,
logger=log,
Expand Down
19 changes: 19 additions & 0 deletions tests/services/test_tomo_align.py
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,25 @@ def test_parse_tomo_align_output(offline_transport):
assert service.alignment_quality == 0.07568


@mock.patch("cryoemservices.services.tomo_align.subprocess.run")
def test_run_tilt_denoising(mock_subprocess, tmp_path):
mock_subprocess().returncode = 0

tilt_in = f"{tmp_path}/processed/relion_murfey/MotionCorr/job002/Movies/tilt.mrc"
tilt_out = (
f"{tmp_path}/spool/relion_murfey/MotionCorr/job002/Movies/tilt_denoised.mrc"
)

denoised_tilt = tomo_align.run_tilt_denoising(tilt_in)

assert denoised_tilt == tilt_out

mock_subprocess.assert_called_with(
["python", "run_denoiser.py", "--nimage", tilt_in, "--dimage", tilt_out],
capture_output=True,
)


def test_resize_tomogram(tmp_path):
"""Test the reshaping of a XZY tomogram"""
with mrcfile.new(tmp_path / "test.mrc") as mrc:
Expand Down
Loading