1010from magicclass .types import Optional , Path
1111from 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-
2713from cylindra .const import FileFilter , nm
2814from cylindra .core import ACTIVE_WIDGETS
2915from cylindra .plugin import register_function
3016from cylindra .widget_utils import add_molecules
3117from cylindra .widgets import CylindraMainWidget
3218from 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
3422if 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-
471446class 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:
543524def _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
593578def _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-
664636def _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 ,
0 commit comments