Skip to content

Commit 440f8fe

Browse files
authored
Merge pull request #203 from hanjinliu/relion-job
Support importing RELION 3D class job
2 parents d2526da + 1f67c4e commit 440f8fe

File tree

9 files changed

+273
-62
lines changed

9 files changed

+273
-62
lines changed

cylindra/widgets/batch/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def _new_projects_from_table(
189189
prj.project_path = save_path
190190
self.constructor.projects._add(prj.project_path)
191191
self.save_batch_project(save_path=save_root)
192+
self.show()
192193

193194
@set_design(text=capitalize, location=constructor)
194195
@thread_worker

cylindra/widgets/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3064,6 +3064,8 @@ def rotate_molecule_toward_spline(
30643064
the splines.
30653065
"""
30663066
layer = assert_layer(layer, self.parent_viewer)
3067+
if self.splines.count() == 0:
3068+
raise ValueError("No spline exists in the tomogram.")
30673069
mole = layer.molecules
30683070
if spline_id_column == "":
30693071
dist_stack = []
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
import numpy as np
6+
7+
try:
8+
import starfile
9+
except ImportError:
10+
11+
class _starfile_module:
12+
def __getattr__(self, name):
13+
raise ImportError(
14+
"The 'starfile' package is required for RELION I/O functions. "
15+
"Please install it via 'pip install starfile'."
16+
)
17+
18+
starfile = _starfile_module()
19+
20+
21+
def get_optimisation_set_star(job_dir_path: Path) -> Path:
22+
if fp := _relion_job_get_last(job_dir_path, "run_it*_optimisation_set.star"):
23+
return fp
24+
raise ValueError(
25+
f"No optimisation set star files found in {job_dir_path}. "
26+
"Please ensure at least one iteration has finished."
27+
)
28+
29+
30+
def get_run_data_star(job_dir_path: Path) -> Path | None:
31+
return _relion_job_get_last(job_dir_path, "run_it*_data.star")
32+
33+
34+
def _relion_job_get_last(job_dir_path: Path, pattern: str) -> Path | None:
35+
path_list = sorted(job_dir_path.glob(pattern), key=lambda p: p.stem)
36+
if len(path_list) == 0:
37+
return None
38+
return path_list[-1]
39+
40+
41+
def relion_project_path(path: Path) -> Path:
42+
return path.parent.parent
43+
44+
45+
def get_job_type(job_dir: Path) -> str:
46+
"""Determine the type of RELION job based on the directory structure."""
47+
if (job_star_path := job_dir / "job.star").exists():
48+
return starfile.read(job_star_path, always_dict=True)["job"]["rlnJobTypeLabel"]
49+
raise ValueError(f"{job_dir} is not a RELION job folder.")
50+
51+
52+
def shape_to_center_zyx(shape: tuple[int, int, int], scale: float) -> np.ndarray:
53+
return (np.array(shape) / 2 - 1) * scale
54+
55+
56+
def strip_relion5_prefix(name: str):
57+
"""Strip the RELION 5.0 "rec" prefix from the name."""
58+
if name.startswith("rec_"):
59+
name = name[4:]
60+
if "." in name:
61+
name = name.split(".")[0]
62+
return name

cylindra_builtins/relion/io.py

Lines changed: 34 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,14 @@
1010
from magicclass.types import Optional, Path
1111
from magicclass.widgets import ConsoleTextEdit
1212

13-
try:
14-
import starfile
15-
except ImportError:
16-
17-
class _starfile_module:
18-
def __getattr__(self, name):
19-
raise ImportError(
20-
"The 'starfile' package is required for RELION I/O functions. "
21-
"Please install it via 'pip install starfile'."
22-
)
23-
24-
starfile = _starfile_module()
25-
26-
2713
from cylindra.const import FileFilter, nm
2814
from cylindra.core import ACTIVE_WIDGETS
2915
from cylindra.plugin import register_function
3016
from cylindra.widget_utils import add_molecules
3117
from cylindra.widgets import CylindraMainWidget
3218
from cylindra.widgets._annotated import MoleculesLayersType, assert_list_of_layers
19+
from cylindra_builtins.relion import _relion_utils
20+
from cylindra_builtins.relion._relion_utils import starfile
3321

3422
if TYPE_CHECKING:
3523
from cylindra.components.tomogram import CylTomogram
@@ -131,7 +119,9 @@ def save_molecules(
131119
"""
132120
save_path = Path(save_path)
133121
layers = assert_list_of_layers(layers, ui.parent_viewer)
134-
tomo_name = tomo_name_override or _strip_relion5_prefix(ui.tomogram.image.name)
122+
tomo_name = tomo_name_override or _relion_utils.strip_relion5_prefix(
123+
ui.tomogram.image.name
124+
)
135125
df = _mole_to_star_df(
136126
[layer.molecules for layer in layers],
137127
ui.tomogram,
@@ -261,7 +251,7 @@ def _iter_dataframe_from_path_sets(
261251
for path_info in path_sets:
262252
path_info = PathInfo(*path_info)
263253
prj = path_info.project_instance(missing_ok=False)
264-
tomo_name = _strip_relion5_prefix(path_info.image.stem)
254+
tomo_name = _relion_utils.strip_relion5_prefix(path_info.image.stem)
265255
img = path_info.lazy_imread()
266256
tomo = CylTomogram.from_image(
267257
img,
@@ -412,8 +402,8 @@ def _parse_relion_job(path, project_root):
412402
if path.name != "job.star" or not path.is_file() or not path.exists():
413403
raise ValueError(f"Path must be an existing RELION job.star file, got {path}")
414404
job_dir_path = Path(path).parent
415-
rln_project_path = _relion_project_path(job_dir_path)
416-
jobtype = _get_job_type(job_dir_path)
405+
rln_project_path = _relion_utils.relion_project_path(job_dir_path)
406+
jobtype = _relion_utils.get_job_type(job_dir_path)
417407
if project_root is None:
418408
project_root = job_dir_path / "cylindra"
419409
if jobtype in ("relion.reconstructtomograms", "relion.denoisetomo"):
@@ -438,36 +428,21 @@ def _parse_relion_job(path, project_root):
438428
paths, scales, moles, tilt_models = _parse_optimisation_star(
439429
opt_star_path, rln_project_path
440430
)
441-
elif jobtype in ("relion.initialmodel.tomo", "relion.refine3d.tomo"):
442-
opt_set_path_list = sorted(
443-
job_dir_path.glob("run_it*_optimisation_set.star"),
444-
key=lambda p: p.stem,
445-
)
446-
if len(opt_set_path_list) == 0:
447-
raise ValueError(
448-
f"No optimisation set star files found in {job_dir_path}. "
449-
"Please ensure at least one iteration has finished."
450-
)
451-
opt_star_path = opt_set_path_list[-1]
431+
elif jobtype in (
432+
"relion.initialmodel.tomo",
433+
"relion.class3d",
434+
"relion.refine3d.tomo",
435+
):
436+
opt_star_path = _relion_utils.get_optimisation_set_star(job_dir_path)
437+
run_data_path = _relion_utils.get_run_data_star(job_dir_path)
452438
paths, scales, moles, tilt_models = _parse_optimisation_star(
453-
opt_star_path, rln_project_path
439+
opt_star_path, rln_project_path, run_data_path
454440
)
455441
else:
456442
raise ValueError(f"Job {job_dir_path.name} is not a supported RELION job.")
457443
return project_root, paths, scales, moles, tilt_models
458444

459445

460-
def _relion_project_path(path: Path) -> Path:
461-
return path.parent.parent
462-
463-
464-
def _get_job_type(job_dir: Path) -> str:
465-
"""Determine the type of RELION job based on the directory structure."""
466-
if (job_star_path := job_dir / "job.star").exists():
467-
return starfile.read(job_star_path, always_dict=True)["job"]["rlnJobTypeLabel"]
468-
raise ValueError(f"{job_dir} is not a RELION job folder.")
469-
470-
471446
class TomogramStar:
472447
"""Object to parse tomograms.star file."""
473448

@@ -518,12 +493,18 @@ def iter_tomo_shapes(self) -> Iterator[tuple[float, float, float]]:
518493
yield from zip(nzs, nys, nxs, strict=False)
519494

520495

521-
def _parse_optimisation_star(opt_star_path: Path, rln_project_path: Path):
496+
def _parse_optimisation_star(
497+
opt_star_path: Path,
498+
rln_project_path: Path,
499+
run_data_path: Path | None = None,
500+
):
522501
paths = []
523502
scales = []
524503
molecules = []
525504
tilt_models = []
526-
for item in _iter_from_optimisation_star(opt_star_path, rln_project_path):
505+
for item in _iter_from_optimisation_star(
506+
opt_star_path, rln_project_path, run_data_path
507+
):
527508
paths.append(rln_project_path / item.tomo_path)
528509
scales.append(item.scale)
529510
molecules.append(item.molecules)
@@ -543,6 +524,7 @@ class OptimizationSetItem:
543524
def _iter_from_optimisation_star(
544525
path: Path,
545526
rln_project_path: Path,
527+
run_data_path: Path | None = None,
546528
) -> "Iterator[OptimizationSetItem]":
547529
opt_star_df = starfile.read(path)
548530
if isinstance(opt_star_df, pd.DataFrame):
@@ -557,13 +539,16 @@ def _iter_from_optimisation_star(
557539
tomo_paths = list(tomostar.iter_tomo_paths(rln_project_path))
558540
tomo_shapes = list(tomostar.iter_tomo_shapes())
559541
tilt_models = list(tomostar.iter_tilt_models(rln_project_path))
560-
particles_df = starfile.read(rln_project_path / particles_path)
542+
if run_data_path is None:
543+
particles_df = starfile.read(rln_project_path / particles_path)
544+
else:
545+
particles_df = starfile.read(run_data_path)
561546

562547
if isinstance(particles_df, dict):
563548
particles_df = particles_df["particles"]
564549
assert isinstance(particles_df, pd.DataFrame)
565550
name_to_center_map = {
566-
tomo_name: _shape_to_center_zyx(tomo_shape, sc_nm)
551+
tomo_name: _relion_utils.shape_to_center_zyx(tomo_shape, sc_nm)
567552
for tomo_name, tomo_shape, sc_nm in zip(
568553
tomo_names, tomo_shapes, scale_nm, strict=False
569554
)
@@ -592,7 +577,7 @@ def _iter_from_optimisation_star(
592577

593578
def _read_star(path: str, tomo: "CylTomogram") -> dict[str, Molecules]:
594579
fpath = Path(path)
595-
center_zyx = _shape_to_center_zyx(tomo.image.shape, tomo.scale)
580+
center_zyx = _relion_utils.shape_to_center_zyx(tomo.image.shape, tomo.scale)
596581
star = starfile.read(fpath)
597582
if not isinstance(star, dict):
598583
star = {"particles": star} # assume particles block
@@ -648,19 +633,6 @@ def _particles_to_molecules(
648633
return {default_key: mole}
649634

650635

651-
def _shape_to_center_zyx(shape: tuple[int, int, int], scale: nm) -> np.ndarray:
652-
return (np.array(shape) / 2 - 1) * scale
653-
654-
655-
def _strip_relion5_prefix(name: str):
656-
"""Strip the RELION 5.0 rec prefix from the name."""
657-
if name.startswith("rec_"):
658-
name = name[4:]
659-
if "." in name:
660-
name = name.split(".")[0]
661-
return name
662-
663-
664636
def _mole_to_star_df(
665637
moles: list[Molecules],
666638
tomo: "CylTomogram",
@@ -682,7 +654,9 @@ def _mole_to_star_df(
682654
ROT_COLUMNS[2]: euler_angle[:, 2],
683655
}
684656
if centered:
685-
centerz, centery, centerx = _shape_to_center_zyx(tomo.image.shape, scale)
657+
centerz, centery, centerx = _relion_utils.shape_to_center_zyx(
658+
tomo.image.shape, scale
659+
)
686660
# Angstrom
687661
_pos_dict = {
688662
POS_CENTERED[2]: (mole.pos[:, 2] - centerx + orig.x) * 10,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
2+
# version 50001
3+
4+
data_job
5+
6+
_rlnJobTypeLabel relion.class3d
7+
_rlnJobIsContinue 0
8+
_rlnJobIsTomo 1
9+
10+
11+
# version 50001
12+
13+
data_joboptions_values
14+
15+
loop_
16+
_rlnJobOptionVariable #1
17+
_rlnJobOptionValue #2
18+
allow_coarser No
19+
ctf_intact_first_peak No
20+
do_apply_helical_symmetry Yes
21+
do_blush No
22+
do_combine_thru_disc No
23+
do_ctf_correction Yes
24+
do_fast_subsets No
25+
do_helix No
26+
do_local_ang_searches Yes
27+
do_local_search_helical_symmetry No
28+
do_pad1 No
29+
do_parallel_discio Yes
30+
do_preread_images No
31+
do_queue No
32+
do_zero_mask Yes
33+
dont_skip_align Yes
34+
fn_cont ""
35+
fn_mask ""
36+
fn_ref Refine3D/job015/run_class001.mrc
37+
gpu_ids ""
38+
helical_nr_asu 1
39+
helical_range_distance -1
40+
helical_rise_inistep 0
41+
helical_rise_initial 0
42+
helical_rise_max 0
43+
helical_rise_min 0
44+
helical_tube_inner_diameter -1
45+
helical_tube_outer_diameter -1
46+
helical_twist_inistep 0
47+
helical_twist_initial 0
48+
helical_twist_max 0
49+
helical_twist_min 0
50+
helical_z_percentage 30
51+
highres_limit -1
52+
in_optimisation ""
53+
in_particles XXX
54+
in_tomograms YYY
55+
in_trajectories ""
56+
ini_high 60
57+
keep_tilt_prior_fixed Yes
58+
min_dedicated 24
59+
nr_classes 9
60+
nr_iter 25
61+
nr_mpi 5
62+
nr_pool 30
63+
nr_threads 6
64+
offset_range 5
65+
offset_step 1
66+
other_args ""
67+
particle_diameter 230
68+
qsub sbatch
69+
qsubscript /public/EM/RELION/relion-slurm-gpu-4.0.csh
70+
queuename openmpi
71+
range_psi 10
72+
range_rot -1
73+
range_tilt 15
74+
ref_correct_greyscale Yes
75+
relax_sym ""
76+
sampling "1.8 degrees"
77+
scratch_dir $RELION_SCRATCH_DIR
78+
sigma_angles 5
79+
sigma_tilt 10
80+
sym_name C1
81+
tau_fudge 1
82+
trust_ref_size Yes
83+
use_direct_entries Yes
84+
use_gpu Yes

0 commit comments

Comments
 (0)